Merge branch 'main' into randomized-tests-operation-script

Max Brunsfeld created

Change summary

.github/workflows/ci.yml                                            |   20 
Cargo.lock                                                          |  169 
Cargo.toml                                                          |    8 
README.md                                                           |   10 
assets/icons/copilot_16.svg                                         |    4 
assets/icons/copilot_disabled_16.svg                                |    2 
assets/icons/copilot_error_16.svg                                   |    2 
assets/icons/copilot_init_16.svg                                    |    1 
assets/icons/ellipsis_14.svg                                        |    3 
assets/icons/feedback_16.svg                                        |    3 
assets/icons/github-copilot-dummy.svg                               |    0 
assets/icons/leave_12.svg                                           |    3 
assets/icons/link_out_12.svg                                        |    5 
assets/icons/logo_96.svg                                            |    3 
assets/icons/speech_bubble_12.svg                                   |    3 
assets/icons/user_plus_12.svg                                       |    4 
assets/icons/user_plus_16.svg                                       |    4 
assets/icons/zed_plus_copilot_32.svg                                |    3 
assets/keymaps/atom.json                                            |   68 
assets/keymaps/default.json                                         |   21 
assets/keymaps/jetbrains.json                                       |   78 
assets/keymaps/sublime_text.json                                    |   60 
assets/keymaps/textmate.json                                        |   90 
assets/keymaps/vim.json                                             |    4 
assets/settings/default.json                                        |   49 
crates/activity_indicator/src/activity_indicator.rs                 |  147 
crates/auto_update/Cargo.toml                                       |    5 
crates/auto_update/src/auto_update.rs                               |    4 
crates/auto_update/src/update_notification.rs                       |    2 
crates/breadcrumbs/Cargo.toml                                       |    1 
crates/breadcrumbs/src/breadcrumbs.rs                               |   66 
crates/call/Cargo.toml                                              |    2 
crates/call/src/call.rs                                             |   21 
crates/call/src/room.rs                                             |  147 
crates/cli/Cargo.toml                                               |    3 
crates/client/Cargo.toml                                            |    6 
crates/client/src/client.rs                                         |   38 
crates/client/src/http.rs                                           |   57 
crates/client/src/telemetry.rs                                      |   22 
crates/client/src/test.rs                                           |   53 
crates/client/src/user.rs                                           |    8 
crates/collab/.env.toml                                             |    1 
crates/collab/Cargo.toml                                            |   11 
crates/collab/k8s/environments/preview.sh                           |    1 
crates/collab/k8s/environments/production.sh                        |    1 
crates/collab/k8s/environments/staging.sh                           |    1 
crates/collab/k8s/manifest.template.yml                             |    9 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql      |   14 
crates/collab/migrations/20230202155735_followers.sql               |   15 
crates/collab/src/api.rs                                            |   13 
crates/collab/src/auth.rs                                           |   73 
crates/collab/src/bin/seed.rs                                       |    6 
crates/collab/src/db.rs                                             |  461 
crates/collab/src/db/follower.rs                                    |   51 
crates/collab/src/db/room.rs                                        |    8 
crates/collab/src/db/tests.rs                                       |   80 
crates/collab/src/lib.rs                                            |    6 
crates/collab/src/main.rs                                           |   17 
crates/collab/src/rpc.rs                                            |   72 
crates/collab/src/tests.rs                                          |   29 
crates/collab/src/tests/integration_tests.rs                        |  406 
crates/collab/src/tests/randomized_integration_tests.rs             |    2 
crates/collab_ui/Cargo.toml                                         |    7 
crates/collab_ui/src/collab_titlebar_item.rs                        |  765 
crates/collab_ui/src/collab_ui.rs                                   |    7 
crates/collab_ui/src/collaborator_list_popover.rs                   |  165 
crates/collab_ui/src/contact_finder.rs                              |   20 
crates/collab_ui/src/contact_list.rs                                |   93 
crates/collab_ui/src/contacts_popover.rs                            |   84 
crates/collab_ui/src/face_pile.rs                                   |  101 
crates/collab_ui/src/incoming_call_notification.rs                  |    6 
crates/collab_ui/src/notifications.rs                               |   10 
crates/collab_ui/src/project_shared_notification.rs                 |    6 
crates/collab_ui/src/sharing_status_indicator.rs                    |    2 
crates/collections/src/collections.rs                               |    7 
crates/command_palette/Cargo.toml                                   |    2 
crates/command_palette/src/command_palette.rs                       |   19 
crates/context_menu/src/context_menu.rs                             |  169 
crates/copilot/Cargo.toml                                           |   38 
crates/copilot/src/copilot.rs                                       |  695 
crates/copilot/src/request.rs                                       |  171 
crates/copilot/src/sign_in.rs                                       |  344 
crates/copilot_button/Cargo.toml                                    |   22 
crates/copilot_button/src/copilot_button.rs                         |  360 
crates/db/Cargo.toml                                                |    3 
crates/db/src/db.rs                                                 |   18 
crates/diagnostics/Cargo.toml                                       |    4 
crates/diagnostics/src/diagnostics.rs                               |   39 
crates/diagnostics/src/items.rs                                     |   13 
crates/editor/Cargo.toml                                            |    9 
crates/editor/src/display_map.rs                                    |  329 
crates/editor/src/display_map/block_map.rs                          |   83 
crates/editor/src/display_map/fold_map.rs                           |   85 
crates/editor/src/display_map/suggestion_map.rs                     |  860 
crates/editor/src/display_map/tab_map.rs                            |  404 
crates/editor/src/display_map/wrap_map.rs                           |   92 
crates/editor/src/editor.rs                                         |  705 
crates/editor/src/editor_tests.rs                                   |  567 
crates/editor/src/element.rs                                        |  342 
crates/editor/src/highlight_matching_bracket.rs                     |   36 
crates/editor/src/hover_popover.rs                                  |   11 
crates/editor/src/items.rs                                          |   59 
crates/editor/src/mouse_context_menu.rs                             |    4 
crates/editor/src/movement.rs                                       |   35 
crates/editor/src/multi_buffer.rs                                   |   37 
crates/editor/src/test.rs                                           |    5 
crates/editor/src/test/editor_lsp_test_context.rs                   |   18 
crates/editor/src/test/editor_test_context.rs                       |    1 
crates/feedback/Cargo.toml                                          |    7 
crates/feedback/src/deploy_feedback_button.rs                       |   72 
crates/feedback/src/feedback.rs                                     |   14 
crates/feedback/src/feedback_editor.rs                              |   85 
crates/feedback/src/feedback_info_text.rs                           |   47 
crates/feedback/src/submit_feedback_button.rs                       |    2 
crates/file_finder/Cargo.toml                                       |    4 
crates/file_finder/src/file_finder.rs                               |   97 
crates/fs/Cargo.toml                                                |    1 
crates/fs/src/fs.rs                                                 |   36 
crates/fs/src/repository.rs                                         |   37 
crates/fuzzy/src/matcher.rs                                         |    1 
crates/fuzzy/src/paths.rs                                           |   46 
crates/go_to_line/Cargo.toml                                        |    2 
crates/gpui/Cargo.toml                                              |    8 
crates/gpui/examples/text.rs                                        |    5 
crates/gpui/src/app.rs                                              |  531 
crates/gpui/src/app/action.rs                                       |    8 
crates/gpui/src/app/menu.rs                                         |   43 
crates/gpui/src/app/test_app_context.rs                             |   21 
crates/gpui/src/assets.rs                                           |   21 
crates/gpui/src/elements.rs                                         |   12 
crates/gpui/src/elements/constrained_box.rs                         |    4 
crates/gpui/src/elements/flex.rs                                    |   38 
crates/gpui/src/elements/image.rs                                   |   56 
crates/gpui/src/elements/label.rs                                   |   18 
crates/gpui/src/elements/text.rs                                    |    8 
crates/gpui/src/font_cache.rs                                       |   35 
crates/gpui/src/fonts.rs                                            |   51 
crates/gpui/src/keymap_matcher.rs                                   |   86 
crates/gpui/src/keymap_matcher/keymap_context.rs                    |   29 
crates/gpui/src/platform.rs                                         |   13 
crates/gpui/src/platform/mac/fonts.rs                               |   25 
crates/gpui/src/platform/mac/fonts/open_type.rs                     |  395 
crates/gpui/src/platform/mac/geometry.rs                            |   53 
crates/gpui/src/platform/mac/platform.rs                            |  130 
crates/gpui/src/platform/mac/sprite_cache.rs                        |   19 
crates/gpui/src/platform/mac/window.rs                              |   40 
crates/gpui/src/platform/test.rs                                    |    7 
crates/gpui/src/presenter.rs                                        |   49 
crates/gpui/src/text_layout.rs                                      |    8 
crates/install_cli/Cargo.toml                                       |   18 
crates/install_cli/src/install_cli.rs                               |   55 
crates/journal/Cargo.toml                                           |    1 
crates/journal/src/journal.rs                                       |    4 
crates/language/Cargo.toml                                          |    7 
crates/language/src/buffer.rs                                       |  163 
crates/language/src/buffer_tests.rs                                 |  297 
crates/language/src/highlight_map.rs                                |    1 
crates/language/src/language.rs                                     |  553 
crates/language/src/syntax_map.rs                                   |   93 
crates/language_selector/Cargo.toml                                 |   21 
crates/language_selector/src/active_buffer_language.rs              |   93 
crates/language_selector/src/language_selector.rs                   |  230 
crates/live_kit_client/Cargo.toml                                   |   12 
crates/live_kit_client/examples/test_app.rs                         |    1 
crates/live_kit_client/src/test.rs                                  |   49 
crates/live_kit_server/Cargo.toml                                   |    3 
crates/lsp/Cargo.toml                                               |    7 
crates/lsp/src/lsp.rs                                               |  236 
crates/node_runtime/Cargo.toml                                      |   22 
crates/node_runtime/src/node_runtime.rs                             |  166 
crates/outline/Cargo.toml                                           |    2 
crates/outline/src/outline.rs                                       |    2 
crates/pando/Cargo.toml                                             |   21 
crates/pando/src/file_format.rs                                     |    0 
crates/pando/src/pando.rs                                           |   15 
crates/picker/Cargo.toml                                            |    2 
crates/picker/src/picker.rs                                         |   63 
crates/plugin/Cargo.toml                                            |    3 
crates/plugin_macros/Cargo.toml                                     |    3 
crates/plugin_runtime/Cargo.toml                                    |    5 
crates/project/Cargo.toml                                           |   11 
crates/project/src/lsp_glob_set.rs                                  |  121 
crates/project/src/project.rs                                       |  558 
crates/project/src/project_tests.rs                                 |  176 
crates/project/src/terminals.rs                                     |   68 
crates/project/src/worktree.rs                                      |  763 
crates/project_panel/Cargo.toml                                     |    4 
crates/project_panel/src/project_panel.rs                           |  257 
crates/project_symbols/Cargo.toml                                   |    2 
crates/project_symbols/src/project_symbols.rs                       |    2 
crates/recent_projects/Cargo.toml                                   |    3 
crates/recent_projects/src/highlighted_workspace_location.rs        |    1 
crates/recent_projects/src/recent_projects.rs                       |   15 
crates/rope/Cargo.toml                                              |    2 
crates/rope/src/rope.rs                                             |   18 
crates/rpc/Cargo.toml                                               |    3 
crates/rpc/proto/zed.proto                                          |   11 
crates/rpc/src/peer.rs                                              |    2 
crates/rpc/src/proto.rs                                             |    1 
crates/rpc/src/rpc.rs                                               |    2 
crates/search/Cargo.toml                                            |    7 
crates/search/src/buffer_search.rs                                  |   12 
crates/search/src/project_search.rs                                 |   40 
crates/settings/Cargo.toml                                          |    4 
crates/settings/src/keymap_file.rs                                  |    6 
crates/settings/src/settings.rs                                     |  859 
crates/settings/src/settings_file.rs                                |  232 
crates/settings/src/watched_json.rs                                 |   31 
crates/terminal/Cargo.toml                                          |    3 
crates/terminal/src/mappings/mouse.rs                               |    3 
crates/terminal/src/terminal.rs                                     |   33 
crates/terminal_view/Cargo.toml                                     |    3 
crates/terminal_view/src/terminal_button.rs                         |  196 
crates/terminal_view/src/terminal_element.rs                        |   17 
crates/terminal_view/src/terminal_view.rs                           |  170 
crates/text/Cargo.toml                                              |    2 
crates/text/src/text.rs                                             |    8 
crates/theme/Cargo.toml                                             |    5 
crates/theme/src/theme.rs                                           |  166 
crates/theme/src/theme_registry.rs                                  |   11 
crates/theme/src/ui.rs                                              |  290 
crates/theme_selector/Cargo.toml                                    |    3 
crates/theme_selector/src/theme_selector.rs                         |    9 
crates/theme_testbench/src/theme_testbench.rs                       |   49 
crates/util/Cargo.toml                                              |   13 
crates/util/src/fs.rs                                               |   28 
crates/util/src/github.rs                                           |   46 
crates/util/src/http.rs                                             |  117 
crates/util/src/paths.rs                                            |   50 
crates/util/src/test/marked_text.rs                                 |    6 
crates/util/src/util.rs                                             |   82 
crates/vim/Cargo.toml                                               |    5 
crates/vim/src/motion.rs                                            |   50 
crates/vim/src/normal.rs                                            |   21 
crates/vim/src/state.rs                                             |   20 
crates/vim/src/test/neovim_backed_test_context.rs                   |   10 
crates/vim/src/test/neovim_connection.rs                            |  252 
crates/vim/src/vim.rs                                               |    2 
crates/vim/test_data/neovim_backed_test_context_works.json          |    4 
crates/vim/test_data/test_a.json                                    |    7 
crates/vim/test_data/test_b.json                                    |    0 
crates/vim/test_data/test_backspace.json                            |   10 
crates/vim/test_data/test_capital_f_and_capital_t.json              |    0 
crates/vim/test_data/test_cc.json                                   |   25 
crates/vim/test_data/test_change_0.json                             |    9 
crates/vim/test_data/test_change_b.json                             |   25 
crates/vim/test_data/test_change_backspace.json                     |   17 
crates/vim/test_data/test_change_e.json                             |   25 
crates/vim/test_data/test_change_end_of_document.json               |   17 
crates/vim/test_data/test_change_end_of_line.json                   |    9 
crates/vim/test_data/test_change_gg.json                            |   21 
crates/vim/test_data/test_change_h.json                             |   17 
crates/vim/test_data/test_change_j.json                             |   17 
crates/vim/test_data/test_change_k.json                             |   17 
crates/vim/test_data/test_change_l.json                             |    9 
crates/vim/test_data/test_change_sentence_object.json               |    0 
crates/vim/test_data/test_change_surrounding_character_objects.json |    0 
crates/vim/test_data/test_change_w.json                             |   29 
crates/vim/test_data/test_change_word_object.json                   |    0 
crates/vim/test_data/test_dd.json                                   |   25 
crates/vim/test_data/test_delete_0.json                             |    9 
crates/vim/test_data/test_delete_b.json                             |   25 
crates/vim/test_data/test_delete_e.json                             |   21 
crates/vim/test_data/test_delete_end_of_document.json               |   17 
crates/vim/test_data/test_delete_end_of_line.json                   |    9 
crates/vim/test_data/test_delete_gg.json                            |   21 
crates/vim/test_data/test_delete_h.json                             |   17 
crates/vim/test_data/test_delete_j.json                             |   17 
crates/vim/test_data/test_delete_k.json                             |   17 
crates/vim/test_data/test_delete_l.json                             |   17 
crates/vim/test_data/test_delete_left.json                          |   16 
crates/vim/test_data/test_delete_sentence_object.json               |    0 
crates/vim/test_data/test_delete_surrounding_character_objects.json |    0 
crates/vim/test_data/test_delete_to_end_of_line.json                |    7 
crates/vim/test_data/test_delete_w.json                             |   21 
crates/vim/test_data/test_delete_word_object.json                   |    0 
crates/vim/test_data/test_e.json                                    |    0 
crates/vim/test_data/test_end_of_document.json                      |   16 
crates/vim/test_data/test_enter.json                                |   11 
crates/vim/test_data/test_enter_visual_mode.json                    |    0 
crates/vim/test_data/test_f_and_t.json                              |    0 
crates/vim/test_data/test_gg.json                                   |   22 
crates/vim/test_data/test_h.json                                    |   10 
crates/vim/test_data/test_h_through_unicode.json                    |   13 
crates/vim/test_data/test_insert_end_of_line.json                   |   10 
crates/vim/test_data/test_insert_first_non_whitespace.json          |   16 
crates/vim/test_data/test_insert_line_above.json                    |   19 
crates/vim/test_data/test_j.json                                    |   13 
crates/vim/test_data/test_jump_to_end.json                          |   15 
crates/vim/test_data/test_jump_to_first_non_whitespace.json         |   19 
crates/vim/test_data/test_jump_to_line_boundaries.json              |    0 
crates/vim/test_data/test_k.json                                    |   16 
crates/vim/test_data/test_l.json                                    |   16 
crates/vim/test_data/test_neovim.json                               |   17 
crates/vim/test_data/test_o.json                                    |   19 
crates/vim/test_data/test_p.json                                    |   14 
crates/vim/test_data/test_percent.json                              |    0 
crates/vim/test_data/test_repeated_cb.json                          |    0 
crates/vim/test_data/test_repeated_ce.json                          |    0 
crates/vim/test_data/test_repeated_cj.json                          |    0 
crates/vim/test_data/test_repeated_cl.json                          |    0 
crates/vim/test_data/test_repeated_word.json                        |    0 
crates/vim/test_data/test_visual_change.json                        |   42 
crates/vim/test_data/test_visual_delete.json                        |   45 
crates/vim/test_data/test_visual_line_change.json                   |   36 
crates/vim/test_data/test_visual_line_delete.json                   |   32 
crates/vim/test_data/test_visual_sentence_object.json               |    1 
crates/vim/test_data/test_visual_word_object.json                   |    0 
crates/vim/test_data/test_w.json                                    |    0 
crates/vim/test_data/test_x.json                                    |   13 
crates/welcome/Cargo.toml                                           |   27 
crates/welcome/src/base_keymap_picker.rs                            |  175 
crates/welcome/src/welcome.rs                                       |  235 
crates/workspace/Cargo.toml                                         |   11 
crates/workspace/src/dock.rs                                        |   67 
crates/workspace/src/dock/toggle_dock_button.rs                     |    2 
crates/workspace/src/item.rs                                        |  102 
crates/workspace/src/notifications.rs                               |   86 
crates/workspace/src/pane.rs                                        |  412 
crates/workspace/src/searchable.rs                                  |   10 
crates/workspace/src/shared_screen.rs                               |   64 
crates/workspace/src/sidebar.rs                                     |   14 
crates/workspace/src/status_bar.rs                                  |   16 
crates/workspace/src/toolbar.rs                                     |   41 
crates/workspace/src/workspace.rs                                   |  549 
crates/zed/BundleDocumentTypes.plist                                |    2 
crates/zed/Cargo.toml                                               |   18 
crates/zed/resources/app-icon-preview.png                           |    0 
crates/zed/resources/app-icon-preview@2x.png                        |    0 
crates/zed/resources/app-icon.png                                   |    0 
crates/zed/resources/app-icon@2x.png                                |    0 
crates/zed/src/languages.rs                                         |   53 
crates/zed/src/languages/c.rs                                       |   32 
crates/zed/src/languages/c/config.toml                              |   20 
crates/zed/src/languages/cpp/config.toml                            |   20 
crates/zed/src/languages/css/config.toml                            |   18 
crates/zed/src/languages/elixir.rs                                  |   39 
crates/zed/src/languages/elixir/config.toml                         |   18 
crates/zed/src/languages/go.rs                                      |   51 
crates/zed/src/languages/go/config.toml                             |   20 
crates/zed/src/languages/html.rs                                    |   82 
crates/zed/src/languages/html/config.toml                           |   20 
crates/zed/src/languages/installation.rs                            |  111 
crates/zed/src/languages/javascript/config.toml                     |   24 
crates/zed/src/languages/javascript/highlights.scm                  |   29 
crates/zed/src/languages/json.rs                                    |  184 
crates/zed/src/languages/json/config.toml                           |    8 
crates/zed/src/languages/language_plugin.rs                         |   28 
crates/zed/src/languages/lua.rs                                     |   38 
crates/zed/src/languages/lua/config.toml                            |   12 
crates/zed/src/languages/lua/highlights.scm                         |   16 
crates/zed/src/languages/python.rs                                  |   78 
crates/zed/src/languages/python/config.toml                         |   24 
crates/zed/src/languages/racket/highlights.scm                      |    4 
crates/zed/src/languages/ruby.rs                                    |   17 
crates/zed/src/languages/ruby/config.toml                           |   18 
crates/zed/src/languages/ruby/highlights.scm                        |    2 
crates/zed/src/languages/rust.rs                                    |   34 
crates/zed/src/languages/rust/config.toml                           |   23 
crates/zed/src/languages/scheme/config.toml                         |   14 
crates/zed/src/languages/toml/config.toml                           |   16 
crates/zed/src/languages/tsx/config.toml                            |   15 
crates/zed/src/languages/tsx/overrides.scm                          |    3 
crates/zed/src/languages/typescript.rs                              |  134 
crates/zed/src/languages/typescript/config.toml                     |   24 
crates/zed/src/languages/typescript/highlights.scm                  |   28 
crates/zed/src/languages/yaml.rs                                    |  101 
crates/zed/src/languages/yaml/config.toml                           |    8 
crates/zed/src/main.rs                                              |  145 
crates/zed/src/menus.rs                                             |  422 
crates/zed/src/zed.rs                                               |  452 
plugins/Cargo.lock                                                  |    3 
plugins/json_language/Cargo.toml                                    |    3 
plugins/json_language/src/lib.rs                                    |   10 
script/bundle                                                       |   65 
script/generate-licenses                                            |    2 
script/start-local-collaboration                                    |    2 
script/terms/terms-of-use.json                                      |    9 
script/terms/terms-of-use.rtf                                       | 1568 
styles/.prettierignore                                              |    2 
styles/package-lock.json                                            |  229 
styles/package.json                                                 |   14 
styles/src/buildLicenses.ts                                         |  115 
styles/src/buildThemes.ts                                           |   72 
styles/src/colorSchemes.ts                                          |   74 
styles/src/common.ts                                                |   95 
styles/src/styleTree/app.ts                                         |  136 
styles/src/styleTree/commandPalette.ts                              |   54 
styles/src/styleTree/components.ts                                  |  404 
styles/src/styleTree/contactFinder.ts                               |  126 
styles/src/styleTree/contactList.ts                                 |  356 
styles/src/styleTree/contactNotification.ts                         |   82 
styles/src/styleTree/contactsPopover.ts                             |   39 
styles/src/styleTree/contextMenu.ts                                 |   84 
styles/src/styleTree/copilot.ts                                     |  226 
styles/src/styleTree/editor.ts                                      |  513 
styles/src/styleTree/feedback.ts                                    |   73 
styles/src/styleTree/hoverPopover.ts                                |   82 
styles/src/styleTree/incomingCallNotification.ts                    |   94 
styles/src/styleTree/picker.ts                                      |  150 
styles/src/styleTree/projectDiagnostics.ts                          |   20 
styles/src/styleTree/projectPanel.ts                                |  134 
styles/src/styleTree/projectSharedNotification.ts                   |   94 
styles/src/styleTree/search.ts                                      |  180 
styles/src/styleTree/sharedScreen.ts                                |   12 
styles/src/styleTree/simpleMessageNotification.ts                   |   70 
styles/src/styleTree/statusBar.ts                                   |  221 
styles/src/styleTree/tabBar.ts                                      |  183 
styles/src/styleTree/terminal.ts                                    |   94 
styles/src/styleTree/tooltip.ts                                     |   40 
styles/src/styleTree/updateNotification.ts                          |   54 
styles/src/styleTree/welcome.ts                                     |  129 
styles/src/styleTree/workspace.ts                                   |  521 
styles/src/system/lib/convert.ts                                    |   11 
styles/src/system/lib/curve.ts                                      |   26 
styles/src/system/lib/generate.ts                                   |  159 
styles/src/system/ref/color.ts                                      |  445 
styles/src/system/ref/curves.ts                                     |   25 
styles/src/system/system.ts                                         |   32 
styles/src/system/types.ts                                          |   66 
styles/src/themes/andromeda.ts                                      |   74 
styles/src/themes/atelier-cave-dark.ts                              |   66 
styles/src/themes/atelier-cave-light.ts                             |   68 
styles/src/themes/atelier-cave.ts                                   |   63 
styles/src/themes/atelier-dune-dark.ts                              |   66 
styles/src/themes/atelier-dune-light.ts                             |   68 
styles/src/themes/atelier-estuary-dark.ts                           |   66 
styles/src/themes/atelier-estuary-light.ts                          |   68 
styles/src/themes/atelier-forest-dark.ts                            |   66 
styles/src/themes/atelier-forest-light.ts                           |   68 
styles/src/themes/atelier-heath-dark.ts                             |   66 
styles/src/themes/atelier-heath-light.ts                            |   68 
styles/src/themes/atelier-lakeside-dark.ts                          |   66 
styles/src/themes/atelier-lakeside-light.ts                         |   68 
styles/src/themes/atelier-plateau-dark.ts                           |   66 
styles/src/themes/atelier-plateau-light.ts                          |   68 
styles/src/themes/atelier-savanna-dark.ts                           |   66 
styles/src/themes/atelier-savanna-light.ts                          |   68 
styles/src/themes/atelier-seaside-dark.ts                           |   66 
styles/src/themes/atelier-seaside-light.ts                          |   68 
styles/src/themes/atelier-sulphurpool-dark.ts                       |   66 
styles/src/themes/atelier-sulphurpool-light.ts                      |   68 
styles/src/themes/atelier-sulphurpool.ts                            |   42 
styles/src/themes/common/atelier-common.ts                          |   66 
styles/src/themes/common/base16.ts                                  |  296 
styles/src/themes/common/colorScheme.ts                             |  127 
styles/src/themes/common/ramps.ts                                   |  359 
styles/src/themes/common/syntax.ts                                  |  304 
styles/src/themes/common/theme.ts                                   |  165 
styles/src/themes/gruvbox-common.ts                                 |  251 
styles/src/themes/gruvbox-dark-hard.ts                              |    6 
styles/src/themes/gruvbox-dark-soft.ts                              |    6 
styles/src/themes/gruvbox-dark.ts                                   |    6 
styles/src/themes/gruvbox-light-hard.ts                             |    6 
styles/src/themes/gruvbox-light-soft.ts                             |    6 
styles/src/themes/gruvbox-light.ts                                  |    6 
styles/src/themes/one-dark.ts                                       |  113 
styles/src/themes/one-light.ts                                      |  111 
styles/src/themes/rose-pine-dawn.ts                                 |   74 
styles/src/themes/rose-pine-moon.ts                                 |   74 
styles/src/themes/rose-pine.ts                                      |   70 
styles/src/themes/sandcastle.ts                                     |   69 
styles/src/themes/solarized.ts                                      |   75 
styles/src/themes/staff/abruzzo.ts                                  |   31 
styles/src/themes/staff/atelier-dune.ts                             |   34 
styles/src/themes/staff/atelier-heath.ts                            |   53 
styles/src/themes/staff/atelier-seaside.ts                          |   34 
styles/src/themes/staff/ayu-mirage.ts                               |   52 
styles/src/themes/staff/ayu.ts                                      |   90 
styles/src/themes/staff/brushtrees.ts                               |   73 
styles/src/themes/staff/dracula.ts                                  |   31 
styles/src/themes/staff/gruvbox-medium.ts                           |  138 
styles/src/themes/staff/monokai.ts                                  |   32 
styles/src/themes/staff/nord.ts                                     |   32 
styles/src/themes/staff/seti-ui.ts                                  |   32 
styles/src/themes/staff/tokyo-night-storm.ts                        |   32 
styles/src/themes/staff/tokyo-night.ts                              |   53 
styles/src/themes/staff/zed-pro.ts                                  |   36 
styles/src/themes/staff/zenburn.ts                                  |   32 
styles/src/themes/summercamp.ts                                     |   74 
styles/src/utils/color.ts                                           |    4 
styles/src/utils/snakeCase.ts                                       |   38 
styles/tsconfig.json                                                |   20 
483 files changed, 25,552 insertions(+), 9,925 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -17,6 +17,26 @@ env:
   RUST_BACKTRACE: 1
 
 jobs:
+  rustfmt:
+    name: Check formatting
+    runs-on:
+      - self-hosted
+      - test
+    steps:
+      - name: Install Rust
+        run: |
+          rustup set profile minimal
+          rustup update stable
+
+      - name: Checkout repo
+        uses: actions/checkout@v2
+        with:
+          clean: false
+          submodules: 'recursive'
+
+      - name: cargo fmt
+        run: cargo fmt --all -- --check
+
   tests:
     name: Run tests
     runs-on:

Cargo.lock 🔗

@@ -518,6 +518,7 @@ dependencies = [
  "menu",
  "project",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smol",
@@ -784,6 +785,7 @@ dependencies = [
  "gpui",
  "itertools",
  "language",
+ "outline",
  "project",
  "search",
  "settings",
@@ -794,7 +796,7 @@ dependencies = [
 [[package]]
 name = "bromberg_sl2"
 version = "0.6.0"
-source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
+source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
 dependencies = [
  "digest 0.9.0",
  "lazy_static",
@@ -1097,6 +1099,7 @@ dependencies = [
  "ipc-channel",
  "plist",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -1111,7 +1114,6 @@ dependencies = [
  "futures 0.3.25",
  "gpui",
  "image",
- "isahc",
  "lazy_static",
  "log",
  "parking_lot 0.11.2",
@@ -1119,6 +1121,7 @@ dependencies = [
  "rand 0.8.5",
  "rpc",
  "serde",
+ "serde_derive",
  "settings",
  "smol",
  "sum_tree",
@@ -1188,7 +1191,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.5.4"
+version = "0.8.2"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -1228,6 +1231,7 @@ dependencies = [
  "sea-orm",
  "sea-query",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "sha-1 0.9.8",
@@ -1257,7 +1261,9 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "context_menu",
  "editor",
+ "feedback",
  "futures 0.3.25",
  "fuzzy",
  "gpui",
@@ -1267,6 +1273,7 @@ dependencies = [
  "postage",
  "project",
  "serde",
+ "serde_derive",
  "settings",
  "theme",
  "util",
@@ -1325,6 +1332,48 @@ dependencies = [
  "theme",
 ]
 
+[[package]]
+name = "copilot"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-tar",
+ "client",
+ "collections",
+ "context_menu",
+ "futures 0.3.25",
+ "gpui",
+ "language",
+ "log",
+ "lsp",
+ "node_runtime",
+ "serde",
+ "serde_derive",
+ "settings",
+ "smol",
+ "theme",
+ "util",
+ "workspace",
+]
+
+[[package]]
+name = "copilot_button"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "context_menu",
+ "copilot",
+ "editor",
+ "futures 0.3.25",
+ "gpui",
+ "settings",
+ "smol",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "core-foundation"
 version = "0.9.3"
@@ -1737,6 +1786,7 @@ dependencies = [
  "log",
  "parking_lot 0.11.2",
  "serde",
+ "serde_derive",
  "smol",
  "sqlez",
  "sqlez_macros",
@@ -1924,6 +1974,7 @@ dependencies = [
  "clock",
  "collections",
  "context_menu",
+ "copilot",
  "ctor",
  "db",
  "drag_and_drop",
@@ -1945,6 +1996,7 @@ dependencies = [
  "rand 0.8.5",
  "rpc",
  "serde",
+ "serde_derive",
  "settings",
  "smallvec",
  "smol",
@@ -2098,6 +2150,7 @@ dependencies = [
  "project",
  "search",
  "serde",
+ "serde_derive",
  "settings",
  "sysinfo",
  "theme",
@@ -2294,6 +2347,7 @@ dependencies = [
  "regex",
  "rope",
  "serde",
+ "serde_derive",
  "serde_json",
  "smol",
  "tempfile",
@@ -2580,9 +2634,9 @@ dependencies = [
 
 [[package]]
 name = "glob"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "globset"
@@ -2662,8 +2716,10 @@ dependencies = [
  "postage",
  "rand 0.8.5",
  "resvg",
+ "schemars",
  "seahash",
  "serde",
+ "serde_derive",
  "serde_json",
  "simplelog",
  "smallvec",
@@ -3017,6 +3073,17 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
 
+[[package]]
+name = "install_cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "log",
+ "smol",
+ "util",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -3156,6 +3223,7 @@ dependencies = [
 name = "journal"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "chrono",
  "dirs 4.0.0",
  "editor",
@@ -3261,6 +3329,7 @@ dependencies = [
  "regex",
  "rpc",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "similar",
@@ -3284,6 +3353,22 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "language_selector"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "editor",
+ "fuzzy",
+ "gpui",
+ "language",
+ "picker",
+ "project",
+ "settings",
+ "theme",
+ "workspace",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -3440,6 +3525,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "serde",
+ "serde_derive",
  "serde_json",
  "sha2 0.10.6",
  "simplelog",
@@ -3460,6 +3546,7 @@ dependencies = [
  "prost-types 0.8.0",
  "reqwest",
  "serde",
+ "serde_derive",
  "sha2 0.10.6",
 ]
 
@@ -3500,6 +3587,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "serde",
+ "serde_derive",
  "serde_json",
  "smol",
  "unindent",
@@ -3865,6 +3953,23 @@ dependencies = [
  "memoffset 0.6.5",
 ]
 
+[[package]]
+name = "node_runtime"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-tar",
+ "futures 0.3.25",
+ "gpui",
+ "parking_lot 0.11.2",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smol",
+ "util",
+]
+
 [[package]]
 name = "nom"
 version = "7.1.1"
@@ -4419,6 +4524,7 @@ dependencies = [
  "bincode",
  "plugin_macros",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -4429,6 +4535,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
+ "serde_derive",
  "syn",
 ]
 
@@ -4440,6 +4547,7 @@ dependencies = [
  "bincode",
  "pollster",
  "serde",
+ "serde_derive",
  "serde_json",
  "smol",
  "wasi-common",
@@ -4577,12 +4685,15 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "ctor",
  "db",
+ "env_logger",
  "fs",
  "fsevent",
  "futures 0.3.25",
  "fuzzy",
  "git",
+ "glob",
  "gpui",
  "ignore",
  "language",
@@ -4591,11 +4702,13 @@ dependencies = [
  "lsp",
  "parking_lot 0.11.2",
  "postage",
+ "pretty_assertions",
  "pulldown-cmark",
  "rand 0.8.5",
  "regex",
  "rpc",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "sha2 0.10.6",
@@ -4961,6 +5074,7 @@ dependencies = [
  "settings",
  "smol",
  "text",
+ "util",
  "workspace",
 ]
 
@@ -5218,6 +5332,7 @@ dependencies = [
  "rand 0.8.5",
  "rsa",
  "serde",
+ "serde_derive",
  "smol",
  "smol-timeout",
  "tempdir",
@@ -5641,6 +5756,7 @@ dependencies = [
  "postage",
  "project",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smallvec",
@@ -5827,8 +5943,10 @@ dependencies = [
  "gpui",
  "json_comments",
  "postage",
+ "pretty_assertions",
  "schemars",
  "serde",
+ "serde_derive",
  "serde_json",
  "serde_path_to_error",
  "sqlez",
@@ -6449,6 +6567,7 @@ dependencies = [
  "procinfo",
  "rand 0.8.5",
  "serde",
+ "serde_derive",
  "settings",
  "shellexpand",
  "smallvec",
@@ -6480,6 +6599,7 @@ dependencies = [
  "project",
  "rand 0.8.5",
  "serde",
+ "serde_derive",
  "settings",
  "shellexpand",
  "smallvec",
@@ -6539,6 +6659,7 @@ dependencies = [
  "indexmap",
  "parking_lot 0.11.2",
  "serde",
+ "serde_derive",
  "serde_json",
  "serde_path_to_error",
  "toml",
@@ -7441,11 +7562,15 @@ dependencies = [
  "dirs 3.0.2",
  "futures 0.3.25",
  "git2",
+ "isahc",
  "lazy_static",
  "log",
  "rand 0.8.5",
+ "serde",
  "serde_json",
+ "smol",
  "tempdir",
+ "url",
 ]
 
 [[package]]
@@ -7532,6 +7657,7 @@ dependencies = [
  "project",
  "search",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "tokio",
@@ -8011,6 +8137,26 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
 
+[[package]]
+name = "welcome"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "db",
+ "editor",
+ "fuzzy",
+ "gpui",
+ "install_cli",
+ "log",
+ "picker",
+ "project",
+ "settings",
+ "theme",
+ "theme_selector",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "wepoll-ffi"
 version = "0.1.2"
@@ -8286,6 +8432,7 @@ dependencies = [
  "futures 0.3.25",
  "gpui",
  "indoc",
+ "install_cli",
  "language",
  "lazy_static",
  "log",
@@ -8294,9 +8441,11 @@ dependencies = [
  "postage",
  "project",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smallvec",
+ "terminal",
  "theme",
  "util",
  "uuid 1.2.2",
@@ -8356,7 +8505,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
 [[package]]
 name = "zed"
-version = "0.75.0"
+version = "0.81.0"
 dependencies = [
  "activity_indicator",
  "anyhow",
@@ -8377,7 +8526,10 @@ dependencies = [
  "collections",
  "command_palette",
  "context_menu",
+ "copilot",
+ "copilot_button",
  "ctor",
+ "db",
  "diagnostics",
  "easy-parallel",
  "editor",
@@ -8393,13 +8545,16 @@ dependencies = [
  "ignore",
  "image",
  "indexmap",
+ "install_cli",
  "isahc",
  "journal",
  "language",
+ "language_selector",
  "lazy_static",
  "libc",
  "log",
  "lsp",
+ "node_runtime",
  "num_cpus",
  "outline",
  "parking_lot 0.11.2",
@@ -8416,6 +8571,7 @@ dependencies = [
  "rust-embed",
  "search",
  "serde",
+ "serde_derive",
  "serde_json",
  "serde_path_to_error",
  "settings",
@@ -8457,6 +8613,7 @@ dependencies = [
  "util",
  "uuid 1.2.2",
  "vim",
+ "welcome",
  "workspace",
 ]
 

Cargo.toml 🔗

@@ -13,6 +13,8 @@ members = [
     "crates/collections",
     "crates/command_palette",
     "crates/context_menu",
+    "crates/copilot",
+    "crates/copilot_button",
     "crates/db",
     "crates/diagnostics",
     "crates/drag_and_drop",
@@ -26,13 +28,16 @@ members = [
     "crates/go_to_line",
     "crates/gpui",
     "crates/gpui_macros",
+    "crates/install_cli",
     "crates/journal",
     "crates/language",
+    "crates/language_selector",
     "crates/live_kit_client",
     "crates/live_kit_server",
     "crates/lsp",
     "crates/media",
     "crates/menu",
+    "crates/node_runtime",
     "crates/outline",
     "crates/picker",
     "crates/plugin",
@@ -58,6 +63,7 @@ members = [
     "crates/util",
     "crates/vim",
     "crates/workspace",
+    "crates/welcome",
     "crates/zed",
 ]
 default-members = ["crates/zed"]
@@ -65,8 +71,10 @@ resolver = "2"
 
 [workspace.dependencies]
 serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
+postage = { version = "0.4.1", features = ["futures-traits"] }
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

README.md 🔗

@@ -23,10 +23,18 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
     git clone https://github.com/zed-industries/zed.dev
     ```
 
+* Initialize submodules
+
+    ```
+    git submodule update --init --recursive
+    ```
+
 * Set up a local `zed` database and seed it with some initial users:
 
+    Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
+
     ```
-    script/bootstrap
+    GITHUB_TOKEN=<$token> script/bootstrap
     ```
 
 ### Testing against locally-running servers

assets/icons/copilot_16.svg 🔗

@@ -0,0 +1,12 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M4.2926 3.48996C3.79162 3.79616 3.44871 4.26316 3.44871 4.93872C3.44871 5.75753 3.65302 6.19648 3.88658 6.43349C4.11948 6.66983 4.47018 6.79529 4.95638 6.79529C5.64158 6.79529 6.23176 6.65786 6.64548 6.37099C7.03216 6.10286 7.32149 5.66636 7.35698 4.91278C7.38386 4.34213 7.36863 3.96084 7.21748 3.68905C7.09721 3.47279 6.81682 3.2089 5.96976 3.11109C5.4731 3.05374 4.81346 3.17162 4.2926 3.48996ZM3.72539 2.5525C4.46348 2.10138 5.36842 1.93724 6.09436 2.02107C7.1336 2.14107 7.8142 2.51324 8.17039 3.15373C8.49569 3.73867 8.47238 4.43479 8.44743 4.96466C8.39736 6.02772 7.95809 6.7938 7.26541 7.27411C6.59976 7.73566 5.75982 7.89249 4.95638 7.89249C4.2936 7.89249 3.61755 7.71967 3.11095 7.20558C2.605 6.69216 2.35705 5.92853 2.35705 4.93872C2.35705 3.80566 2.96744 3.01576 3.72539 2.5525Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69546 8.97734C7.02432 8.97734 7.29091 9.24528 7.29091 9.57581V10.8725C7.29091 11.203 7.02432 11.471 6.69546 11.471C6.3666 11.471 6.1 11.203 6.1 10.8725V9.57581C6.1 9.24528 6.3666 8.97734 6.69546 8.97734Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.45301 7.32072C2.56382 6.90477 2.81104 6.35118 3.40175 6.17048L3.74851 7.31556C3.74509 7.31822 3.73425 7.32798 3.71842 7.35038C3.68409 7.39897 3.64151 7.48723 3.6034 7.6303C3.52629 7.91973 3.49839 8.31081 3.4984 8.73318V10.8761C3.5122 10.9688 3.52011 11.0083 3.53501 11.0478C3.5474 11.0807 3.57295 11.1339 3.6523 11.2153C3.83266 11.4004 4.24428 11.6866 5.21016 12.1174C5.99398 12.467 6.35125 12.6243 6.68361 12.7078C6.99799 12.7869 7.30564 12.8031 7.99999 12.8031V14C7.31311 14 6.86876 13.9882 6.3946 13.869C5.95125 13.7575 5.49691 13.5549 4.78914 13.2391C4.76868 13.23 4.74801 13.2208 4.72712 13.2115C3.73729 12.77 3.14865 12.4092 2.80139 12.0527C2.61692 11.8634 2.49682 11.6721 2.42136 11.4719C2.35507 11.2961 2.33141 11.1302 2.31663 11.0266C2.31561 11.0194 2.31463 11.0126 2.31369 11.0061L2.30749 10.9632V8.73321C2.30748 8.28334 2.33457 7.76532 2.45301 7.32072Z" fill="white"/>

assets/icons/ellipsis_14.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
+</svg>

assets/icons/feedback_16.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9083 3.19699L7.99999 10.3949L0.0916311 3.1969C0.346537 2.49164 1.10447 1.98018 2 1.98018H14C14.8943 1.98018 15.653 2.49168 15.9083 3.19699ZM16 4.7153L12.1526 8.21715L16 11.688V4.7153ZM8.52024 11.5232L11.4199 8.88404L15.9081 12.933C15.6528 13.6378 14.8941 14.1501 14 14.1501H2C1.10461 14.1501 0.346779 13.6378 0.0917535 12.9331L4.58012 8.88404L7.47975 11.5232L7.99999 11.9967L8.52024 11.5232ZM3.84742 8.21715L0 4.71532V11.688L3.84742 8.21715Z" fill="white"/>
+</svg>

assets/icons/leave_12.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.5 1H7.5H8.75C8.88807 1 9 1.11193 9 1.25V4.5" stroke="#838994" stroke-linecap="round"/>
+<path d="M3.64645 5.64645C3.45118 5.84171 3.45118 6.15829 3.64645 6.35355C3.84171 6.54882 4.15829 6.54882 4.35355 6.35355L3.64645 5.64645ZM8.64645 0.646447L3.64645 5.64645L4.35355 6.35355L9.35355 1.35355L8.64645 0.646447Z" fill="#838994"/>
+<path d="M7.5 6.5V9C7.5 9.27614 7.27614 9.5 7 9.5H1C0.723858 9.5 0.5 9.27614 0.5 9V3C0.5 2.72386 0.723858 2.5 1 2.5H3.5" stroke="#838994" stroke-linecap="round"/>
+</svg>

assets/icons/logo_96.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z" fill="white"/>
+</svg>

assets/icons/speech_bubble_12.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
+</svg>

assets/icons/user_plus_12.svg 🔗

@@ -1,3 +1,5 @@
 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.2 6.00001C5.52563 6.00001 6.6 4.92545 6.6 3.60001C6.6 2.27457 5.52563 1.20001 4.2 1.20001C2.87438 1.20001 1.8 2.27457 1.8 3.60001C1.8 4.92545 2.87438 6.00001 4.2 6.00001ZM5.15063 6.90001H3.24938C1.45519 6.90001 0 8.35501 0 10.1494C0 10.5094 0.291 10.8 0.649875 10.8H7.7505C8.10938 10.8 8.4 10.5094 8.4 10.1494C8.4 8.35501 6.945 6.90001 5.15063 6.90001ZM11.55 4.95001H10.65V4.05001C10.65 3.80251 10.4494 3.60001 10.2 3.60001C9.95063 3.60001 9.75 3.80157 9.75 4.05001V4.95001H8.85C8.6025 4.95001 8.4 5.15251 8.4 5.40001C8.4 5.64751 8.60156 5.85001 8.85 5.85001H9.75V6.75001C9.75 6.99939 9.9525 7.20001 10.2 7.20001C10.4475 7.20001 10.65 6.99845 10.65 6.75001V5.85001H11.55C11.7994 5.85001 12 5.64939 12 5.40001C12 5.15064 11.7994 4.95001 11.55 4.95001Z" fill="white"/>
+<path d="M5.75062 7.09998H3.24938C1.45519 7.09998 0 8.55498 0 10.3493C0 10.7093 0.291 11 0.649875 11H8.3505C8.70938 11 9 10.7093 9 10.3493C9 8.55498 7.545 7.09998 5.75062 7.09998Z" fill="white"/>
+<path d="M7 3.5C7 4.82544 5.82562 6 4.5 6C3.17438 6 2 4.82544 2 3.5C2 2.17456 3.17438 1 4.5 1C5.82562 1 7 2.17456 7 3.5Z" fill="white"/>
+<path d="M9.5 3.75V5.5M9.5 7.25V5.5M9.5 5.5H11.25M9.5 5.5H7.75" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/user_plus_16.svg 🔗

@@ -1,3 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69772 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0422C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM14.475 6.77502H13.425V5.72502C13.425 5.43627 13.1909 5.20002 12.9 5.20002C12.6091 5.20002 12.375 5.43518 12.375 5.72502V6.77502H11.325C11.0363 6.77502 10.8 7.01127 10.8 7.30002C10.8 7.58877 11.0352 7.82502 11.325 7.82502H12.375V8.87502C12.375 9.16596 12.6112 9.40002 12.9 9.40002C13.1887 9.40002 13.425 9.16487 13.425 8.87502V7.82502H14.475C14.7659 7.82502 15 7.59096 15 7.30002C15 7.00909 14.7659 6.77502 14.475 6.77502Z" fill="white"/>
+<path d="M7.00906 8.99999H4.79094C2.69772 8.99999 1 11.1475 1 13.2409C1 13.6609 1.3395 14 1.75819 14H10.0422C10.4609 14 10.8 13.6609 10.8 13.2409C10.8 11.1475 9.1025 8.99999 7.00906 8.99999Z" fill="white"/>
+<path d="M9 5C9 6.54634 7.44657 7.99998 5.90001 7.99998C4.35344 7.99998 3 6.54634 3 5C3 3.45366 4.45344 2 6 2C7.54656 2 9 3.45366 9 5Z" fill="white"/>
+<path d="M13.025 6H14.475C14.7659 6 15 6.20906 15 6.5C15 6.79094 14.7659 7 14.475 7H13V8.49995C13 8.7898 12.7638 9.02495 12.475 9.02495C12.1863 9.02495 11.95 8.79089 11.95 8.49995V7H10.525C10.2352 7 10 6.78875 10 6.5C10 6.21125 10.2362 6 10.525 6H11.975V4.525C11.975 4.23516 12.2091 4 12.5 4C12.7909 4 13.025 4.23625 13.025 4.525V6Z" fill="white"/>
 </svg>

assets/icons/zed_plus_copilot_32.svg 🔗

@@ -0,0 +1,14 @@
+<svg width="93" height="32" viewBox="0 0 93 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.03996 7.04962C8.00936 7.67635 7.30396 8.63219 7.30396 10.0149C7.30396 11.6908 7.72425 12.5893 8.2047 13.0744C8.68381 13.5581 9.40526 13.8149 10.4054 13.8149C11.815 13.8149 13.0291 13.5336 13.8802 12.9464C14.6756 12.3977 15.2708 11.5042 15.3438 9.96182C15.3991 8.79382 15.3678 8.01341 15.0568 7.45711C14.8094 7.01449 14.2326 6.47436 12.4901 6.27416C11.4684 6.15678 10.1114 6.39804 9.03996 7.04962ZM7.87312 5.13084C9.39147 4.2075 11.2531 3.87155 12.7464 4.04312C14.8843 4.28874 16.2844 5.05049 17.0171 6.36142C17.6863 7.55867 17.6384 8.98348 17.587 10.068C17.484 12.2439 16.5804 13.8118 15.1554 14.7949C13.7861 15.7396 12.0582 16.0606 10.4054 16.0606C9.04201 16.0606 7.65128 15.7069 6.60913 14.6547C5.56832 13.6038 5.05825 12.0408 5.05825 10.0149C5.05825 7.6958 6.3139 6.07903 7.87312 5.13084Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.983 18.2811C14.6595 18.2811 15.2079 18.8295 15.2079 19.506V22.16C15.2079 22.8365 14.6595 23.385 13.983 23.385C13.3065 23.385 12.758 22.8365 12.758 22.16V19.506C12.758 18.8295 13.3065 18.2811 13.983 18.2811Z" fill="white"/>

assets/keymaps/atom.json 🔗

@@ -0,0 +1,68 @@
+[
+  {
+    "bindings": {
+      "cmd-k cmd-p": "workspace::ActivatePreviousPane",
+      "cmd-k cmd-n": "workspace::ActivateNextPane"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "cmd-b": "editor::GoToDefinition",
+      "cmd-<": "editor::ScrollCursorCenter",
+      "cmd-g": [
+        "editor::SelectNext",
+        {
+          "replace_newest": true
+        }
+      ],
+      "ctrl-shift-down": "editor::AddSelectionBelow",
+      "ctrl-shift-up": "editor::AddSelectionAbove",
+      "cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "cmd-r": "outline::Toggle"
+    }
+  },
+  {
+    "context": "BufferSearchBar",
+    "bindings": {
+      "cmd-f3": "search::SelectNextMatch",
+      "cmd-shift-f3": "search::SelectPrevMatch"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "cmd-\\": "workspace::ToggleLeftSidebar",
+      "cmd-k cmd-b": "workspace::ToggleLeftSidebar",
+      "cmd-t": "file_finder::Toggle",
+      "cmd-shift-r": "project_symbols::Toggle"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "alt-cmd-/": "search::ToggleRegex",
+      "ctrl-0": "project_panel::ToggleFocus"
+    }
+  },
+  {
+    "context": "ProjectPanel",
+    "bindings": {
+      "ctrl-[": "project_panel::CollapseSelectedEntry",
+      "ctrl-b": "project_panel::CollapseSelectedEntry",
+      "alt-b": "project_panel::CollapseSelectedEntry",
+      "ctrl-]": "project_panel::ExpandSelectedEntry",
+      "ctrl-f": "project_panel::ExpandSelectedEntry",
+      "ctrl-shift-c": "project_panel::CopyPath"
+    }
+  },
+  {
+    "context": "Dock",
+    "bindings": {}
+  }
+]

assets/keymaps/default.json 🔗

@@ -176,7 +176,10 @@
                 {
                     "focus": false
                 }
-            ]
+            ],
+            "alt-]": "copilot::NextSuggestion",
+            "alt-[": "copilot::PreviousSuggestion",
+            "alt-\\": "copilot::Toggle"
         }
     },
     {
@@ -228,6 +231,7 @@
                     "replace_newest": true
                 }
             ],
+            "cmd-k cmd-i": "editor::Hover",
             "cmd-/": [
                 "editor::ToggleComments",
                 {
@@ -248,7 +252,8 @@
             "alt-cmd-[": "editor::Fold",
             "alt-cmd-]": "editor::UnfoldLines",
             "ctrl-space": "editor::ShowCompletions",
-            "cmd-.": "editor::ToggleCodeActions"
+            "cmd-.": "editor::ToggleCodeActions",
+            "alt-cmd-r": "editor::RevealInFinder"
         }
     },
     {
@@ -352,7 +357,8 @@
             "cmd-shift-p": "command_palette::Toggle",
             "cmd-shift-m": "diagnostics::Deploy",
             "cmd-shift-e": "project_panel::ToggleFocus",
-            "cmd-alt-s": "workspace::SaveAll"
+            "cmd-alt-s": "workspace::SaveAll",
+            "cmd-k m": "language_selector::Toggle"
         }
     },
     // Bindings from Sublime Text
@@ -418,7 +424,7 @@
     {
         "bindings": {
             "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
-            "cmd-shift-c": "collab::ToggleCollaborationMenu",
+            "cmd-shift-c": "collab::ToggleContactsMenu",
             "cmd-alt-i": "zed::DebugElements"
         }
     },
@@ -456,7 +462,7 @@
         }
     },
     {
-        "context": "Dock",
+        "context": "Pane && docked",
         "bindings": {
             "shift-escape": "dock::HideDock",
             "cmd-escape": "dock::RemoveTabFromDock"
@@ -472,7 +478,8 @@
             "cmd-v": "project_panel::Paste",
             "cmd-alt-c": "project_panel::CopyPath",
             "f2": "project_panel::Rename",
-            "backspace": "project_panel::Delete"
+            "backspace": "project_panel::Delete",
+            "alt-cmd-r": "project_panel::RevealInFinder"
         }
     },
     {
@@ -536,4 +543,4 @@
             ]
         }
     }
-]
+]

assets/keymaps/jetbrains.json 🔗

@@ -0,0 +1,78 @@
+[
+  {
+    "bindings": {
+      "cmd-shift-[": "pane::ActivatePrevItem",
+      "cmd-shift-]": "pane::ActivateNextItem"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "ctrl->": "zed::IncreaseBufferFontSize",
+      "ctrl-<": "zed::DecreaseBufferFontSize",
+      "cmd-d": "editor::DuplicateLine",
+      "cmd-pagedown": "editor::MovePageDown",
+      "cmd-pageup": "editor::MovePageUp",
+      "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
+      "shift-enter": "editor::NewlineBelow",
+      "cmd--": "editor::Fold",
+      "cmd-=": "editor::UnfoldLines",
+      "alt-shift-g": "editor::SplitSelectionIntoLines",
+      "ctrl-g": [
+        "editor::SelectNext",
+        {
+          "replace_newest": false
+        }
+      ],
+      "cmd-/": [
+        "editor::ToggleComments",
+        {
+          "advance_downwards": true
+        }
+      ],
+      "shift-alt-up": "editor::MoveLineUp",
+      "shift-alt-down": "editor::MoveLineDown",
+      "cmd-[": "pane::GoBack",
+      "cmd-]": "pane::GoForward",
+      "alt-f7": "editor::FindAllReferences",
+      "cmd-alt-f7": "editor::FindAllReferences",
+      "cmd-b": "editor::GoToDefinition",
+      "cmd-alt-b": "editor::GoToDefinition",
+      "cmd-shift-b": "editor::GoToTypeDefinition",
+      "alt-enter": "editor::ToggleCodeActions",
+      "f2": "editor::GoToDiagnostic",
+      "cmd-f2": "editor::GoToPrevDiagnostic",
+      "ctrl-alt-shift-down": "editor::GoToHunk",
+      "ctrl-alt-shift-up": "editor::GoToPrevHunk",
+      "cmd-home": "editor::MoveToBeginning",
+      "cmd-end": "editor::MoveToEnd",
+      "cmd-shift-home": "editor::SelectToBeginning",
+      "cmd-shift-end": "editor::SelectToEnd"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "cmd-f12": "outline::Toggle",
+      "cmd-7": "outline::Toggle",
+      "cmd-shift-o": "file_finder::Toggle",
+      "cmd-l": "go_to_line::Toggle"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "cmd-shift-a": "command_palette::Toggle",
+      "cmd-alt-o": "project_symbols::Toggle",
+      "cmd-1": "workspace::ToggleLeftSidebar",
+      "cmd-6": "diagnostics::Deploy",
+      "alt-f12": "dock::FocusDock"
+    }
+  },
+  {
+    "context": "Dock",
+    "bindings": {
+      "alt-f12": "dock::HideDock"
+    }
+  }
+]

assets/keymaps/sublime_text.json 🔗

@@ -0,0 +1,60 @@
+[
+  {
+    "bindings": {
+      "cmd-shift-[": "pane::ActivatePrevItem",
+      "cmd-shift-]": "pane::ActivateNextItem",
+      "ctrl-pagedown": "pane::ActivatePrevItem",
+      "ctrl-pageup": "pane::ActivateNextItem",
+      "ctrl-shift-tab": "pane::ActivateNextItem",
+      "ctrl-tab": "pane::ActivatePrevItem",
+      "cmd-+": "zed::IncreaseBufferFontSize"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "ctrl-shift-up": "editor::AddSelectionAbove",
+      "ctrl-shift-down": "editor::AddSelectionBelow",
+      "cmd-shift-space": "editor::SelectAll",
+      "ctrl-shift-m": "editor::SelectLargerSyntaxNode",
+      "cmd-shift-a": "editor::SelectLargerSyntaxNode",
+      "shift-f12": "editor::FindAllReferences",
+      "alt-cmd-down": "editor::GoToDefinition",
+      "alt-shift-cmd-down": "editor::FindAllReferences",
+      "ctrl-.": "editor::GoToHunk",
+      "ctrl-,": "editor::GoToPrevHunk",
+      "ctrl-backspace": "editor::DeleteToPreviousWordStart",
+      "ctrl-delete": "editor::DeleteToNextWordEnd"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "cmd-r": "outline::Toggle"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "f4": "search::SelectNextMatch",
+      "shift-f4": "search::SelectPrevMatch"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "ctrl-`": "dock::FocusDock",
+      "cmd-k cmd-b": "workspace::ToggleLeftSidebar",
+      "cmd-t": "file_finder::Toggle",
+      "shift-cmd-r": "project_symbols::Toggle",
+      // Currently busted: https://github.com/zed-industries/feedback/issues/898
+      "ctrl-0": "project_panel::ToggleFocus"
+    }
+  },
+  {
+    "context": "Dock",
+    "bindings": {
+      "ctrl-`": "dock::HideDock"
+    }
+  }
+]

assets/keymaps/textmate.json 🔗

@@ -0,0 +1,90 @@
+[
+  {
+    "bindings": {
+      "cmd-shift-o": "projects::OpenRecent",
+      "cmd-alt-tab": "project_panel::ToggleFocus"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "cmd-l": "go_to_line::Toggle",
+      "ctrl-shift-d": "editor::DuplicateLine",
+      "cmd-b": "editor::GoToDefinition",
+      "cmd-j": "editor::ScrollCursorCenter",
+      "cmd-enter": "editor::NewlineBelow",
+      "cmd-shift-l": "editor::SelectLine",
+      "cmd-shift-t": "outline::Toggle",
+      "alt-backspace": "editor::DeleteToPreviousWordStart",
+      "alt-shift-backspace": "editor::DeleteToNextWordEnd",
+      "alt-delete": "editor::DeleteToNextWordEnd",
+      "alt-shift-delete": "editor::DeleteToNextWordEnd",
+      "ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
+      "ctrl-delete": "editor::DeleteToNextSubwordEnd",
+      "alt-left": [
+        "editor::MoveToPreviousWordStart",
+        {
+          "stop_at_soft_wraps": true
+        }
+      ],
+      "alt-right": [
+        "editor::MoveToNextWordEnd",
+        {
+          "stop_at_soft_wraps": true
+        }
+      ],
+      "ctrl-left": "editor::MoveToPreviousSubwordStart",
+      "ctrl-right": "editor::MoveToNextSubwordEnd",
+      "cmd-shift-left": "editor::SelectToBeginningOfLine",
+      "cmd-shift-right": "editor::SelectToEndOfLine",
+      "alt-shift-left": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
+        }
+      ],
+      "alt-shift-right": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
+        }
+      ],
+      "ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
+      "ctrl-shift-right": "editor::SelectToNextSubwordEnd"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {}
+  },
+  {
+    "context": "BufferSearchBar",
+    "bindings": {
+      "ctrl-s": "search::SelectNextMatch",
+      "ctrl-shift-s": "search::SelectPrevMatch"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
+      "cmd-t": "file_finder::Toggle",
+      "cmd-shift-t": "project_symbols::Toggle"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "alt-cmd-r": "search::ToggleRegex",
+      "ctrl-tab": "project_panel::ToggleFocus"
+    }
+  },
+  {
+    "context": "ProjectPanel",
+    "bindings": {}
+  },
+  {
+    "context": "Dock",
+    "bindings": {}
+  }
+]

assets/keymaps/vim.json 🔗

@@ -27,6 +27,7 @@
             "h": "vim::Left",
             "backspace": "vim::Backspace",
             "j": "vim::Down",
+            "enter": "vim::NextLineStart",
             "k": "vim::Up",
             "l": "vim::Right",
             "$": "vim::EndOfLine",
@@ -233,7 +234,8 @@
             "escape": [
                 "vim::SwitchMode",
                 "Normal"
-            ]
+            ],
+            "d": "editor::GoToDefinition"
         }
     },
     {

assets/settings/default.json 🔗

@@ -3,11 +3,21 @@
     "theme": "One Dark",
     // The name of a font to use for rendering text in the editor
     "buffer_font_family": "Zed Mono",
+    // The OpenType features to enable for text in the editor.
+    "buffer_font_features": {
+        // Disable ligatures:
+        // "calt": false
+    },
     // The default font size for text in the editor
     "buffer_font_size": 15,
     // The factor to grow the active pane by. Defaults to 1.0
     // which gives the same size as all other panes.
     "active_pane_magnification": 1.0,
+    // Enable / disable copilot integration.
+    "enable_copilot_integration": true,
+    // Controls whether copilot provides suggestion immediately
+    // or waits for a `copilot::Toggle`
+    "copilot": "on",
     // Whether to enable vim modes and key bindings
     "vim_mode": false,
     // Whether to show the informational hover box when moving the mouse
@@ -20,13 +30,8 @@
     // Whether to pop the completions menu while typing in an editor without
     // explicitly requesting it.
     "show_completions_on_input": true,
-    // Whether the screen sharing icon is showed in the os status bar.
+    // Whether the screen sharing icon is shown in the os status bar.
     "show_call_status_icon": true,
-    // Whether new projects should start out 'online'. Online projects
-    // appear in the contacts panel under your name, so that your contacts
-    // can see which projects you are working on. Regardless of this
-    // setting, projects keep their last online status when you reopen them.
-    "projects_online_by_default": true,
     // Whether to use language servers to provide code intelligence.
     "enable_language_server": true,
     // When to automatically save edited buffers. This setting can
@@ -50,7 +55,13 @@
     //     "default_dock_anchor": "right"
     // 3. Position the dock full screen over the entire workspace"
     //     "default_dock_anchor": "expanded"
-    "default_dock_anchor": "right",
+    "default_dock_anchor": "bottom",
+    // Whether or not to remove any trailing whitespace from lines of a buffer
+    // before saving it.
+    "remove_trailing_whitespace_on_save": true,
+    // Whether or not to ensure there's a single newline at the end of a buffer
+    // when saving it.
+    "ensure_final_newline_on_save": true,
     // Whether or not to perform a buffer format before saving
     "format_on_save": "on",
     // How to perform a buffer format. This setting can take two values:
@@ -83,7 +94,7 @@
     "hard_tabs": false,
     // How many columns a tab should occupy.
     "tab_size": 4,
-    // Control what info Zed sends to our servers
+    // Control what info is collected by Zed.
     "telemetry": {
         // Send debug info like crash reports.
         "diagnostics": true,
@@ -114,7 +125,7 @@
     // Settings specific to the terminal
     "terminal": {
         // What shell to use when opening a terminal. May take 3 values:
-        // 1. Use the system's default terminal configuration (e.g. $TERM).
+        // 1. Use the system's default terminal configuration in /etc/passwd
         //      "shell": "system"
         // 2. A program:
         //      "shell": {
@@ -194,13 +205,9 @@
     // Different settings for specific languages.
     "languages": {
         "Plain Text": {
-            "soft_wrap": "preferred_line_length"
-        },
-        "C": {
-            "tab_size": 2
-        },
-        "C++": {
-            "tab_size": 2
+            "soft_wrap": "preferred_line_length",
+            // Copilot can be a little strange on non-code files
+            "copilot": "off"
         },
         "Elixir": {
             "tab_size": 2
@@ -210,10 +217,9 @@
             "hard_tabs": true
         },
         "Markdown": {
-            "soft_wrap": "preferred_line_length"
-        },
-        "Rust": {
-            "tab_size": 4
+            "soft_wrap": "preferred_line_length",
+            // Copilot can be a little strange on non-code files
+            "copilot": "off"
         },
         "JavaScript": {
             "tab_size": 2
@@ -226,6 +232,9 @@
         },
         "YAML": {
             "tab_size": 2
+        },
+        "JSON": {
+            "copilot": "off"
         }
     },
     // LSP Specific settings.

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -33,6 +33,19 @@ struct LspStatus {
     status: LanguageServerBinaryStatus,
 }
 
+struct PendingWork<'a> {
+    language_server_name: &'a str,
+    progress_token: &'a str,
+    progress: &'a LanguageServerProgress,
+}
+
+#[derive(Default)]
+struct Content {
+    icon: Option<&'static str>,
+    message: String,
+    action: Option<Box<dyn Action>>,
+}
+
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ActivityIndicator::show_error_message);
     cx.add_action(ActivityIndicator::dismiss_error_message);
@@ -69,6 +82,8 @@ impl ActivityIndicator {
             if let Some(auto_updater) = auto_updater.as_ref() {
                 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
             }
+            cx.observe_active_labeled_tasks(|_, cx| cx.notify())
+                .detach();
 
             Self {
                 statuses: Default::default(),
@@ -130,7 +145,7 @@ impl ActivityIndicator {
     fn pending_language_server_work<'a>(
         &self,
         cx: &'a AppContext,
-    ) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
+    ) -> impl Iterator<Item = PendingWork<'a>> {
         self.project
             .read(cx)
             .language_server_statuses()
@@ -142,23 +157,29 @@ impl ActivityIndicator {
                     let mut pending_work = status
                         .pending_work
                         .iter()
-                        .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
+                        .map(|(token, progress)| PendingWork {
+                            language_server_name: status.name.as_str(),
+                            progress_token: token.as_str(),
+                            progress,
+                        })
                         .collect::<SmallVec<[_; 4]>>();
-                    pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
+                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
                     Some(pending_work)
                 }
             })
             .flatten()
     }
 
-    fn content_to_render(
-        &mut self,
-        cx: &mut RenderContext<Self>,
-    ) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
+    fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
         // Show any language server has pending activity.
         let mut pending_work = self.pending_language_server_work(cx);
-        if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
-            let mut message = lang_server_name.to_string();
+        if let Some(PendingWork {
+            language_server_name,
+            progress_token,
+            progress,
+        }) = pending_work.next()
+        {
+            let mut message = language_server_name.to_string();
 
             message.push_str(": ");
             if let Some(progress_message) = progress.message.as_ref() {
@@ -176,7 +197,11 @@ impl ActivityIndicator {
                 write!(&mut message, " + {} more", additional_work_count).unwrap();
             }
 
-            return (None, message, None);
+            return Content {
+                icon: None,
+                message,
+                action: None,
+            };
         }
 
         // Show any language server installation info.
@@ -199,19 +224,19 @@ impl ActivityIndicator {
         }
 
         if !downloading.is_empty() {
-            return (
-                Some(DOWNLOAD_ICON),
-                format!(
+            return Content {
+                icon: Some(DOWNLOAD_ICON),
+                message: format!(
                     "Downloading {} language server{}...",
                     downloading.join(", "),
                     if downloading.len() > 1 { "s" } else { "" }
                 ),
-                None,
-            );
+                action: None,
+            };
         } else if !checking_for_update.is_empty() {
-            return (
-                Some(DOWNLOAD_ICON),
-                format!(
+            return Content {
+                icon: Some(DOWNLOAD_ICON),
+                message: format!(
                     "Checking for updates to {} language server{}...",
                     checking_for_update.join(", "),
                     if checking_for_update.len() > 1 {
@@ -220,53 +245,61 @@ impl ActivityIndicator {
                         ""
                     }
                 ),
-                None,
-            );
+                action: None,
+            };
         } else if !failed.is_empty() {
-            return (
-                Some(WARNING_ICON),
-                format!(
+            return Content {
+                icon: Some(WARNING_ICON),
+                message: format!(
                     "Failed to download {} language server{}. Click to show error.",
                     failed.join(", "),
                     if failed.len() > 1 { "s" } else { "" }
                 ),
-                Some(Box::new(ShowErrorMessage)),
-            );
+                action: Some(Box::new(ShowErrorMessage)),
+            };
         }
 
         // Show any application auto-update info.
         if let Some(updater) = &self.auto_updater {
-            match &updater.read(cx).status() {
-                AutoUpdateStatus::Checking => (
-                    Some(DOWNLOAD_ICON),
-                    "Checking for Zed updates…".to_string(),
-                    None,
-                ),
-                AutoUpdateStatus::Downloading => (
-                    Some(DOWNLOAD_ICON),
-                    "Downloading Zed update…".to_string(),
-                    None,
-                ),
-                AutoUpdateStatus::Installing => (
-                    Some(DOWNLOAD_ICON),
-                    "Installing Zed update…".to_string(),
-                    None,
-                ),
-                AutoUpdateStatus::Updated => (
-                    None,
-                    "Click to restart and update Zed".to_string(),
-                    Some(Box::new(workspace::Restart)),
-                ),
-                AutoUpdateStatus::Errored => (
-                    Some(WARNING_ICON),
-                    "Auto update failed".to_string(),
-                    Some(Box::new(DismissErrorMessage)),
-                ),
+            return match &updater.read(cx).status() {
+                AutoUpdateStatus::Checking => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Checking for Zed updates…".to_string(),
+                    action: None,
+                },
+                AutoUpdateStatus::Downloading => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Downloading Zed update…".to_string(),
+                    action: None,
+                },
+                AutoUpdateStatus::Installing => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Installing Zed update…".to_string(),
+                    action: None,
+                },
+                AutoUpdateStatus::Updated => Content {
+                    icon: None,
+                    message: "Click to restart and update Zed".to_string(),
+                    action: Some(Box::new(workspace::Restart)),
+                },
+                AutoUpdateStatus::Errored => Content {
+                    icon: Some(WARNING_ICON),
+                    message: "Auto update failed".to_string(),
+                    action: Some(Box::new(DismissErrorMessage)),
+                },
                 AutoUpdateStatus::Idle => Default::default(),
-            }
-        } else {
-            Default::default()
+            };
         }
+
+        if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
+            return Content {
+                icon: None,
+                message: most_recent_active_task.to_string(),
+                action: None,
+            };
+        }
+
+        Default::default()
     }
 }
 
@@ -280,7 +313,11 @@ impl View for ActivityIndicator {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let (icon, message, action) = self.content_to_render(cx);
+        let Content {
+            icon,
+            message,
+            action,
+        } = self.content_to_render(cx);
 
         let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
             let theme = &cx

crates/auto_update/Cargo.toml 🔗

@@ -22,7 +22,8 @@ anyhow = "1.0.38"
 isahc = "1.7"
 lazy_static = "1.4"
 log = "0.4"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 smol = "1.2.5"
 tempdir = "0.3.7"

crates/auto_update/src/auto_update.rs 🔗

@@ -1,8 +1,7 @@
 mod update_notification;
 
 use anyhow::{anyhow, Context, Result};
-use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
-use client::{ZED_APP_PATH, ZED_APP_VERSION};
+use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -14,6 +13,7 @@ use smol::{fs::File, io::AsyncReadExt, process::Command};
 use std::{ffi::OsString, sync::Arc, time::Duration};
 use update_notification::UpdateNotification;
 use util::channel::ReleaseChannel;
+use util::http::HttpClient;
 use workspace::Workspace;
 
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";

crates/auto_update/src/update_notification.rs 🔗

@@ -78,7 +78,7 @@ impl View for UpdateNotification {
                 )
                 .with_child({
                     let style = theme.action_message.style_for(state, false);
-                    Text::new("View the release notes".to_string(), style.text.clone())
+                    Text::new("View the release notes", style.text.clone())
                         .contained()
                         .with_style(style.container)
                         .boxed()

crates/breadcrumbs/Cargo.toml 🔗

@@ -18,6 +18,7 @@ search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+outline = { path = "../outline" }
 itertools = "0.10"
 
 [dev-dependencies]

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::{
-    elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
+    elements::*, AppContext, Entity, MouseButton, RenderContext, Subscription, View, ViewContext,
+    ViewHandle,
 };
 use itertools::Itertools;
 use search::ProjectSearchView;
@@ -14,6 +15,7 @@ pub enum Event {
 }
 
 pub struct Breadcrumbs {
+    pane_focused: bool,
     active_item: Option<Box<dyn ItemHandle>>,
     project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -22,6 +24,7 @@ pub struct Breadcrumbs {
 impl Breadcrumbs {
     pub fn new() -> Self {
         Self {
+            pane_focused: false,
             active_item: Default::default(),
             subscription: Default::default(),
             project_search: Default::default(),
@@ -39,24 +42,53 @@ impl View for Breadcrumbs {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let active_item = match &self.active_item {
+            Some(active_item) => active_item,
+            None => return Empty::new().boxed(),
+        };
+        let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
         let theme = cx.global::<Settings>().theme.clone();
-        if let Some(breadcrumbs) = self
-            .active_item
-            .as_ref()
-            .and_then(|item| item.breadcrumbs(&theme, cx))
-        {
-            Flex::row()
-                .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
-                    Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed()
-                }))
-                .contained()
-                .with_style(theme.breadcrumbs.container)
+        let style = &theme.workspace.breadcrumbs;
+
+        let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
+            Some(breadcrumbs) => breadcrumbs,
+            None => return Empty::new().boxed(),
+        };
+
+        let crumbs = Flex::row()
+            .with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
+                Label::new(" 〉 ", style.default.text.clone()).boxed()
+            }))
+            .constrained()
+            .with_height(theme.workspace.breadcrumb_height)
+            .contained();
+
+        if not_editor || !self.pane_focused {
+            return crumbs
+                .with_style(style.default.container)
                 .aligned()
                 .left()
-                .boxed()
-        } else {
-            Empty::new().boxed()
+                .boxed();
         }
+
+        MouseEventHandler::<Breadcrumbs>::new(0, cx, |state, _| {
+            let style = style.style_for(state, false);
+            crumbs.with_style(style.container).boxed()
+        })
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(outline::Toggle);
+        })
+        .with_tooltip::<Breadcrumbs, _>(
+            0,
+            "Show symbol outline".to_owned(),
+            Some(Box::new(outline::Toggle)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .left()
+        .boxed()
     }
 }
 
@@ -103,4 +135,8 @@ impl ToolbarItemView for Breadcrumbs {
             current_location
         }
     }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut gpui::MutableAppContext) {
+        self.pane_focused = pane_focused;
+    }
 }

crates/call/Cargo.toml 🔗

@@ -34,7 +34,7 @@ util = { path = "../util" }
 anyhow = "1.0.38"
 async-broadcast = "0.4"
 futures = "0.3"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }

crates/call/src/call.rs 🔗

@@ -264,12 +264,13 @@ impl ActiveCall {
         Ok(())
     }
 
-    pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+    pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        cx.notify();
         if let Some((room, _)) = self.room.take() {
-            room.update(cx, |room, cx| room.leave(cx))?;
-            cx.notify();
+            room.update(cx, |room, cx| room.leave(cx))
+        } else {
+            Task::ready(Ok(()))
         }
-        Ok(())
     }
 
     pub fn share_project(
@@ -284,6 +285,18 @@ impl ActiveCall {
         }
     }
 
+    pub fn unshare_project(
+        &mut self,
+        project: ModelHandle<Project>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Some((room, _)) = self.room.as_ref() {
+            room.update(cx, |room, cx| room.unshare_project(project, cx))
+        } else {
+            Err(anyhow!("no active call"))
+        }
+    }
+
     pub fn set_location(
         &mut self,
         project: Option<&ModelHandle<Project>>,

crates/call/src/room.rs 🔗

@@ -17,10 +17,10 @@ use language::LanguageRegistry;
 use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
 use postage::stream::Stream;
 use project::Project;
-use std::{mem, sync::Arc, time::Duration};
+use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
 use util::{post_inc, ResultExt, TryFutureExt};
 
-pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
+pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
@@ -55,6 +55,7 @@ pub struct Room {
     leave_when_empty: bool,
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
+    follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
     subscriptions: Vec<client::Subscription>,
     pending_room_update: Option<Task<()>>,
     maintain_connection: Option<Task<Option<()>>>,
@@ -63,10 +64,27 @@ pub struct Room {
 impl Entity for Room {
     type Event = Event;
 
-    fn release(&mut self, _: &mut MutableAppContext) {
+    fn release(&mut self, cx: &mut MutableAppContext) {
         if self.status.is_online() {
-            log::info!("room was released, sending leave message");
-            let _ = self.client.send(proto::LeaveRoom {});
+            self.leave_internal(cx).detach_and_log_err(cx);
+        }
+    }
+
+    fn app_will_quit(
+        &mut self,
+        cx: &mut MutableAppContext,
+    ) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
+        if self.status.is_online() {
+            let leave = self.leave_internal(cx);
+            Some(
+                cx.background()
+                    .spawn(async move {
+                        leave.await.log_err();
+                    })
+                    .boxed(),
+            )
+        } else {
+            None
         }
     }
 }
@@ -148,6 +166,7 @@ impl Room {
             pending_room_update: None,
             client,
             user_store,
+            follows_by_leader_id_project_id: Default::default(),
             maintain_connection: Some(maintain_connection),
         }
     }
@@ -232,13 +251,17 @@ impl Room {
             && self.pending_call_count == 0
     }
 
-    pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
+    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 MutableAppContext) -> Task<Result<()>> {
         if self.status.is_offline() {
-            return Err(anyhow!("room is offline"));
+            return Task::ready(Err(anyhow!("room is offline")));
         }
 
-        cx.notify();
-        cx.emit(Event::Left);
         log::info!("leaving room");
 
         for project in self.shared_projects.drain() {
@@ -252,6 +275,7 @@ impl Room {
             if let Some(project) = project.upgrade(cx) {
                 project.update(cx, |project, cx| {
                     project.disconnected_from_host(cx);
+                    project.close(cx);
                 });
             }
         }
@@ -264,8 +288,12 @@ impl Room {
         self.live_kit.take();
         self.pending_room_update.take();
         self.maintain_connection.take();
-        self.client.send(proto::LeaveRoom {})?;
-        Ok(())
+
+        let leave_room = self.client.request(proto::LeaveRoom {});
+        cx.background().spawn(async move {
+            leave_room.await?;
+            anyhow::Ok(())
+        })
     }
 
     async fn maintain_connection(
@@ -275,14 +303,12 @@ impl Room {
     ) -> Result<()> {
         let mut client_status = client.status();
         loop {
-            let is_connected = client_status
-                .next()
-                .await
-                .map_or(false, |s| s.is_connected());
-
+            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(&cx)
                     .ok_or_else(|| anyhow!("room was dropped"))?
                     .update(&mut cx, |this, cx| {
@@ -296,12 +322,7 @@ impl Room {
                     let client_reconnection = async {
                         let mut remaining_attempts = 3;
                         while remaining_attempts > 0 {
-                            log::info!(
-                                "waiting for client status change, remaining attempts {}",
-                                remaining_attempts
-                            );
-                            let Some(status) = client_status.next().await else { break };
-                            if status.is_connected() {
+                            if client_status.borrow().is_connected() {
                                 log::info!("client reconnected, attempting to rejoin room");
 
                                 let Some(this) = this.upgrade(&cx) else { break };
@@ -315,7 +336,15 @@ impl Room {
                                 } else {
                                     remaining_attempts -= 1;
                                 }
+                            } 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
                     }
@@ -337,18 +366,20 @@ impl Room {
                     }
                 }
 
-                // 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(&cx) {
-                    log::info!("reconnection failed, leaving room");
-                    let _ = this.update(&mut cx, |this, cx| this.leave(cx));
-                }
-                return Err(anyhow!(
-                    "can't reconnect to room: client failed to re-establish connection"
-                ));
+                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(&cx) {
+            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<()>> {
@@ -457,6 +488,12 @@ impl Room {
         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())
+    }
+
     async fn handle_room_updated(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -487,11 +524,13 @@ impl Room {
             .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| {
                 (
@@ -499,6 +538,7 @@ impl Room {
                     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);
@@ -587,7 +627,7 @@ impl Room {
 
                             if let Some(live_kit) = this.live_kit.as_ref() {
                                 let tracks =
-                                    live_kit.room.remote_video_tracks(&peer_id.to_string());
+                                    live_kit.room.remote_video_tracks(&user.id.to_string());
                                 for track in tracks {
                                     this.remote_video_track_updated(
                                         RemoteVideoTrackUpdate::Subscribed(track),
@@ -620,6 +660,27 @@ impl Room {
                     }
                 }
 
+                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");
@@ -723,10 +784,10 @@ impl Room {
             this.update(&mut cx, |this, cx| {
                 this.pending_call_count -= 1;
                 if this.should_leave() {
-                    this.leave(cx)?;
+                    this.leave(cx).detach_and_log_err(cx);
                 }
-                result
-            })?;
+            });
+            result?;
             Ok(())
         })
     }
@@ -793,6 +854,20 @@ impl Room {
         })
     }
 
+    pub(crate) fn unshare_project(
+        &mut self,
+        project: ModelHandle<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<&ModelHandle<Project>>,

crates/cli/Cargo.toml 🔗

@@ -17,7 +17,8 @@ anyhow = "1.0"
 clap = { version = "3.1", features = ["derive"] }
 dirs = "3.0"
 ipc-channel = "0.16"
-serde = { version = "1.0", features = ["derive", "rc"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation = "0.9"

crates/client/Cargo.toml 🔗

@@ -23,11 +23,10 @@ async-recursion = "0.3"
 async-tungstenite = { version = "0.16", features = ["async-tls"] }
 futures = "0.3"
 image = "0.23"
-isahc = "1.7"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = "0.8.3"
 smol = "1.2.5"
 thiserror = "1.0.29"
@@ -35,7 +34,8 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
 tiny_http = "0.8"
 uuid = { version = "1.1.2", features = ["v4"] }
 url = "2.2"
-serde = { version = "*", features = ["derive"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 settings = { path = "../settings" }
 tempfile = "3"
 

crates/client/src/client.rs 🔗

@@ -1,7 +1,6 @@
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
-pub mod http;
 pub mod telemetry;
 pub mod user;
 
@@ -18,7 +17,6 @@ use gpui::{
     AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
     AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
 };
-use http::HttpClient;
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
 use postage::watch;
@@ -41,6 +39,7 @@ use telemetry::Telemetry;
 use thiserror::Error;
 use url::Url;
 use util::channel::ReleaseChannel;
+use util::http::HttpClient;
 use util::{ResultExt, TryFutureExt};
 
 pub use rpc::*;
@@ -66,12 +65,12 @@ 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);
 
-actions!(client, [Authenticate]);
+actions!(client, [SignIn, SignOut]);
 
 pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
     cx.add_global_action({
         let client = client.clone();
-        move |_: &Authenticate, cx| {
+        move |_: &SignIn, cx| {
             let client = client.clone();
             cx.spawn(
                 |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
@@ -79,6 +78,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
             .detach();
         }
     });
+    cx.add_global_action({
+        let client = client.clone();
+        move |_: &SignOut, cx| {
+            let client = client.clone();
+            cx.spawn(|cx| async move {
+                client.disconnect(&cx);
+            })
+            .detach();
+        }
+    });
 }
 
 pub struct Client {
@@ -120,7 +129,7 @@ pub enum EstablishConnectionError {
     #[error("{0}")]
     Other(#[from] anyhow::Error),
     #[error("{0}")]
-    Http(#[from] http::Error),
+    Http(#[from] util::http::Error),
     #[error("{0}")]
     Io(#[from] std::io::Error),
     #[error("{0}")]
@@ -169,6 +178,10 @@ 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 {
@@ -280,7 +293,7 @@ impl<T: Entity> PendingEntitySubscription<T> {
 
         state
             .entities_by_type_and_remote_id
-            .insert(id, WeakSubscriber::Model(model.downgrade().into()));
+            .insert(id, WeakSubscriber::Model(model.downgrade().into_any()));
         drop(state);
         for message in messages {
             self.client.handle_message(message, cx);
@@ -447,7 +460,7 @@ impl Client {
         self.state
             .write()
             .entities_by_type_and_remote_id
-            .insert(id, WeakSubscriber::View(cx.weak_handle().into()));
+            .insert(id, WeakSubscriber::View(cx.weak_handle().into_any()));
         Subscription::Entity {
             client: Arc::downgrade(self),
             id,
@@ -491,7 +504,7 @@ impl Client {
         let mut state = self.state.write();
         state
             .models_by_message_type
-            .insert(message_type_id, model.downgrade().into());
+            .insert(message_type_id, model.downgrade().into_any());
 
         let prev_handler = state.message_handlers.insert(
             message_type_id,
@@ -1152,11 +1165,9 @@ impl Client {
         })
     }
 
-    pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
-        let conn_id = self.connection_id()?;
-        self.peer.disconnect(conn_id);
+    pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+        self.peer.teardown();
         self.set_status(Status::SignedOut, cx);
-        Ok(())
     }
 
     fn connection_id(&self) -> Result<ConnectionId> {
@@ -1384,10 +1395,11 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::{FakeHttpClient, FakeServer};
+    use crate::test::FakeServer;
     use gpui::{executor::Deterministic, TestAppContext};
     use parking_lot::Mutex;
     use std::future;
+    use util::http::FakeHttpClient;
 
     #[gpui::test(iterations = 10)]
     async fn test_reconnection(cx: &mut TestAppContext) {

crates/client/src/http.rs 🔗

@@ -1,57 +0,0 @@
-pub use anyhow::{anyhow, Result};
-use futures::future::BoxFuture;
-use isahc::{
-    config::{Configurable, RedirectPolicy},
-    AsyncBody,
-};
-pub use isahc::{
-    http::{Method, Uri},
-    Error,
-};
-use smol::future::FutureExt;
-use std::{sync::Arc, time::Duration};
-pub use url::Url;
-
-pub type Request = isahc::Request<AsyncBody>;
-pub type Response = isahc::Response<AsyncBody>;
-
-pub trait HttpClient: Send + Sync {
-    fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>>;
-
-    fn get<'a>(
-        &'a self,
-        uri: &str,
-        body: AsyncBody,
-        follow_redirects: bool,
-    ) -> BoxFuture<'a, Result<Response, Error>> {
-        let request = isahc::Request::builder()
-            .redirect_policy(if follow_redirects {
-                RedirectPolicy::Follow
-            } else {
-                RedirectPolicy::None
-            })
-            .method(Method::GET)
-            .uri(uri)
-            .body(body);
-        match request {
-            Ok(request) => self.send(request),
-            Err(error) => async move { Err(error.into()) }.boxed(),
-        }
-    }
-}
-
-pub fn client() -> Arc<dyn HttpClient> {
-    Arc::new(
-        isahc::HttpClient::builder()
-            .connect_timeout(Duration::from_secs(5))
-            .low_speed_timeout(100, Duration::from_secs(5))
-            .build()
-            .unwrap(),
-    )
-}
-
-impl HttpClient for isahc::HttpClient {
-    fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>> {
-        Box::pin(async move { self.send_async(req).await })
-    }
-}

crates/client/src/telemetry.rs 🔗

@@ -1,11 +1,9 @@
-use crate::http::HttpClient;
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     executor::Background,
     serde_json::{self, value::Map, Value},
     AppContext, Task,
 };
-use isahc::Request;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
@@ -19,6 +17,7 @@ use std::{
     time::{Duration, SystemTime, UNIX_EPOCH},
 };
 use tempfile::NamedTempFile;
+use util::http::HttpClient;
 use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
 
@@ -220,11 +219,11 @@ impl Telemetry {
                                 "App": true
                             }),
                         }])?;
-                        let request = Request::post(MIXPANEL_ENGAGE_URL)
-                            .header("Content-Type", "application/json")
-                            .body(json_bytes.into())?;
-                        this.http_client.send(request).await?;
-                        Ok(())
+
+                        this.http_client
+                            .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
+                            .await?;
+                        anyhow::Ok(())
                     }
                     .log_err(),
                 )
@@ -316,11 +315,10 @@ impl Telemetry {
 
                         json_bytes.clear();
                         serde_json::to_writer(&mut json_bytes, &events)?;
-                        let request = Request::post(MIXPANEL_EVENTS_URL)
-                            .header("Content-Type", "application/json")
-                            .body(json_bytes.into())?;
-                        this.http_client.send(request).await?;
-                        Ok(())
+                        this.http_client
+                            .post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
+                            .await?;
+                        anyhow::Ok(())
                     }
                     .log_err(),
                 )

crates/client/src/test.rs 🔗

@@ -1,16 +1,14 @@
-use crate::{
-    http::{self, HttpClient, Request, Response},
-    Client, Connection, Credentials, EstablishConnectionError, UserStore,
-};
+use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
 use anyhow::{anyhow, Result};
-use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
+use futures::{stream::BoxStream, StreamExt};
 use gpui::{executor, ModelHandle, TestAppContext};
 use parking_lot::Mutex;
 use rpc::{
     proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
     ConnectionId, Peer, Receipt, TypedEnvelope,
 };
-use std::{fmt, rc::Rc, sync::Arc};
+use std::{rc::Rc, sync::Arc};
+use util::http::FakeHttpClient;
 
 pub struct FakeServer {
     peer: Arc<Peer>,
@@ -219,46 +217,3 @@ impl Drop for FakeServer {
         self.disconnect();
     }
 }
-
-pub struct FakeHttpClient {
-    handler: Box<
-        dyn 'static
-            + Send
-            + Sync
-            + Fn(Request) -> BoxFuture<'static, Result<Response, http::Error>>,
-    >,
-}
-
-impl FakeHttpClient {
-    pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
-    where
-        Fut: 'static + Send + Future<Output = Result<Response, http::Error>>,
-        F: 'static + Send + Sync + Fn(Request) -> Fut,
-    {
-        Arc::new(Self {
-            handler: Box::new(move |req| Box::pin(handler(req))),
-        })
-    }
-
-    pub fn with_404_response() -> Arc<dyn HttpClient> {
-        Self::create(|_| async move {
-            Ok(isahc::Response::builder()
-                .status(404)
-                .body(Default::default())
-                .unwrap())
-        })
-    }
-}
-
-impl fmt::Debug for FakeHttpClient {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("FakeHttpClient").finish()
-    }
-}
-
-impl HttpClient for FakeHttpClient {
-    fn send(&self, req: Request) -> BoxFuture<Result<Response, crate::http::Error>> {
-        let future = (self.handler)(req);
-        Box::pin(async move { future.await.map(Into::into) })
-    }
-}

crates/client/src/user.rs 🔗

@@ -1,4 +1,4 @@
-use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
+use super::{proto, Client, Status, TypedEnvelope};
 use anyhow::{anyhow, Context, Result};
 use collections::{hash_map::Entry, HashMap, HashSet};
 use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
@@ -7,6 +7,7 @@ use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use settings::Settings;
 use std::sync::{Arc, Weak};
+use util::http::HttpClient;
 use util::{StaffMode, TryFutureExt as _};
 
 #[derive(Default, Debug)]
@@ -183,6 +184,11 @@ impl UserStore {
         }
     }
 
+    #[cfg(feature = "test-support")]
+    pub fn clear_cache(&mut self) {
+        self.users.clear();
+    }
+
     async fn handle_update_invite_info(
         this: ModelHandle<Self>,
         message: TypedEnvelope<proto::UpdateInviteInfo>,

crates/collab/.env.toml 🔗

@@ -1,4 +1,5 @@
 DATABASE_URL = "postgres://postgres@localhost/zed"
+DATABASE_MAX_CONNECTIONS = 5
 HTTP_PORT = 8080
 API_TOKEN = "secret"
 INVITE_LINK_PREFIX = "http://localhost:3000/invites/"

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.5.4"
+version = "0.8.2"
 publish = false
 
 [[bin]]
@@ -31,6 +31,7 @@ futures = "0.3"
 hyper = "0.14"
 lazy_static = "1.4"
 lipsum = { version = "0.8", optional = true }
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 nanoid = "0.4"
 parking_lot = "0.11.1"
 prometheus = "0.13"
@@ -40,8 +41,9 @@ scrypt = "0.7"
 # Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
 sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
 sea-query = "0.27"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = "1.0"
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 sha-1 = "0.9"
 sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
 time = { version = "0.3", features = ["serde", "serde-well-known"] }
@@ -74,11 +76,10 @@ workspace = { path = "../workspace", features = ["test-support"] }
 
 ctor = "0.1"
 env_logger = "0.9"
-log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 util = { path = "../util" }
 lazy_static = "1.4"
 sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 sqlx = { version = "0.6", features = ["sqlite"] }
 unindent = "0.1"
 

crates/collab/k8s/manifest.template.yml 🔗

@@ -59,6 +59,13 @@ spec:
           ports:
             - containerPort: 8080
               protocol: TCP
+          livenessProbe:
+            httpGet:
+              path: /healthz
+              port: 8080
+            initialDelaySeconds: 5
+            periodSeconds: 5
+            timeoutSeconds: 5
           readinessProbe:
             httpGet:
               path: /
@@ -73,6 +80,8 @@ spec:
                 secretKeyRef:
                   name: database
                   key: url
+            - name: DATABASE_MAX_CONNECTIONS
+              value: "${DATABASE_MAX_CONNECTIONS}"
             - name: API_TOKEN
               valueFrom:
                 secretKeyRef:

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

@@ -143,3 +143,17 @@ CREATE TABLE "servers" (
     "id" INTEGER PRIMARY KEY AUTOINCREMENT,
     "environment" VARCHAR NOT NULL
 );
+
+CREATE TABLE "followers" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
+    "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+    "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "leader_connection_id" INTEGER NOT NULL,
+    "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "follower_connection_id" INTEGER NOT NULL
+);
+CREATE UNIQUE INDEX 
+    "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
+ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
+CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

crates/collab/migrations/20230202155735_followers.sql 🔗

@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS "followers" (
+    "id" SERIAL PRIMARY KEY,
+    "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
+    "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
+    "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "leader_connection_id" INTEGER NOT NULL,
+    "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
+    "follower_connection_id" INTEGER NOT NULL
+);
+
+CREATE UNIQUE INDEX 
+    "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
+ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
+
+CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

crates/collab/src/api.rs 🔗

@@ -78,6 +78,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
 struct AuthenticatedUserParams {
     github_user_id: Option<i32>,
     github_login: String,
+    github_email: Option<String>,
 }
 
 #[derive(Debug, Serialize)]
@@ -92,7 +93,11 @@ async fn get_authenticated_user(
 ) -> Result<Json<AuthenticatedUserResponse>> {
     let user = app
         .db
-        .get_user_by_github_account(&params.github_login, params.github_user_id)
+        .get_or_create_user_by_github_account(
+            &params.github_login,
+            params.github_user_id,
+            params.github_email.as_deref(),
+        )
         .await?
         .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
     let metrics_id = app.db.get_user_metrics_id(user.id).await?;
@@ -297,11 +302,7 @@ async fn create_access_token(
     let mut user_id = user.id;
     if let Some(impersonate) = params.impersonate {
         if user.admin {
-            if let Some(impersonated_user) = app
-                .db
-                .get_user_by_github_account(&impersonate, None)
-                .await?
-            {
+            if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
                 user_id = impersonated_user.id;
             } else {
                 return Err(Error::Http(

crates/collab/src/auth.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    db::{self, UserId},
+    db::{self, AccessTokenId, Database, UserId},
     AppState, Error, Result,
 };
 use anyhow::{anyhow, Context};
@@ -8,12 +8,24 @@ use axum::{
     middleware::Next,
     response::IntoResponse,
 };
+use lazy_static::lazy_static;
+use prometheus::{exponential_buckets, register_histogram, Histogram};
 use rand::thread_rng;
 use scrypt::{
     password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
     Scrypt,
 };
-use std::sync::Arc;
+use serde::{Deserialize, Serialize};
+use std::{sync::Arc, time::Instant};
+
+lazy_static! {
+    static ref METRIC_ACCESS_TOKEN_HASHING_TIME: Histogram = register_histogram!(
+        "access_token_hashing_time",
+        "time spent hashing access tokens",
+        exponential_buckets(10.0, 2.0, 10).unwrap(),
+    )
+    .unwrap();
+}
 
 pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
     let mut auth_header = req
@@ -42,20 +54,14 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
         )
     })?;
 
-    let mut credentials_valid = false;
     let state = req.extensions().get::<Arc<AppState>>().unwrap();
-    if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
-        if state.config.api_token == admin_token {
-            credentials_valid = true;
-        }
+    let credentials_valid = if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
+        state.config.api_token == admin_token
     } else {
-        for password_hash in state.db.get_access_token_hashes(user_id).await? {
-            if verify_access_token(access_token, &password_hash)? {
-                credentials_valid = true;
-                break;
-            }
-        }
-    }
+        verify_access_token(&access_token, user_id, &state.db)
+            .await
+            .unwrap_or(false)
+    };
 
     if credentials_valid {
         let user = state
@@ -75,13 +81,26 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
 
 const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
 
+#[derive(Serialize, Deserialize)]
+struct AccessTokenJson {
+    version: usize,
+    id: AccessTokenId,
+    token: String,
+}
+
 pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<String> {
+    const VERSION: usize = 1;
     let access_token = rpc::auth::random_token();
     let access_token_hash =
         hash_access_token(&access_token).context("failed to hash access token")?;
-    db.create_access_token_hash(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
+    let id = db
+        .create_access_token(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
         .await?;
-    Ok(access_token)
+    Ok(serde_json::to_string(&AccessTokenJson {
+        version: VERSION,
+        id,
+        token: access_token,
+    })?)
 }
 
 fn hash_access_token(token: &str) -> Result<String> {
@@ -89,7 +108,7 @@ fn hash_access_token(token: &str) -> Result<String> {
     let params = if cfg!(debug_assertions) {
         scrypt::Params::new(1, 1, 1).unwrap()
     } else {
-        scrypt::Params::recommended()
+        scrypt::Params::new(14, 8, 1).unwrap()
     };
 
     Ok(Scrypt
@@ -112,7 +131,21 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<St
     Ok(encrypted_access_token)
 }
 
-pub fn verify_access_token(token: &str, hash: &str) -> Result<bool> {
-    let hash = PasswordHash::new(hash).map_err(anyhow::Error::new)?;
-    Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok())
+pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc<Database>) -> Result<bool> {
+    let token: AccessTokenJson = serde_json::from_str(&token)?;
+
+    let db_token = db.get_access_token(token.id).await?;
+    if db_token.user_id != user_id {
+        return Err(anyhow!("no such access token"))?;
+    }
+
+    let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?;
+    let t0 = Instant::now();
+    let is_valid = Scrypt
+        .verify_password(token.token.as_bytes(), &db_hash)
+        .is_ok();
+    let duration = t0.elapsed();
+    log::info!("hashed access token in {:?}", duration);
+    METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64);
+    Ok(is_valid)
 }

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

@@ -1,4 +1,4 @@
-use collab::db;
+use collab::{db, executor::Executor};
 use db::{ConnectOptions, Database};
 use serde::{de::DeserializeOwned, Deserialize};
 use std::fmt::Write;
@@ -13,7 +13,7 @@ struct GitHubUser {
 #[tokio::main]
 async fn main() {
     let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
-    let db = Database::new(ConnectOptions::new(database_url))
+    let db = Database::new(ConnectOptions::new(database_url), Executor::Production)
         .await
         .expect("failed to connect to postgres database");
     let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
@@ -59,7 +59,7 @@ async fn main() {
 
     for (github_user, admin) in zed_users {
         if db
-            .get_user_by_github_account(&github_user.login, Some(github_user.id))
+            .get_user_by_github_login(&github_user.login)
             .await
             .expect("failed to fetch user")
             .is_none()

crates/collab/src/db.rs 🔗

@@ -1,5 +1,6 @@
 mod access_token;
 mod contact;
+mod follower;
 mod language_server;
 mod project;
 mod project_collaborator;
@@ -14,6 +15,7 @@ mod worktree;
 mod worktree_diagnostic_summary;
 mod worktree_entry;
 
+use crate::executor::Executor;
 use crate::{Error, Result};
 use anyhow::anyhow;
 use collections::{BTreeMap, HashMap, HashSet};
@@ -21,6 +23,8 @@ pub use contact::Contact;
 use dashmap::DashMap;
 use futures::StreamExt;
 use hyper::StatusCode;
+use rand::prelude::StdRng;
+use rand::{Rng, SeedableRng};
 use rpc::{proto, ConnectionId};
 use sea_orm::Condition;
 pub use sea_orm::ConnectOptions;
@@ -45,20 +49,20 @@ pub struct Database {
     options: ConnectOptions,
     pool: DatabaseConnection,
     rooms: DashMap<RoomId, Arc<Mutex<()>>>,
-    #[cfg(test)]
-    background: Option<std::sync::Arc<gpui::executor::Background>>,
+    rng: Mutex<StdRng>,
+    executor: Executor,
     #[cfg(test)]
     runtime: Option<tokio::runtime::Runtime>,
 }
 
 impl Database {
-    pub async fn new(options: ConnectOptions) -> Result<Self> {
+    pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
         Ok(Self {
             options: options.clone(),
             pool: sea_orm::Database::connect(options).await?,
             rooms: DashMap::with_capacity(16384),
-            #[cfg(test)]
-            background: None,
+            rng: Mutex::new(StdRng::seed_from_u64(0)),
+            executor,
             #[cfg(test)]
             runtime: None,
         })
@@ -157,7 +161,7 @@ impl Database {
         room_id: RoomId,
         new_server_id: ServerId,
     ) -> Result<RoomGuard<RefreshedRoom>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let stale_participant_filter = Condition::all()
                 .add(room_participant::Column::RoomId.eq(room_id))
                 .add(room_participant::Column::AnsweringConnectionId.is_not_null())
@@ -190,17 +194,18 @@ impl Database {
                     .filter(room_participant::Column::RoomId.eq(room_id))
                     .exec(&*tx)
                     .await?;
+                project::Entity::delete_many()
+                    .filter(project::Column::RoomId.eq(room_id))
+                    .exec(&*tx)
+                    .await?;
                 room::Entity::delete_by_id(room_id).exec(&*tx).await?;
             }
 
-            Ok((
-                room_id,
-                RefreshedRoom {
-                    room,
-                    stale_participant_user_ids,
-                    canceled_calls_to_user_ids,
-                },
-            ))
+            Ok(RefreshedRoom {
+                room,
+                stale_participant_user_ids,
+                canceled_calls_to_user_ids,
+            })
         })
         .await
     }
@@ -293,10 +298,21 @@ impl Database {
         .await
     }
 
-    pub async fn get_user_by_github_account(
+    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .filter(user::Column::GithubLogin.eq(github_login))
+                .one(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_or_create_user_by_github_account(
         &self,
         github_login: &str,
         github_user_id: Option<i32>,
+        github_email: Option<&str>,
     ) -> Result<Option<User>> {
         self.transaction(|tx| async move {
             let tx = &*tx;
@@ -318,7 +334,19 @@ impl Database {
                     user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
                     Ok(Some(user_by_github_login.update(tx).await?))
                 } else {
-                    Ok(None)
+                    let user = user::Entity::insert(user::ActiveModel {
+                        email_address: ActiveValue::set(github_email.map(|email| email.into())),
+                        github_login: ActiveValue::set(github_login.into()),
+                        github_user_id: ActiveValue::set(Some(github_user_id)),
+                        admin: ActiveValue::set(false),
+                        invite_count: ActiveValue::set(0),
+                        invite_code: ActiveValue::set(None),
+                        metrics_id: ActiveValue::set(Uuid::new_v4()),
+                        ..Default::default()
+                    })
+                    .exec_with_returning(&*tx)
+                    .await?;
+                    Ok(Some(user))
                 }
             } else {
                 Ok(user::Entity::find()
@@ -1129,18 +1157,16 @@ impl Database {
         user_id: UserId,
         connection: ConnectionId,
         live_kit_room: &str,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+    ) -> Result<proto::Room> {
+        self.transaction(|tx| async move {
             let room = room::ActiveModel {
                 live_kit_room: ActiveValue::set(live_kit_room.into()),
                 ..Default::default()
             }
             .insert(&*tx)
             .await?;
-            let room_id = room.id;
-
             room_participant::ActiveModel {
-                room_id: ActiveValue::set(room_id),
+                room_id: ActiveValue::set(room.id),
                 user_id: ActiveValue::set(user_id),
                 answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
                 answering_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -1157,8 +1183,8 @@ impl Database {
             .insert(&*tx)
             .await?;
 
-            let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, room))
+            let room = self.get_room(room.id, &tx).await?;
+            Ok(room)
         })
         .await
     }
@@ -1171,7 +1197,7 @@ impl Database {
         called_user_id: UserId,
         initial_project_id: Option<ProjectId>,
     ) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             room_participant::ActiveModel {
                 room_id: ActiveValue::set(room_id),
                 user_id: ActiveValue::set(called_user_id),
@@ -1190,7 +1216,7 @@ impl Database {
             let room = self.get_room(room_id, &tx).await?;
             let incoming_call = Self::build_incoming_call(&room, called_user_id)
                 .ok_or_else(|| anyhow!("failed to build incoming call"))?;
-            Ok((room_id, (room, incoming_call)))
+            Ok((room, incoming_call))
         })
         .await
     }
@@ -1200,7 +1226,7 @@ impl Database {
         room_id: RoomId,
         called_user_id: UserId,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             room_participant::Entity::delete_many()
                 .filter(
                     room_participant::Column::RoomId
@@ -1210,7 +1236,7 @@ impl Database {
                 .exec(&*tx)
                 .await?;
             let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, room))
+            Ok(room)
         })
         .await
     }
@@ -1257,7 +1283,7 @@ impl Database {
         calling_connection: ConnectionId,
         called_user_id: UserId,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
                 .filter(
                     Condition::all()
@@ -1276,14 +1302,13 @@ impl Database {
                 .one(&*tx)
                 .await?
                 .ok_or_else(|| anyhow!("no call to cancel"))?;
-            let room_id = participant.room_id;
 
             room_participant::Entity::delete(participant.into_active_model())
                 .exec(&*tx)
                 .await?;
 
             let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, room))
+            Ok(room)
         })
         .await
     }
@@ -1294,7 +1319,7 @@ impl Database {
         user_id: UserId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let result = room_participant::Entity::update_many()
                 .filter(
                     Condition::all()
@@ -1316,7 +1341,7 @@ impl Database {
                 Err(anyhow!("room does not exist or was already joined"))?
             } else {
                 let room = self.get_room(room_id, &tx).await?;
-                Ok((room_id, room))
+                Ok(room)
             }
         })
         .await
@@ -1328,9 +1353,9 @@ impl Database {
         user_id: UserId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<RejoinedRoom>> {
-        self.room_transaction(|tx| async {
+        let room_id = RoomId::from_proto(rejoin_room.id);
+        self.room_transaction(room_id, |tx| async {
             let tx = tx;
-            let room_id = RoomId::from_proto(rejoin_room.id);
             let participant_update = room_participant::Entity::update_many()
                 .filter(
                     Condition::all()
@@ -1549,14 +1574,11 @@ impl Database {
             }
 
             let room = self.get_room(room_id, &tx).await?;
-            Ok((
-                room_id,
-                RejoinedRoom {
-                    room,
-                    rejoined_projects,
-                    reshared_projects,
-                },
-            ))
+            Ok(RejoinedRoom {
+                room,
+                rejoined_projects,
+                reshared_projects,
+            })
         })
         .await
     }
@@ -1717,13 +1739,75 @@ impl Database {
         .await
     }
 
+    pub async fn follow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::ActiveModel {
+                room_id: ActiveValue::set(room_id),
+                project_id: ActiveValue::set(project_id),
+                leader_connection_server_id: ActiveValue::set(ServerId(
+                    leader_connection.owner_id as i32,
+                )),
+                leader_connection_id: ActiveValue::set(leader_connection.id as i32),
+                follower_connection_server_id: ActiveValue::set(ServerId(
+                    follower_connection.owner_id as i32,
+                )),
+                follower_connection_id: ActiveValue::set(follower_connection.id as i32),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn unfollow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(follower::Column::ProjectId.eq(project_id))
+                        .add(
+                            follower::Column::LeaderConnectionServerId
+                                .eq(leader_connection.owner_id),
+                        )
+                        .add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
+                        .add(
+                            follower::Column::FollowerConnectionServerId
+                                .eq(follower_connection.owner_id),
+                        )
+                        .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
     pub async fn update_room_participant_location(
         &self,
         room_id: RoomId,
         connection: ConnectionId,
         location: proto::ParticipantLocation,
     ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(|tx| async {
+        self.room_transaction(room_id, |tx| async {
             let tx = tx;
             let location_kind;
             let location_project_id;
@@ -1769,7 +1853,7 @@ impl Database {
 
             if result.rows_affected == 1 {
                 let room = self.get_room(room_id, &tx).await?;
-                Ok((room_id, room))
+                Ok(room)
             } else {
                 Err(anyhow!("could not update room participant location"))?
             }
@@ -1926,12 +2010,25 @@ impl Database {
                 }
             }
         }
+        drop(db_projects);
+
+        let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
+        let mut followers = Vec::new();
+        while let Some(db_follower) = db_followers.next().await {
+            let db_follower = db_follower?;
+            followers.push(proto::Follower {
+                leader_id: Some(db_follower.leader_connection().into()),
+                follower_id: Some(db_follower.follower_connection().into()),
+                project_id: db_follower.project_id.to_proto(),
+            });
+        }
 
         Ok(proto::Room {
             id: db_room.id.to_proto(),
             live_kit_room: db_room.live_kit_room,
             participants: participants.into_values().collect(),
             pending_participants,
+            followers,
         })
     }
 
@@ -1963,7 +2060,7 @@ impl Database {
         connection: ConnectionId,
         worktrees: &[proto::WorktreeMetadata],
     ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
-        self.room_transaction(|tx| async move {
+        self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
                 .filter(
                     Condition::all()
@@ -2024,7 +2121,7 @@ impl Database {
             .await?;
 
             let room = self.get_room(room_id, &tx).await?;
-            Ok((room_id, (project.id, room)))
+            Ok((project.id, room))
         })
         .await
     }
@@ -2034,7 +2131,8 @@ impl Database {
         project_id: ProjectId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
 
             let project = project::Entity::find_by_id(project_id)
@@ -2042,12 +2140,11 @@ impl Database {
                 .await?
                 .ok_or_else(|| anyhow!("project not found"))?;
             if project.host_connection()? == connection {
-                let room_id = project.room_id;
                 project::Entity::delete(project.into_active_model())
                     .exec(&*tx)
                     .await?;
                 let room = self.get_room(room_id, &tx).await?;
-                Ok((room_id, (room, guest_connection_ids)))
+                Ok((room, guest_connection_ids))
             } else {
                 Err(anyhow!("cannot unshare a project hosted by another user"))?
             }
@@ -2061,7 +2158,8 @@ impl Database {
         connection: ConnectionId,
         worktrees: &[proto::WorktreeMetadata],
     ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let project = project::Entity::find_by_id(project_id)
                 .filter(
                     Condition::all()
@@ -2079,7 +2177,7 @@ impl Database {
 
             let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
             let room = self.get_room(project.room_id, &tx).await?;
-            Ok((project.room_id, (room, guest_connection_ids)))
+            Ok((room, guest_connection_ids))
         })
         .await
     }
@@ -2124,12 +2222,12 @@ impl Database {
         update: &proto::UpdateWorktree,
         connection: ConnectionId,
     ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project_id = ProjectId::from_proto(update.project_id);
-            let worktree_id = update.worktree_id as i64;
-
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
+            let _project = project::Entity::find_by_id(project_id)
                 .filter(
                     Condition::all()
                         .add(project::Column::HostConnectionId.eq(connection.id as i32))
@@ -2140,7 +2238,6 @@ impl Database {
                 .one(&*tx)
                 .await?
                 .ok_or_else(|| anyhow!("no such project"))?;
-            let room_id = project.room_id;
 
             // Update metadata.
             worktree::Entity::update(worktree::ActiveModel {
@@ -2220,7 +2317,7 @@ impl Database {
             }
 
             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok((room_id, connection_ids))
+            Ok(connection_ids)
         })
         .await
     }
@@ -2230,9 +2327,10 @@ impl Database {
         update: &proto::UpdateDiagnosticSummary,
         connection: ConnectionId,
     ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project_id = ProjectId::from_proto(update.project_id);
-            let worktree_id = update.worktree_id as i64;
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let summary = update
                 .summary
                 .as_ref()
@@ -2274,7 +2372,7 @@ impl Database {
             .await?;
 
             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok((project.room_id, connection_ids))
+            Ok(connection_ids)
         })
         .await
     }
@@ -2284,8 +2382,9 @@ impl Database {
         update: &proto::StartLanguageServer,
         connection: ConnectionId,
     ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project_id = ProjectId::from_proto(update.project_id);
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let server = update
                 .server
                 .as_ref()
@@ -2319,7 +2418,7 @@ impl Database {
             .await?;
 
             let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok((project.room_id, connection_ids))
+            Ok(connection_ids)
         })
         .await
     }
@@ -2329,7 +2428,8 @@ impl Database {
         project_id: ProjectId,
         connection: ConnectionId,
     ) -> Result<RoomGuard<(Project, ReplicaId)>> {
-        self.room_transaction(|tx| async move {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let participant = room_participant::Entity::find()
                 .filter(
                     Condition::all()
@@ -2455,7 +2555,6 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
-            let room_id = project.room_id;
             let project = Project {
                 collaborators: collaborators
                     .into_iter()
@@ -2475,7 +2574,7 @@ impl Database {
                     })
                     .collect(),
             };
-            Ok((room_id, (project, replica_id as ReplicaId)))
+            Ok((project, replica_id as ReplicaId))
         })
         .await
     }
@@ -2484,8 +2583,9 @@ impl Database {
         &self,
         project_id: ProjectId,
         connection: ConnectionId,
-    ) -> Result<RoomGuard<LeftProject>> {
-        self.room_transaction(|tx| async move {
+    ) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let result = project_collaborator::Entity::delete_many()
                 .filter(
                     Condition::all()
@@ -2515,13 +2615,39 @@ impl Database {
                 .map(|collaborator| collaborator.connection())
                 .collect();
 
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::any()
+                        .add(
+                            Condition::all()
+                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(
+                                    follower::Column::LeaderConnectionServerId
+                                        .eq(connection.owner_id),
+                                )
+                                .add(follower::Column::LeaderConnectionId.eq(connection.id)),
+                        )
+                        .add(
+                            Condition::all()
+                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(
+                                    follower::Column::FollowerConnectionServerId
+                                        .eq(connection.owner_id),
+                                )
+                                .add(follower::Column::FollowerConnectionId.eq(connection.id)),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(project.room_id, &tx).await?;
             let left_project = LeftProject {
                 id: project_id,
                 host_user_id: project.host_user_id,
                 host_connection_id: project.host_connection()?,
                 connection_ids,
             };
-            Ok((project.room_id, left_project))
+            Ok((room, left_project))
         })
         .await
     }
@@ -2531,11 +2657,8 @@ impl Database {
         project_id: ProjectId,
         connection_id: ConnectionId,
     ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
-        self.room_transaction(|tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let collaborators = project_collaborator::Entity::find()
                 .filter(project_collaborator::Column::ProjectId.eq(project_id))
                 .all(&*tx)
@@ -2553,7 +2676,7 @@ impl Database {
                 .iter()
                 .any(|collaborator| collaborator.connection_id == connection_id)
             {
-                Ok((project.room_id, collaborators))
+                Ok(collaborators)
             } else {
                 Err(anyhow!("no such project"))?
             }
@@ -2566,11 +2689,8 @@ impl Database {
         project_id: ProjectId,
         connection_id: ConnectionId,
     ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
-        self.room_transaction(|tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
             let mut collaborators = project_collaborator::Entity::find()
                 .filter(project_collaborator::Column::ProjectId.eq(project_id))
                 .stream(&*tx)
@@ -2583,7 +2703,7 @@ impl Database {
             }
 
             if connection_ids.contains(&connection_id) {
-                Ok((project.room_id, connection_ids))
+                Ok(connection_ids)
             } else {
                 Err(anyhow!("no such project"))?
             }
@@ -2613,18 +2733,29 @@ impl Database {
         Ok(guest_connection_ids)
     }
 
+    async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
+        self.transaction(|tx| async move {
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("project {} not found", project_id))?;
+            Ok(project.room_id)
+        })
+        .await
+    }
+
     // access tokens
 
-    pub async fn create_access_token_hash(
+    pub async fn create_access_token(
         &self,
         user_id: UserId,
         access_token_hash: &str,
         max_access_token_count: usize,
-    ) -> Result<()> {
+    ) -> Result<AccessTokenId> {
         self.transaction(|tx| async {
             let tx = tx;
 
-            access_token::ActiveModel {
+            let token = access_token::ActiveModel {
                 user_id: ActiveValue::set(user_id),
                 hash: ActiveValue::set(access_token_hash.into()),
                 ..Default::default()
@@ -2647,26 +2778,20 @@ impl Database {
                 )
                 .exec(&*tx)
                 .await?;
-            Ok(())
+            Ok(token.id)
         })
         .await
     }
 
-    pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result<Vec<String>> {
-        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-        enum QueryAs {
-            Hash,
-        }
-
+    pub async fn get_access_token(
+        &self,
+        access_token_id: AccessTokenId,
+    ) -> Result<access_token::Model> {
         self.transaction(|tx| async move {
-            Ok(access_token::Entity::find()
-                .select_only()
-                .column(access_token::Column::Hash)
-                .filter(access_token::Column::UserId.eq(user_id))
-                .order_by_desc(access_token::Column::Id)
-                .into_values::<_, QueryAs>()
-                .all(&*tx)
-                .await?)
+            Ok(access_token::Entity::find_by_id(access_token_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such access token"))?)
         })
         .await
     }
@@ -2677,30 +2802,26 @@ impl Database {
         Fut: Send + Future<Output = Result<T>>,
     {
         let body = async {
+            let mut i = 0;
             loop {
                 let (tx, result) = self.with_transaction(&f).await?;
                 match result {
-                    Ok(result) => {
-                        match tx.commit().await.map_err(Into::into) {
-                            Ok(()) => return Ok(result),
-                            Err(error) => {
-                                if is_serialization_error(&error) {
-                                    // Retry (don't break the loop)
-                                } else {
-                                    return Err(error);
-                                }
+                    Ok(result) => match tx.commit().await.map_err(Into::into) {
+                        Ok(()) => return Ok(result),
+                        Err(error) => {
+                            if !self.retry_on_serialization_error(&error, i).await {
+                                return Err(error);
                             }
                         }
-                    }
+                    },
                     Err(error) => {
                         tx.rollback().await?;
-                        if is_serialization_error(&error) {
-                            // Retry (don't break the loop)
-                        } else {
+                        if !self.retry_on_serialization_error(&error, i).await {
                             return Err(error);
                         }
                     }
                 }
+                i += 1;
             }
         };
 
@@ -2713,6 +2834,7 @@ impl Database {
         Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
     {
         let body = async {
+            let mut i = 0;
             loop {
                 let (tx, result) = self.with_transaction(&f).await?;
                 match result {
@@ -2728,56 +2850,72 @@ impl Database {
                                 }));
                             }
                             Err(error) => {
-                                if is_serialization_error(&error) {
-                                    // Retry (don't break the loop)
-                                } else {
+                                if !self.retry_on_serialization_error(&error, i).await {
                                     return Err(error);
                                 }
                             }
                         }
                     }
-                    Ok(None) => {
-                        match tx.commit().await.map_err(Into::into) {
-                            Ok(()) => return Ok(None),
-                            Err(error) => {
-                                if is_serialization_error(&error) {
-                                    // Retry (don't break the loop)
-                                } else {
-                                    return Err(error);
-                                }
+                    Ok(None) => match tx.commit().await.map_err(Into::into) {
+                        Ok(()) => return Ok(None),
+                        Err(error) => {
+                            if !self.retry_on_serialization_error(&error, i).await {
+                                return Err(error);
                             }
                         }
-                    }
+                    },
                     Err(error) => {
                         tx.rollback().await?;
-                        if is_serialization_error(&error) {
-                            // Retry (don't break the loop)
-                        } else {
+                        if !self.retry_on_serialization_error(&error, i).await {
                             return Err(error);
                         }
                     }
                 }
+                i += 1;
             }
         };
 
         self.run(body).await
     }
 
-    async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
+    async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
     where
         F: Send + Fn(TransactionHandle) -> Fut,
-        Fut: Send + Future<Output = Result<(RoomId, T)>>,
+        Fut: Send + Future<Output = Result<T>>,
     {
-        let data = self
-            .optional_room_transaction(move |tx| {
-                let future = f(tx);
-                async {
-                    let data = future.await?;
-                    Ok(Some(data))
+        let body = async {
+            let mut i = 0;
+            loop {
+                let lock = self.rooms.entry(room_id).or_default().clone();
+                let _guard = lock.lock_owned().await;
+                let (tx, result) = self.with_transaction(&f).await?;
+                match result {
+                    Ok(data) => match tx.commit().await.map_err(Into::into) {
+                        Ok(()) => {
+                            return Ok(RoomGuard {
+                                data,
+                                _guard,
+                                _not_send: PhantomData,
+                            });
+                        }
+                        Err(error) => {
+                            if !self.retry_on_serialization_error(&error, i).await {
+                                return Err(error);
+                            }
+                        }
+                    },
+                    Err(error) => {
+                        tx.rollback().await?;
+                        if !self.retry_on_serialization_error(&error, i).await {
+                            return Err(error);
+                        }
+                    }
                 }
-            })
-            .await?;
-        Ok(data.unwrap())
+                i += 1;
+            }
+        };
+
+        self.run(body).await
     }
 
     async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
@@ -2799,14 +2937,14 @@ impl Database {
         Ok((tx, result))
     }
 
-    async fn run<F, T>(&self, future: F) -> T
+    async fn run<F, T>(&self, future: F) -> Result<T>
     where
-        F: Future<Output = T>,
+        F: Future<Output = Result<T>>,
     {
         #[cfg(test)]
         {
-            if let Some(background) = self.background.as_ref() {
-                background.simulate_random_delay().await;
+            if let Executor::Deterministic(executor) = &self.executor {
+                executor.simulate_random_delay().await;
             }
 
             self.runtime.as_ref().unwrap().block_on(future)
@@ -2817,6 +2955,27 @@ impl Database {
             future.await
         }
     }
+
+    async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool {
+        // If the error is due to a failure to serialize concurrent transactions, then retry
+        // this transaction after a delay. With each subsequent retry, double the delay duration.
+        // Also vary the delay randomly in order to ensure different database connections retry
+        // at different times.
+        if is_serialization_error(error) {
+            let base_delay = 4_u64 << prev_attempt_count.min(16);
+            let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
+            log::info!(
+                "retrying transaction after serialization error. delay: {} ms.",
+                randomized_delay
+            );
+            self.executor
+                .sleep(Duration::from_millis(randomized_delay as u64))
+                .await;
+            true
+        } else {
+            false
+        }
+    }
 }
 
 fn is_serialization_error(error: &Error) -> bool {
@@ -3011,6 +3170,7 @@ macro_rules! id_type {
 
 id_type!(AccessTokenId);
 id_type!(ContactId);
+id_type!(FollowerId);
 id_type!(RoomId);
 id_type!(RoomParticipantId);
 id_type!(ProjectId);
@@ -3117,7 +3277,6 @@ mod test {
     use gpui::executor::Background;
     use lazy_static::lazy_static;
     use parking_lot::Mutex;
-    use rand::prelude::*;
     use sea_orm::ConnectionTrait;
     use sqlx::migrate::MigrateDatabase;
     use std::sync::Arc;
@@ -3139,7 +3298,9 @@ mod test {
             let mut db = runtime.block_on(async {
                 let mut options = ConnectOptions::new(url);
                 options.max_connections(5);
-                let db = Database::new(options).await.unwrap();
+                let db = Database::new(options, Executor::Deterministic(background))
+                    .await
+                    .unwrap();
                 let sql = include_str!(concat!(
                     env!("CARGO_MANIFEST_DIR"),
                     "/migrations.sqlite/20221109000000_test_schema.sql"
@@ -3154,7 +3315,6 @@ mod test {
                 db
             });
 
-            db.background = Some(background);
             db.runtime = Some(runtime);
 
             Self {
@@ -3188,13 +3348,14 @@ mod test {
                 options
                     .max_connections(5)
                     .idle_timeout(Duration::from_secs(0));
-                let db = Database::new(options).await.unwrap();
+                let db = Database::new(options, Executor::Deterministic(background))
+                    .await
+                    .unwrap();
                 let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
                 db.migrate(Path::new(migrations_path), false).await.unwrap();
                 db
             });
 
-            db.background = Some(background);
             db.runtime = Some(runtime);
 
             Self {

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

@@ -0,0 +1,51 @@
+use super::{FollowerId, ProjectId, RoomId, ServerId};
+use rpc::ConnectionId;
+use sea_orm::entity::prelude::*;
+use serde::Serialize;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[sea_orm(table_name = "followers")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: FollowerId,
+    pub room_id: RoomId,
+    pub project_id: ProjectId,
+    pub leader_connection_server_id: ServerId,
+    pub leader_connection_id: i32,
+    pub follower_connection_server_id: ServerId,
+    pub follower_connection_id: i32,
+}
+
+impl Model {
+    pub fn leader_connection(&self) -> ConnectionId {
+        ConnectionId {
+            owner_id: self.leader_connection_server_id.0 as u32,
+            id: self.leader_connection_id as u32,
+        }
+    }
+
+    pub fn follower_connection(&self) -> ConnectionId {
+        ConnectionId {
+            owner_id: self.follower_connection_server_id.0 as u32,
+            id: self.follower_connection_id as u32,
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::room::Entity",
+        from = "Column::RoomId",
+        to = "super::room::Column::Id"
+    )]
+    Room,
+}
+
+impl Related<super::room::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Room.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -15,6 +15,8 @@ pub enum Relation {
     RoomParticipant,
     #[sea_orm(has_many = "super::project::Entity")]
     Project,
+    #[sea_orm(has_many = "super::follower::Entity")]
+    Follower,
 }
 
 impl Related<super::room_participant::Entity> for Entity {
@@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
     }
 }
 
+impl Related<super::follower::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Follower.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}

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

@@ -92,8 +92,8 @@ test_both_dbs!(
 );
 
 test_both_dbs!(
-    test_get_user_by_github_account_postgres,
-    test_get_user_by_github_account_sqlite,
+    test_get_or_create_user_by_github_account_postgres,
+    test_get_or_create_user_by_github_account_sqlite,
     db,
     {
         let user_id1 = db
@@ -124,7 +124,7 @@ test_both_dbs!(
             .user_id;
 
         let user = db
-            .get_user_by_github_account("login1", None)
+            .get_or_create_user_by_github_account("login1", None, None)
             .await
             .unwrap()
             .unwrap();
@@ -133,19 +133,28 @@ test_both_dbs!(
         assert_eq!(user.github_user_id, Some(101));
 
         assert!(db
-            .get_user_by_github_account("non-existent-login", None)
+            .get_or_create_user_by_github_account("non-existent-login", None, None)
             .await
             .unwrap()
             .is_none());
 
         let user = db
-            .get_user_by_github_account("the-new-login2", Some(102))
+            .get_or_create_user_by_github_account("the-new-login2", Some(102), None)
             .await
             .unwrap()
             .unwrap();
         assert_eq!(user.id, user_id2);
         assert_eq!(&user.github_login, "the-new-login2");
         assert_eq!(user.github_user_id, Some(102));
+
+        let user = db
+            .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com"))
+            .await
+            .unwrap()
+            .unwrap();
+        assert_eq!(&user.github_login, "login3");
+        assert_eq!(user.github_user_id, Some(103));
+        assert_eq!(user.email_address, Some("user3@example.com".into()));
     }
 );
 
@@ -168,30 +177,63 @@ test_both_dbs!(
             .unwrap()
             .user_id;
 
-        db.create_access_token_hash(user, "h1", 3).await.unwrap();
-        db.create_access_token_hash(user, "h2", 3).await.unwrap();
+        let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
+        let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
         assert_eq!(
-            db.get_access_token_hashes(user).await.unwrap(),
-            &["h2".to_string(), "h1".to_string()]
+            db.get_access_token(token_1).await.unwrap(),
+            access_token::Model {
+                id: token_1,
+                user_id: user,
+                hash: "h1".into(),
+            }
         );
-
-        db.create_access_token_hash(user, "h3", 3).await.unwrap();
         assert_eq!(
-            db.get_access_token_hashes(user).await.unwrap(),
-            &["h3".to_string(), "h2".to_string(), "h1".to_string(),]
+            db.get_access_token(token_2).await.unwrap(),
+            access_token::Model {
+                id: token_2,
+                user_id: user,
+                hash: "h2".into()
+            }
         );
 
-        db.create_access_token_hash(user, "h4", 3).await.unwrap();
+        let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
         assert_eq!(
-            db.get_access_token_hashes(user).await.unwrap(),
-            &["h4".to_string(), "h3".to_string(), "h2".to_string(),]
+            db.get_access_token(token_3).await.unwrap(),
+            access_token::Model {
+                id: token_3,
+                user_id: user,
+                hash: "h3".into()
+            }
+        );
+        assert_eq!(
+            db.get_access_token(token_2).await.unwrap(),
+            access_token::Model {
+                id: token_2,
+                user_id: user,
+                hash: "h2".into()
+            }
         );
+        assert!(db.get_access_token(token_1).await.is_err());
 
-        db.create_access_token_hash(user, "h5", 3).await.unwrap();
+        let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
         assert_eq!(
-            db.get_access_token_hashes(user).await.unwrap(),
-            &["h5".to_string(), "h4".to_string(), "h3".to_string()]
+            db.get_access_token(token_4).await.unwrap(),
+            access_token::Model {
+                id: token_4,
+                user_id: user,
+                hash: "h4".into()
+            }
+        );
+        assert_eq!(
+            db.get_access_token(token_3).await.unwrap(),
+            access_token::Model {
+                id: token_3,
+                user_id: user,
+                hash: "h3".into()
+            }
         );
+        assert!(db.get_access_token(token_2).await.is_err());
+        assert!(db.get_access_token(token_1).await.is_err());
     }
 );
 

crates/collab/src/lib.rs 🔗

@@ -10,6 +10,7 @@ mod tests;
 
 use axum::{http::StatusCode, response::IntoResponse};
 use db::Database;
+use executor::Executor;
 use serde::Deserialize;
 use std::{path::PathBuf, sync::Arc};
 
@@ -91,6 +92,7 @@ impl std::error::Error for Error {}
 pub struct Config {
     pub http_port: u16,
     pub database_url: String,
+    pub database_max_connections: u32,
     pub api_token: String,
     pub invite_link_prefix: String,
     pub live_kit_server: Option<String>,
@@ -116,8 +118,8 @@ pub struct AppState {
 impl AppState {
     pub async fn new(config: Config) -> Result<Arc<Self>> {
         let mut db_options = db::ConnectOptions::new(config.database_url.clone());
-        db_options.max_connections(5);
-        let db = Database::new(db_options).await?;
+        db_options.max_connections(config.database_max_connections);
+        let db = Database::new(db_options, Executor::Production).await?;
         let live_kit_client = if let Some(((server, key), secret)) = config
             .live_kit_server
             .as_ref()

crates/collab/src/main.rs 🔗

@@ -1,11 +1,12 @@
 use anyhow::anyhow;
-use axum::{routing::get, Router};
+use axum::{routing::get, Extension, Router};
 use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
 use db::Database;
 use std::{
     env::args,
     net::{SocketAddr, TcpListener},
     path::Path,
+    sync::Arc,
 };
 use tokio::signal::unix::SignalKind;
 use tracing_log::LogTracer;
@@ -31,7 +32,7 @@ async fn main() -> Result<()> {
             let config = envy::from_env::<MigrateConfig>().expect("error loading config");
             let mut db_options = db::ConnectOptions::new(config.database_url.clone());
             db_options.max_connections(5);
-            let db = Database::new(db_options).await?;
+            let db = Database::new(db_options, Executor::Production).await?;
 
             let migrations_path = config
                 .migrations_path
@@ -66,7 +67,12 @@ async fn main() -> Result<()> {
 
             let app = collab::api::routes(rpc_server.clone(), state.clone())
                 .merge(collab::rpc::routes(rpc_server.clone()))
-                .merge(Router::new().route("/", get(handle_root)));
+                .merge(
+                    Router::new()
+                        .route("/", get(handle_root))
+                        .route("/healthz", get(handle_liveness_probe))
+                        .layer(Extension(state.clone())),
+                );
 
             axum::Server::from_tcp(listener)?
                 .serve(app.into_make_service_with_connect_info::<SocketAddr>())
@@ -95,6 +101,11 @@ async fn handle_root() -> String {
     format!("collab v{VERSION}")
 }
 
+async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {
+    state.db.get_all_users(0, 1).await?;
+    Ok("ok".to_string())
+}
+
 pub fn init_tracing(config: &Config) -> Option<()> {
     use std::str::FromStr;
     use tracing_subscriber::layer::SubscriberExt;

crates/collab/src/rpc.rs 🔗

@@ -53,11 +53,11 @@ use std::{
     },
     time::Duration,
 };
-use tokio::sync::watch;
+use tokio::sync::{watch, Semaphore};
 use tower::ServiceBuilder;
 use tracing::{info_span, instrument, Instrument};
 
-pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
+pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
 
 lazy_static! {
@@ -186,7 +186,7 @@ impl Server {
             .add_request_handler(create_room)
             .add_request_handler(join_room)
             .add_request_handler(rejoin_room)
-            .add_message_handler(leave_room)
+            .add_request_handler(leave_room)
             .add_request_handler(call)
             .add_request_handler(cancel_call)
             .add_message_handler(decline_call)
@@ -270,8 +270,11 @@ impl Server {
                         let mut live_kit_room = String::new();
                         let mut delete_live_kit_room = false;
 
-                        if let Ok(mut refreshed_room) =
-                            app_state.db.refresh_room(room_id, server_id).await
+                        if let Some(mut refreshed_room) = app_state
+                            .db
+                            .refresh_room(room_id, server_id)
+                            .await
+                            .trace_err()
                         {
                             tracing::info!(
                                 room_id = room_id.0,
@@ -539,8 +542,13 @@ impl Server {
             // This arrangement ensures we will attempt to process earlier messages first, but fall
             // back to processing messages arrived later in the spirit of making progress.
             let mut foreground_message_handlers = FuturesUnordered::new();
+            let concurrent_handlers = Arc::new(Semaphore::new(256));
             loop {
-                let next_message = incoming_rx.next().fuse();
+                let next_message = async {
+                    let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
+                    let message = incoming_rx.next().await;
+                    (permit, message)
+                }.fuse();
                 futures::pin_mut!(next_message);
                 futures::select_biased! {
                     _ = teardown.changed().fuse() => return Ok(()),
@@ -551,7 +559,8 @@ impl Server {
                         break;
                     }
                     _ = foreground_message_handlers.next() => {}
-                    message = next_message => {
+                    next_message = next_message => {
+                        let (permit, message) = next_message;
                         if let Some(message) = message {
                             let type_name = message.payload_type_name();
                             let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
@@ -561,7 +570,10 @@ impl Server {
                                 let handle_message = (handler)(message, session.clone());
                                 drop(span_enter);
 
-                                let handle_message = handle_message.instrument(span);
+                                let handle_message = async move {
+                                    handle_message.await;
+                                    drop(permit);
+                                }.instrument(span);
                                 if is_background {
                                     executor.spawn_detached(handle_message);
                                 } else {
@@ -1090,8 +1102,14 @@ async fn rejoin_room(
     Ok(())
 }
 
-async fn leave_room(_message: proto::LeaveRoom, session: Session) -> Result<()> {
-    leave_room_for_session(&session).await
+async fn leave_room(
+    _: proto::LeaveRoom,
+    response: Response<proto::LeaveRoom>,
+    session: Session,
+) -> Result<()> {
+    leave_room_for_session(&session).await?;
+    response.send(proto::Ack {})?;
+    Ok(())
 }
 
 async fn call(
@@ -1312,6 +1330,7 @@ async fn join_project(
         .filter(|collaborator| collaborator.connection_id != session.connection_id)
         .map(|collaborator| collaborator.to_proto())
         .collect::<Vec<_>>();
+
     let worktrees = project
         .worktrees
         .iter()
@@ -1404,7 +1423,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
     let sender_id = session.connection_id;
     let project_id = ProjectId::from_proto(request.project_id);
 
-    let project = session
+    let (room, project) = &*session
         .db()
         .await
         .leave_project(project_id, sender_id)
@@ -1415,7 +1434,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
         host_connection_id = %project.host_connection_id,
         "leave project"
     );
+
     project_left(&project, &session);
+    room_updated(&room, &session.peer);
 
     Ok(())
 }
@@ -1724,6 +1745,7 @@ async fn follow(
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
     let follower_id = session.connection_id;
+
     {
         let project_connection_ids = session
             .db()
@@ -1744,6 +1766,14 @@ async fn follow(
         .views
         .retain(|view| view.leader_id != Some(follower_id.into()));
     response.send(response_payload)?;
+
+    let room = session
+        .db()
+        .await
+        .follow(project_id, leader_id, follower_id)
+        .await?;
+    room_updated(&room, &session.peer);
+
     Ok(())
 }
 
@@ -1753,17 +1783,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
         .leader_id
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
-    let project_connection_ids = session
+    let follower_id = session.connection_id;
+
+    if !session
         .db()
         .await
         .project_connection_ids(project_id, session.connection_id)
-        .await?;
-    if !project_connection_ids.contains(&leader_id) {
+        .await?
+        .contains(&leader_id)
+    {
         Err(anyhow!("no such peer"))?;
     }
+
     session
         .peer
         .forward_send(session.connection_id, leader_id, request)?;
+
+    let room = session
+        .db()
+        .await
+        .unfollow(project_id, leader_id, follower_id)
+        .await?;
+    room_updated(&room, &session.peer);
+
     Ok(())
 }
 
@@ -1833,7 +1875,7 @@ async fn fuzzy_search_users(
         1 | 2 => session
             .db()
             .await
-            .get_user_by_github_account(&query, None)
+            .get_user_by_github_login(&query)
             .await?
             .into_iter()
             .collect(),

crates/collab/src/tests.rs 🔗

@@ -7,15 +7,12 @@ use crate::{
 use anyhow::anyhow;
 use call::ActiveCall;
 use client::{
-    self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials,
-    EstablishConnectionError, UserStore,
+    self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
 };
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
 use futures::{channel::oneshot, StreamExt as _};
-use gpui::{
-    executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle,
-};
+use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle};
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
@@ -31,6 +28,7 @@ use std::{
     },
 };
 use theme::ThemeRegistry;
+use util::http::FakeHttpClient;
 use workspace::Workspace;
 
 mod integration_tests;
@@ -105,11 +103,7 @@ impl TestServer {
         });
 
         let http = FakeHttpClient::with_404_response();
-        let user_id = if let Ok(Some(user)) = self
-            .app_state
-            .db
-            .get_user_by_github_account(name, None)
-            .await
+        let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
         {
             user.id
         } else {
@@ -193,12 +187,13 @@ impl TestServer {
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
-            languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
+            languages: Arc::new(LanguageRegistry::test()),
             themes: ThemeRegistry::new((), cx.font_cache()),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _| unimplemented!(),
-            dock_default_item_factory: |_, _| unimplemented!(),
+            dock_default_item_factory: |_, _| None,
+            background_actions: || &[],
         });
 
         Project::init(&client);
@@ -468,15 +463,7 @@ impl TestClient {
         cx: &mut TestAppContext,
     ) -> ViewHandle<Workspace> {
         let (_, root_view) = cx.add_window(|_| EmptyView);
-        cx.add_view(&root_view, |cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        })
+        cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
     }
 }
 

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

@@ -274,10 +274,14 @@ async fn test_basic_calls(
     }
 
     // User A leaves the room.
-    active_call_a.update(cx_a, |call, cx| {
-        call.hang_up(cx).unwrap();
-        assert!(call.room().is_none());
-    });
+    active_call_a
+        .update(cx_a, |call, cx| {
+            let hang_up = call.hang_up(cx);
+            assert!(call.room().is_none());
+            hang_up
+        })
+        .await
+        .unwrap();
     deterministic.run_until_parked();
     assert_eq!(
         room_participants(&room_a, cx_a),
@@ -557,6 +561,7 @@ async fn test_room_uniqueness(
     // Client C can successfully call client B after client B leaves the room.
     active_call_b
         .update(cx_b, |call, cx| call.hang_up(cx))
+        .await
         .unwrap();
     deterministic.run_until_parked();
     active_call_c
@@ -733,6 +738,14 @@ async fn test_server_restarts(
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
+    client_a
+        .fs
+        .insert_tree("/a", json!({ "a.txt": "a-contents" }))
+        .await;
+
+    // Invite client B to collaborate on a project
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+
     let client_b = server.create_client(cx_b, "user_b").await;
     let client_c = server.create_client(cx_c, "user_c").await;
     let client_d = server.create_client(cx_d, "user_d").await;
@@ -753,19 +766,19 @@ async fn test_server_restarts(
     // User A calls users B, C, and D.
     active_call_a
         .update(cx_a, |call, cx| {
-            call.invite(client_b.user_id().unwrap(), None, cx)
+            call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
         })
         .await
         .unwrap();
     active_call_a
         .update(cx_a, |call, cx| {
-            call.invite(client_c.user_id().unwrap(), None, cx)
+            call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
         })
         .await
         .unwrap();
     active_call_a
         .update(cx_a, |call, cx| {
-            call.invite(client_d.user_id().unwrap(), None, cx)
+            call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
         })
         .await
         .unwrap();
@@ -821,7 +834,7 @@ async fn test_server_restarts(
 
     // Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
     client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
-    deterministic.advance_clock(RECEIVE_TIMEOUT);
+    deterministic.advance_clock(RECONNECT_TIMEOUT);
     assert_eq!(
         room_participants(&room_a, cx_a),
         RoomParticipants {
@@ -928,6 +941,7 @@ async fn test_server_restarts(
     // User D hangs up.
     active_call_d
         .update(cx_d, |call, cx| call.hang_up(cx))
+        .await
         .unwrap();
     deterministic.run_until_parked();
     assert_eq!(
@@ -993,7 +1007,7 @@ async fn test_server_restarts(
     client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
     client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
     client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
-    deterministic.advance_clock(RECEIVE_TIMEOUT);
+    deterministic.advance_clock(RECONNECT_TIMEOUT);
     assert_eq!(
         room_participants(&room_a, cx_a),
         RoomParticipants {
@@ -1083,7 +1097,7 @@ async fn test_calls_on_multiple_connections(
     assert!(incoming_call_b2.next().await.unwrap().is_none());
 
     // User B disconnects the client that is not on the call. Everything should be fine.
-    client_b1.disconnect(&cx_b1.to_async()).unwrap();
+    client_b1.disconnect(&cx_b1.to_async());
     deterministic.advance_clock(RECEIVE_TIMEOUT);
     client_b1
         .authenticate_and_connect(false, &cx_b1.to_async())
@@ -1091,7 +1105,10 @@ async fn test_calls_on_multiple_connections(
         .unwrap();
 
     // User B hangs up, and user A calls them again.
-    active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
+    active_call_b2
+        .update(cx_b2, |call, cx| call.hang_up(cx))
+        .await
+        .unwrap();
     deterministic.run_until_parked();
     active_call_a
         .update(cx_a, |call, cx| {
@@ -1126,7 +1143,10 @@ async fn test_calls_on_multiple_connections(
     assert!(incoming_call_b2.next().await.unwrap().is_some());
 
     // User A hangs up, causing both connections to stop ringing.
-    active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
+    active_call_a
+        .update(cx_a, |call, cx| call.hang_up(cx))
+        .await
+        .unwrap();
     deterministic.run_until_parked();
     assert!(incoming_call_b1.next().await.unwrap().is_none());
     assert!(incoming_call_b2.next().await.unwrap().is_none());
@@ -1363,7 +1383,10 @@ async fn test_unshare_project(
         .unwrap();
 
     // When client B leaves the room, the project becomes read-only.
-    active_call_b.update(cx_b, |call, cx| call.hang_up(cx).unwrap());
+    active_call_b
+        .update(cx_b, |call, cx| call.hang_up(cx))
+        .await
+        .unwrap();
     deterministic.run_until_parked();
     assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
 
@@ -1392,7 +1415,10 @@ async fn test_unshare_project(
         .unwrap();
 
     // When client A (the host) leaves the room, the project gets unshared and guests are notified.
-    active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
+    active_call_a
+        .update(cx_a, |call, cx| call.hang_up(cx))
+        .await
+        .unwrap();
     deterministic.run_until_parked();
     project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
     project_c2.read_with(cx_c, |project, _| {
@@ -1441,15 +1467,7 @@ async fn test_host_disconnect(
     deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let (_, workspace_b) = cx_b.add_window(|cx| {
-        Workspace::new(
-            Default::default(),
-            0,
-            project_b.clone(),
-            |_, _| unimplemented!(),
-            cx,
-        )
-    });
+    let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -1726,10 +1744,6 @@ async fn test_project_reconnect(
             vec![
                 "a.txt",
                 "b.txt",
-                "subdir1",
-                "subdir1/c.txt",
-                "subdir1/d.txt",
-                "subdir1/e.txt",
                 "subdir2",
                 "subdir2/f.txt",
                 "subdir2/g.txt",
@@ -1762,10 +1776,6 @@ async fn test_project_reconnect(
             vec![
                 "a.txt",
                 "b.txt",
-                "subdir1",
-                "subdir1/c.txt",
-                "subdir1/d.txt",
-                "subdir1/e.txt",
                 "subdir2",
                 "subdir2/f.txt",
                 "subdir2/g.txt",
@@ -1857,10 +1867,6 @@ async fn test_project_reconnect(
             vec![
                 "a.txt",
                 "b.txt",
-                "subdir1",
-                "subdir1/c.txt",
-                "subdir1/d.txt",
-                "subdir1/e.txt",
                 "subdir2",
                 "subdir2/f.txt",
                 "subdir2/g.txt",
@@ -2244,7 +2250,9 @@ async fn test_propagate_saves_and_fs_changes(
     });
 
     // Edit the buffer as the host and concurrently save as guest B.
-    let save_b = project_b.update(cx_b, |project, cx| project.save_buffer(buffer_b.clone(), cx));
+    let save_b = project_b.update(cx_b, |project, cx| {
+        project.save_buffer(buffer_b.clone(), cx)
+    });
     buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
     save_b.await.unwrap();
     assert_eq!(
@@ -2917,7 +2925,10 @@ async fn test_buffer_conflict_after_save(
         assert!(!buf.has_conflict());
     });
 
-    project_b.update(cx_b, |project, cx| project.save_buffer(buffer_b.clone(), cx))
+    project_b
+        .update(cx_b, |project, cx| {
+            project.save_buffer(buffer_b.clone(), cx)
+        })
         .await
         .unwrap();
     cx_a.foreground().forbid_parking();
@@ -3222,7 +3233,7 @@ async fn test_leaving_project(
     buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
 
     // Drop client B's connection and ensure client A and client C observe client B leaving.
-    client_b.disconnect(&cx_b.to_async()).unwrap();
+    client_b.disconnect(&cx_b.to_async());
     deterministic.advance_clock(RECONNECT_TIMEOUT);
     project_a.read_with(cx_a, |project, _| {
         assert_eq!(project.collaborators().len(), 1);
@@ -3879,9 +3890,11 @@ async fn test_formatting_buffer(
         })
         .await
         .unwrap();
+
+    // The edits from the LSP are applied, and a final newline is added.
     assert_eq!(
         buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
-        "let honey = \"two\""
+        "let honey = \"two\"\n"
     );
 
     // Ensure buffer can be formatted using an external command. Notice how the
@@ -4691,15 +4704,7 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| {
-        Workspace::new(
-            Default::default(),
-            0,
-            project_b.clone(),
-            |_, _| unimplemented!(),
-            cx,
-        )
-    });
+    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -4922,15 +4927,7 @@ async fn test_collaborating_with_renames(
         .unwrap();
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| {
-        Workspace::new(
-            Default::default(),
-            0,
-            project_b.clone(),
-            |_, _| unimplemented!(),
-            cx,
-        )
-    });
+    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@@ -5464,7 +5461,10 @@ async fn test_contacts(
         [("user_b".to_string(), "online", "busy")]
     );
 
-    active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
+    active_call_a
+        .update(cx_a, |call, cx| call.hang_up(cx))
+        .await
+        .unwrap();
     deterministic.run_until_parked();
     assert_eq!(
         contacts(&client_a, cx_a),
@@ -5767,7 +5767,7 @@ async fn test_contact_requests(
         .is_empty());
 
     async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
-        client.disconnect(&cx.to_async()).unwrap();
+        client.disconnect(&cx.to_async());
         client.clear_contacts(cx).await;
         client
             .authenticate_and_connect(false, &cx.to_async())
@@ -5777,10 +5777,12 @@ async fn test_contact_requests(
 }
 
 #[gpui::test(iterations = 10)]
-async fn test_following(
+async fn test_basic_following(
     deterministic: Arc<Deterministic>,
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+    cx_d: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
     cx_a.update(editor::init);
@@ -5789,8 +5791,15 @@ async fn test_following(
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    let client_d = server.create_client(cx_d, "user_d").await;
     server
-        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .create_room(&mut [
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+            (&client_d, cx_d),
+        ])
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
@@ -5822,8 +5831,10 @@ async fn test_following(
         .await
         .unwrap();
 
-    // Client A opens some editors.
     let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+
+    // Client A opens some editors.
     let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
     let editor_a1 = workspace_a
         .update(cx_a, |workspace, cx| {
@@ -5843,7 +5854,6 @@ async fn test_following(
         .unwrap();
 
     // Client B opens an editor.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b);
     let editor_b1 = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -5853,29 +5863,184 @@ async fn test_following(
         .downcast::<Editor>()
         .unwrap();
 
-    let client_a_id = project_b.read_with(cx_b, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-    let client_b_id = project_a.read_with(cx_a, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
+    let peer_id_a = client_a.peer_id().unwrap();
+    let peer_id_b = client_b.peer_id().unwrap();
+    let peer_id_c = client_c.peer_id().unwrap();
+    let peer_id_d = client_d.peer_id().unwrap();
 
-    // When client B starts following client A, all visible view states are replicated to client B.
+    // Client A updates their selections in those editors
     editor_a1.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
     });
     editor_a2.update(cx_a, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
     });
+
+    // When client B starts following client A, all visible view states are replicated to client B.
     workspace_b
         .update(cx_b, |workspace, cx| {
             workspace
-                .toggle_follow(&ToggleFollow(client_a_id), cx)
+                .toggle_follow(&ToggleFollow(peer_id_a), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+
+    cx_c.foreground().run_until_parked();
+    let active_call_c = cx_c.read(ActiveCall::global);
+    let project_c = client_c.build_remote_project(project_id, cx_c).await;
+    let workspace_c = client_c.build_workspace(&project_c, cx_c);
+    active_call_c
+        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+        .await
+        .unwrap();
+    drop(project_c);
+
+    // Client C also follows client A.
+    workspace_c
+        .update(cx_c, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(peer_id_a), cx)
                 .unwrap()
         })
         .await
         .unwrap();
 
+    cx_d.foreground().run_until_parked();
+    let active_call_d = cx_d.read(ActiveCall::global);
+    let project_d = client_d.build_remote_project(project_id, cx_d).await;
+    let workspace_d = client_d.build_workspace(&project_d, cx_d);
+    active_call_d
+        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
+        .await
+        .unwrap();
+    drop(project_d);
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C unfollows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
+    });
+
+    // All clients see that clients B is following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C re-follows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
+    });
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client D follows client C.
+    workspace_d
+        .update(cx_d, |workspace, cx| {
+            workspace
+                .toggle_follow(&ToggleFollow(peer_id_c), cx)
+                .unwrap()
+        })
+        .await
+        .unwrap();
+
+    // All clients see that D is following C
+    cx_d.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_c, project_id),
+                &[peer_id_d],
+                "checking followers for C as {name}"
+            );
+        });
+    }
+
+    // Client C closes the project.
+    cx_c.drop_last(workspace_c);
+
+    // Clients A and B see that client B is following A, and client C is not present in the followers.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // All clients see that no-one is following C
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_c, project_id),
+                &[],
+                "checking followers for C as {name}"
+            );
+        });
+    }
+
     let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
         workspace
             .active_item(cx)
@@ -6028,14 +6193,14 @@ async fn test_following(
     workspace_a
         .update(cx_a, |workspace, cx| {
             workspace
-                .toggle_follow(&ToggleFollow(client_b_id), cx)
+                .toggle_follow(&ToggleFollow(peer_id_b), cx)
                 .unwrap()
         })
         .await
         .unwrap();
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        Some(client_b_id)
+        Some(peer_id_b)
     );
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, cx| workspace
@@ -6107,7 +6272,7 @@ async fn test_following(
     );
 
     // Following interrupts when client B disconnects.
-    client_b.disconnect(&cx_b.to_async()).unwrap();
+    client_b.disconnect(&cx_b.to_async());
     deterministic.advance_clock(RECONNECT_TIMEOUT);
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
@@ -6115,6 +6280,99 @@ async fn test_following(
     );
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_join_call_after_screen_was_shared(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    // Call users B and C from client A.
+    active_call_a
+        .update(cx_a, |call, cx| {
+            call.invite(client_b.user_id().unwrap(), None, cx)
+        })
+        .await
+        .unwrap();
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    deterministic.run_until_parked();
+    assert_eq!(
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: Default::default(),
+            pending: vec!["user_b".to_string()]
+        }
+    );
+
+    // User B receives the call.
+    let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
+    let call_b = incoming_call_b.next().await.unwrap().unwrap();
+    assert_eq!(call_b.calling_user.github_login, "user_a");
+
+    // User A shares their screen
+    let display = MacOSDisplay::new();
+    active_call_a
+        .update(cx_a, |call, cx| {
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
+        })
+        .await
+        .unwrap();
+
+    client_b.user_store.update(cx_b, |user_store, _| {
+        user_store.clear_cache();
+    });
+
+    // User B joins the room
+    active_call_b
+        .update(cx_b, |call, cx| call.accept_incoming(cx))
+        .await
+        .unwrap();
+    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+    assert!(incoming_call_b.next().await.unwrap().is_none());
+
+    deterministic.run_until_parked();
+    assert_eq!(
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: vec!["user_b".to_string()],
+            pending: vec![],
+        }
+    );
+    assert_eq!(
+        room_participants(&room_b, cx_b),
+        RoomParticipants {
+            remote: vec!["user_a".to_string()],
+            pending: vec![],
+        }
+    );
+
+    // Ensure User B sees User A's screenshare.
+    room_b.read_with(cx_b, |room, _| {
+        assert_eq!(
+            room.remote_participants()
+                .get(&client_a.user_id().unwrap())
+                .unwrap()
+                .tracks
+                .len(),
+            1
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_following_tab_order(
     deterministic: Arc<Deterministic>,

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

@@ -554,7 +554,7 @@ async fn apply_client_operation(
             }
 
             log::info!("{}: hanging up", client.username);
-            active_call.update(cx, |call, cx| call.hang_up(cx))?;
+            active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
         }
 
         ClientOperation::InviteContactToCall { user_id } => {

crates/collab_ui/Cargo.toml 🔗

@@ -27,7 +27,9 @@ call = { path = "../call" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
 editor = { path = "../editor" }
+feedback = { path = "../feedback" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 menu = { path = "../menu" }
@@ -40,8 +42,9 @@ workspace = { path = "../workspace" }
 anyhow = "1.0"
 futures = "0.3"
 log = "0.4"
-postage = { version = "0.4.1", features = ["futures-traits"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
+postage = { workspace = true }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,33 +1,60 @@
-use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing};
-use call::{ActiveCall, ParticipantLocation};
-use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
+use crate::{
+    collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
+    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
+    ToggleScreenSharing,
+};
+use call::{ActiveCall, ParticipantLocation, Room};
+use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
 use clock::ReplicaId;
 use contacts_popover::ContactsPopover;
+use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     actions,
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
+    impl_internal_actions,
     json::{self, ToJson},
-    Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
+    CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext,
     Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use settings::Settings;
-use std::ops::Range;
-use theme::Theme;
+use std::{ops::Range, sync::Arc};
+use theme::{AvatarStyle, Theme};
+use util::ResultExt;
 use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
 
-actions!(collab, [ToggleCollaborationMenu, ShareProject]);
+actions!(
+    collab,
+    [
+        ToggleCollaboratorList,
+        ToggleContactsMenu,
+        ToggleUserMenu,
+        ShareProject,
+        UnshareProject,
+    ]
+);
+
+impl_internal_actions!(collab, [LeaveCall]);
+
+#[derive(Copy, Clone, PartialEq)]
+pub(crate) struct LeaveCall;
 
 pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
     cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
     cx.add_action(CollabTitlebarItem::share_project);
+    cx.add_action(CollabTitlebarItem::unshare_project);
+    cx.add_action(CollabTitlebarItem::leave_call);
+    cx.add_action(CollabTitlebarItem::toggle_user_menu);
 }
 
 pub struct CollabTitlebarItem {
     workspace: WeakViewHandle<Workspace>,
     user_store: ModelHandle<UserStore>,
     contacts_popover: Option<ViewHandle<ContactsPopover>>,
+    user_menu: ViewHandle<ContextMenu>,
+    collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -47,27 +74,62 @@ impl View for CollabTitlebarItem {
             return Empty::new().boxed();
         };
 
+        let project = workspace.read(cx).project().read(cx);
+        let mut project_title = String::new();
+        for (i, name) in project.worktree_root_names(cx).enumerate() {
+            if i > 0 {
+                project_title.push_str(", ");
+            }
+            project_title.push_str(name);
+        }
+        if project_title.is_empty() {
+            project_title = "empty project".to_owned();
+        }
+
         let theme = cx.global::<Settings>().theme.clone();
 
-        let mut container = Flex::row();
+        let mut left_container = Flex::row();
+        let mut right_container = Flex::row().align_children_center();
+
+        left_container.add_child(
+            Label::new(project_title, theme.workspace.titlebar.title.clone())
+                .contained()
+                .with_margin_right(theme.workspace.titlebar.item_spacing)
+                .aligned()
+                .left()
+                .boxed(),
+        );
 
-        container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
+        let user = workspace.read(cx).user_store().read(cx).current_user();
+        let peer_id = workspace.read(cx).client().peer_id();
+        if let Some(((user, peer_id), room)) = user
+            .zip(peer_id)
+            .zip(ActiveCall::global(cx).read(cx).room().cloned())
+        {
+            left_container
+                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
 
-        if workspace.read(cx).client().status().borrow().is_connected() {
-            let project = workspace.read(cx).project().read(cx);
-            if project.is_shared()
-                || project.is_remote()
-                || ActiveCall::global(cx).read(cx).room().is_none()
-            {
-                container.add_child(self.render_toggle_contacts_button(&theme, cx));
-            } else {
-                container.add_child(self.render_share_button(&theme, cx));
-            }
+            right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
+            right_container
+                .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
+            right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
         }
-        container.add_children(self.render_collaborators(&workspace, &theme, cx));
-        container.add_children(self.render_current_user(&workspace, &theme, cx));
-        container.add_children(self.render_connection_status(&workspace, cx));
-        container.boxed()
+
+        let status = workspace.read(cx).client().status();
+        let status = &*status.borrow();
+
+        if matches!(status, client::Status::Connected { .. }) {
+            right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
+            right_container.add_child(self.render_user_menu_button(&theme, cx));
+        } else {
+            right_container.add_children(self.render_connection_status(status, cx));
+            right_container.add_child(self.render_sign_in_button(&theme, cx));
+        }
+
+        Stack::new()
+            .with_child(left_container.boxed())
+            .with_child(right_container.aligned().right().boxed())
+            .boxed()
     }
 }
 
@@ -80,7 +142,7 @@ impl CollabTitlebarItem {
         let active_call = ActiveCall::global(cx);
         let mut subscriptions = Vec::new();
         subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
-        subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
+        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
         subscriptions.push(cx.observe_window_activation(|this, active, cx| {
             this.window_activation_changed(active, cx)
         }));
@@ -112,6 +174,12 @@ impl CollabTitlebarItem {
             workspace: workspace.downgrade(),
             user_store: user_store.clone(),
             contacts_popover: None,
+            user_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(cx);
+                menu.set_position_mode(OverlayPositionMode::Local);
+                menu
+            }),
+            collaborator_list_popover: None,
             _subscriptions: subscriptions,
         }
     }
@@ -129,6 +197,13 @@ impl CollabTitlebarItem {
         }
     }
 
+    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
+        if ActiveCall::global(cx).read(cx).room().is_none() {
+            self.contacts_popover = None;
+        }
+        cx.notify();
+    }
+
     fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let active_call = ActiveCall::global(cx);
@@ -139,41 +214,120 @@ impl CollabTitlebarItem {
         }
     }
 
-    pub fn toggle_contacts_popover(
+    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let active_call = ActiveCall::global(cx);
+            let project = workspace.read(cx).project().clone();
+            active_call
+                .update(cx, |call, cx| call.unshare_project(project, cx))
+                .log_err();
+        }
+    }
+
+    pub fn toggle_collaborator_list_popover(
         &mut self,
-        _: &ToggleCollaborationMenu,
+        _: &ToggleCollaboratorList,
         cx: &mut ViewContext<Self>,
     ) {
-        match self.contacts_popover.take() {
+        match self.collaborator_list_popover.take() {
             Some(_) => {}
             None => {
                 if let Some(workspace) = self.workspace.upgrade(cx) {
-                    let project = workspace.read(cx).project().clone();
                     let user_store = workspace.read(cx).user_store().clone();
-                    let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+                    let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
+
                     cx.subscribe(&view, |this, _, event, cx| {
                         match event {
-                            contacts_popover::Event::Dismissed => {
-                                this.contacts_popover = None;
+                            collaborator_list_popover::Event::Dismissed => {
+                                this.collaborator_list_popover = None;
                             }
                         }
 
                         cx.notify();
                     })
                     .detach();
-                    self.contacts_popover = Some(view);
+
+                    self.collaborator_list_popover = Some(view);
                 }
             }
         }
         cx.notify();
     }
 
+    pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
+        if self.contacts_popover.take().is_none() {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                let project = workspace.read(cx).project().clone();
+                let user_store = workspace.read(cx).user_store().clone();
+                let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
+                cx.subscribe(&view, |this, _, event, cx| {
+                    match event {
+                        contacts_popover::Event::Dismissed => {
+                            this.contacts_popover = None;
+                        }
+                    }
+
+                    cx.notify();
+                })
+                .detach();
+                self.contacts_popover = Some(view);
+            }
+        }
+
+        cx.notify();
+    }
+
+    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
+        let theme = cx.global::<Settings>().theme.clone();
+        let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
+        let item_style = theme.context_menu.item.disabled_style().clone();
+        self.user_menu.update(cx, |user_menu, cx| {
+            let items = if let Some(user) = self.user_store.read(cx).current_user() {
+                vec![
+                    ContextMenuItem::Static(Box::new(move |_| {
+                        Flex::row()
+                            .with_children(user.avatar.clone().map(|avatar| {
+                                Self::render_face(
+                                    avatar,
+                                    avatar_style.clone(),
+                                    Color::transparent_black(),
+                                )
+                            }))
+                            .with_child(
+                                Label::new(user.github_login.clone(), item_style.label.clone())
+                                    .boxed(),
+                            )
+                            .contained()
+                            .with_style(item_style.container)
+                            .boxed()
+                    })),
+                    ContextMenuItem::item("Sign out", SignOut),
+                    ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
+                ]
+            } else {
+                vec![
+                    ContextMenuItem::item("Sign in", SignIn),
+                    ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
+                ]
+            };
+
+            user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
+        });
+    }
+
+    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
+        ActiveCall::global(cx)
+            .update(cx, |call, cx| call.hang_up(cx))
+            .detach_and_log_err(cx);
+    }
+
     fn render_toggle_contacts_button(
         &self,
         theme: &Theme,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         let titlebar = &theme.workspace.titlebar;
+
         let badge = if self
             .user_store
             .read(cx)
@@ -194,13 +348,14 @@ impl CollabTitlebarItem {
                     .boxed(),
             )
         };
+
         Stack::new()
             .with_child(
-                MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
+                MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
                     let style = titlebar
                         .toggle_contacts_button
                         .style_for(state, self.contacts_popover.is_some());
-                    Svg::new("icons/plus_8.svg")
+                    Svg::new("icons/user_plus_16.svg")
                         .with_color(style.color)
                         .constrained()
                         .with_width(style.icon_width)
@@ -214,39 +369,30 @@ impl CollabTitlebarItem {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, cx| {
-                    cx.dispatch_action(ToggleCollaborationMenu);
+                    cx.dispatch_action(ToggleContactsMenu);
                 })
-                .aligned()
+                .with_tooltip::<ToggleContactsMenu, _>(
+                    0,
+                    "Show contacts menu".into(),
+                    Some(Box::new(ToggleContactsMenu)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
                 .boxed(),
             )
             .with_children(badge)
-            .with_children(self.contacts_popover.as_ref().map(|popover| {
-                Overlay::new(
-                    ChildView::new(popover, cx)
-                        .contained()
-                        .with_margin_top(titlebar.height)
-                        .with_margin_left(titlebar.toggle_contacts_button.default.button_width)
-                        .with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
-                        .boxed(),
-                )
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::BottomLeft)
-                .with_z_index(999)
-                .boxed()
-            }))
+            .with_children(self.render_contacts_popover_host(titlebar, cx))
             .boxed()
     }
 
     fn render_toggle_screen_sharing_button(
         &self,
         theme: &Theme,
+        room: &ModelHandle<Room>,
         cx: &mut RenderContext<Self>,
-    ) -> Option<ElementBox> {
-        let active_call = ActiveCall::global(cx);
-        let room = active_call.read(cx).room().cloned()?;
+    ) -> ElementBox {
         let icon;
         let tooltip;
-
         if room.read(cx).is_screen_sharing() {
             icon = "icons/disable_screen_sharing_12.svg";
             tooltip = "Stop Sharing Screen"
@@ -256,226 +402,383 @@ impl CollabTitlebarItem {
         }
 
         let titlebar = &theme.workspace.titlebar;
+        MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
+            let style = titlebar.call_control.style_for(state, false);
+            Svg::new(icon)
+                .with_color(style.color)
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .with_height(style.button_width)
+                .contained()
+                .with_style(style.container)
+                .boxed()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, cx| {
+            cx.dispatch_action(ToggleScreenSharing);
+        })
+        .with_tooltip::<ToggleScreenSharing, _>(
+            0,
+            tooltip.into(),
+            Some(Box::new(ToggleScreenSharing)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .boxed()
+    }
+
+    fn render_in_call_share_unshare_button(
+        &self,
+        workspace: &ViewHandle<Workspace>,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> Option<ElementBox> {
+        let project = workspace.read(cx).project();
+        if project.read(cx).is_remote() {
+            return None;
+        }
+
+        let is_shared = project.read(cx).is_shared();
+        let label = if is_shared { "Unshare" } else { "Share" };
+        let tooltip = if is_shared {
+            "Unshare project from call participants"
+        } else {
+            "Share project with call participants"
+        };
+
+        let titlebar = &theme.workspace.titlebar;
+
+        enum ShareUnshare {}
         Some(
-            MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
-                let style = titlebar.call_control.style_for(state, false);
-                Svg::new(icon)
-                    .with_color(style.color)
-                    .constrained()
-                    .with_width(style.icon_width)
-                    .aligned()
-                    .constrained()
-                    .with_width(style.button_width)
-                    .with_height(style.button_width)
-                    .contained()
-                    .with_style(style.container)
-                    .boxed()
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, cx| {
-                cx.dispatch_action(ToggleScreenSharing);
-            })
-            .with_tooltip::<ToggleScreenSharing, _>(
-                0,
-                tooltip.into(),
-                Some(Box::new(ToggleScreenSharing)),
-                theme.tooltip.clone(),
-                cx,
-            )
-            .aligned()
-            .boxed(),
+            Stack::new()
+                .with_child(
+                    MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
+                        //TODO: Ensure this button has consistant width for both text variations
+                        let style = titlebar
+                            .share_button
+                            .style_for(state, self.contacts_popover.is_some());
+                        Label::new(label, style.text.clone())
+                            .contained()
+                            .with_style(style.container)
+                            .boxed()
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_click(MouseButton::Left, move |_, cx| {
+                        if is_shared {
+                            cx.dispatch_action(UnshareProject);
+                        } else {
+                            cx.dispatch_action(ShareProject);
+                        }
+                    })
+                    .with_tooltip::<ShareUnshare, _>(
+                        0,
+                        tooltip.to_owned(),
+                        None,
+                        theme.tooltip.clone(),
+                        cx,
+                    )
+                    .boxed(),
+                )
+                .aligned()
+                .contained()
+                .with_margin_left(theme.workspace.titlebar.item_spacing)
+                .boxed(),
         )
     }
 
-    fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
-        enum Share {}
+    fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
+        let titlebar = &theme.workspace.titlebar;
 
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
+                    let style = titlebar.call_control.style_for(state, false);
+                    Svg::new("icons/ellipsis_14.svg")
+                        .with_color(style.color)
+                        .constrained()
+                        .with_width(style.icon_width)
+                        .aligned()
+                        .constrained()
+                        .with_width(style.button_width)
+                        .with_height(style.button_width)
+                        .contained()
+                        .with_style(style.container)
+                        .boxed()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(ToggleUserMenu);
+                })
+                .with_tooltip::<ToggleUserMenu, _>(
+                    0,
+                    "Toggle user menu".to_owned(),
+                    Some(Box::new(ToggleUserMenu)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .contained()
+                .with_margin_left(theme.workspace.titlebar.item_spacing)
+                .boxed(),
+            )
+            .with_child(
+                ChildView::new(&self.user_menu, cx)
+                    .aligned()
+                    .bottom()
+                    .right()
+                    .boxed(),
+            )
+            .boxed()
+    }
+
+    fn render_sign_in_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
         let titlebar = &theme.workspace.titlebar;
-        MouseEventHandler::<Share>::new(0, cx, |state, _| {
-            let style = titlebar.share_button.style_for(state, false);
-            Label::new("Share".into(), style.text.clone())
+        MouseEventHandler::<SignIn>::new(0, cx, |state, _| {
+            let style = titlebar.sign_in_prompt.style_for(state, false);
+            Label::new("Sign In", style.text.clone())
                 .contained()
                 .with_style(style.container)
                 .boxed()
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
-        .with_tooltip::<Share, _>(
-            0,
-            "Share project with call participants".into(),
-            None,
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .contained()
-        .with_margin_left(theme.workspace.titlebar.avatar_margin)
+        .on_click(MouseButton::Left, move |_, cx| {
+            cx.dispatch_action(SignIn);
+        })
         .boxed()
     }
 
+    fn render_contacts_popover_host<'a>(
+        &'a self,
+        _theme: &'a theme::Titlebar,
+        cx: &'a RenderContext<Self>,
+    ) -> Option<ElementBox> {
+        self.contacts_popover.as_ref().map(|popover| {
+            Overlay::new(ChildView::new(popover, cx).boxed())
+                .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                .with_anchor_corner(AnchorCorner::TopRight)
+                .with_z_index(999)
+                .aligned()
+                .bottom()
+                .right()
+                .boxed()
+        })
+    }
+
     fn render_collaborators(
         &self,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
+        room: &ModelHandle<Room>,
         cx: &mut RenderContext<Self>,
     ) -> Vec<ElementBox> {
-        let active_call = ActiveCall::global(cx);
-        if let Some(room) = active_call.read(cx).room().cloned() {
-            let project = workspace.read(cx).project().read(cx);
-            let mut participants = room
-                .read(cx)
-                .remote_participants()
-                .values()
-                .cloned()
-                .collect::<Vec<_>>();
-            participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
-            participants
-                .into_iter()
-                .filter_map(|participant| {
-                    let project = workspace.read(cx).project().read(cx);
-                    let replica_id = project
-                        .collaborators()
-                        .get(&participant.peer_id)
-                        .map(|collaborator| collaborator.replica_id);
-                    let user = participant.user.clone();
-                    Some(self.render_avatar(
+        let mut participants = room
+            .read(cx)
+            .remote_participants()
+            .values()
+            .cloned()
+            .collect::<Vec<_>>();
+        participants.sort_by_cached_key(|p| p.user.github_login.clone());
+
+        participants
+            .into_iter()
+            .filter_map(|participant| {
+                let project = workspace.read(cx).project().read(cx);
+                let replica_id = project
+                    .collaborators()
+                    .get(&participant.peer_id)
+                    .map(|collaborator| collaborator.replica_id);
+                let user = participant.user.clone();
+                Some(
+                    Container::new(self.render_face_pile(
                         &user,
                         replica_id,
-                        Some((
-                            participant.peer_id,
-                            &user.github_login,
-                            participant.location,
-                        )),
+                        participant.peer_id,
+                        Some(participant.location),
                         workspace,
                         theme,
                         cx,
                     ))
-                })
-                .collect()
-        } else {
-            Default::default()
-        }
+                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
+                    .boxed(),
+                )
+            })
+            .collect()
     }
 
     fn render_current_user(
         &self,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
+        user: &Arc<User>,
+        peer_id: PeerId,
         cx: &mut RenderContext<Self>,
-    ) -> Option<ElementBox> {
-        let user = workspace.read(cx).user_store().read(cx).current_user();
+    ) -> ElementBox {
         let replica_id = workspace.read(cx).project().read(cx).replica_id();
-        let status = *workspace.read(cx).client().status().borrow();
-        if let Some(user) = user {
-            Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
-        } else if matches!(status, client::Status::UpgradeRequired) {
-            None
-        } else {
-            Some(
-                MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
-                    let style = theme
-                        .workspace
-                        .titlebar
-                        .sign_in_prompt
-                        .style_for(state, false);
-                    Label::new("Sign in".to_string(), style.text.clone())
-                        .contained()
-                        .with_style(style.container)
-                        .boxed()
-                })
-                .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
-                .with_cursor_style(CursorStyle::PointingHand)
-                .aligned()
-                .boxed(),
-            )
-        }
+        Container::new(self.render_face_pile(
+            user,
+            Some(replica_id),
+            peer_id,
+            None,
+            workspace,
+            theme,
+            cx,
+        ))
+        .with_margin_right(theme.workspace.titlebar.item_spacing)
+        .boxed()
     }
 
-    fn render_avatar(
+    fn render_face_pile(
         &self,
         user: &User,
         replica_id: Option<ReplicaId>,
-        peer: Option<(PeerId, &str, ParticipantLocation)>,
+        peer_id: PeerId,
+        location: Option<ParticipantLocation>,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
-        let is_followed = peer.map_or(false, |(peer_id, _, _)| {
-            workspace.read(cx).is_following(peer_id)
-        });
+        let project_id = workspace.read(cx).project().read(cx).remote_id();
+        let room = ActiveCall::global(cx).read(cx).room();
+        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
+        let followed_by_self = room
+            .and_then(|room| {
+                Some(
+                    is_being_followed
+                        && room
+                            .read(cx)
+                            .followers_for(peer_id, project_id?)
+                            .iter()
+                            .any(|&follower| {
+                                Some(follower) == workspace.read(cx).client().peer_id()
+                            }),
+                )
+            })
+            .unwrap_or(false);
 
-        let mut avatar_style;
-        if let Some((_, _, location)) = peer.as_ref() {
-            if let ParticipantLocation::SharedProject { project_id } = *location {
-                if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
-                    avatar_style = theme.workspace.titlebar.avatar;
-                } else {
-                    avatar_style = theme.workspace.titlebar.inactive_avatar;
-                }
-            } else {
-                avatar_style = theme.workspace.titlebar.inactive_avatar;
-            }
-        } else {
-            avatar_style = theme.workspace.titlebar.avatar;
-        }
+        let leader_style = theme.workspace.titlebar.leader_avatar;
+        let follower_style = theme.workspace.titlebar.follower_avatar;
 
-        let mut replica_color = None;
+        let mut background_color = theme
+            .workspace
+            .titlebar
+            .container
+            .background_color
+            .unwrap_or_default();
         if let Some(replica_id) = replica_id {
-            let color = theme.editor.replica_selection_style(replica_id).cursor;
-            replica_color = Some(color);
-            if is_followed {
-                avatar_style.border = Border::all(1.0, color);
+            if followed_by_self {
+                let selection = theme.editor.replica_selection_style(replica_id).selection;
+                background_color = Color::blend(selection, background_color);
+                background_color.a = 255;
             }
         }
 
-        let content = Stack::new()
+        let mut content = Stack::new()
             .with_children(user.avatar.as_ref().map(|avatar| {
-                Image::new(avatar.clone())
-                    .with_style(avatar_style)
-                    .constrained()
-                    .with_width(theme.workspace.titlebar.avatar_width)
-                    .aligned()
-                    .boxed()
-            }))
-            .with_children(replica_color.map(|replica_color| {
-                AvatarRibbon::new(replica_color)
-                    .constrained()
-                    .with_width(theme.workspace.titlebar.avatar_ribbon.width)
-                    .with_height(theme.workspace.titlebar.avatar_ribbon.height)
-                    .aligned()
-                    .bottom()
-                    .boxed()
+                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
+                    .with_child(Self::render_face(
+                        avatar.clone(),
+                        Self::location_style(workspace, location, leader_style, cx),
+                        background_color,
+                    ))
+                    .with_children(
+                        (|| {
+                            let project_id = project_id?;
+                            let room = room?.read(cx);
+                            let followers = room.followers_for(peer_id, project_id);
+
+                            Some(followers.into_iter().flat_map(|&follower| {
+                                let remote_participant =
+                                    room.remote_participant_for_peer_id(follower);
+
+                                let avatar = remote_participant
+                                    .and_then(|p| p.user.avatar.clone())
+                                    .or_else(|| {
+                                        if follower == workspace.read(cx).client().peer_id()? {
+                                            workspace
+                                                .read(cx)
+                                                .user_store()
+                                                .read(cx)
+                                                .current_user()?
+                                                .avatar
+                                                .clone()
+                                        } else {
+                                            None
+                                        }
+                                    })?;
+
+                                let location = remote_participant.map(|p| p.location);
+
+                                Some(Self::render_face(
+                                    avatar.clone(),
+                                    Self::location_style(workspace, location, follower_style, cx),
+                                    background_color,
+                                ))
+                            }))
+                        })()
+                        .into_iter()
+                        .flatten(),
+                    );
+
+                let mut container = face_pile
+                    .contained()
+                    .with_style(theme.workspace.titlebar.leader_selection);
+
+                if let Some(replica_id) = replica_id {
+                    if followed_by_self {
+                        let color = theme.editor.replica_selection_style(replica_id).selection;
+                        container = container.with_background_color(color);
+                    }
+                }
+
+                container.boxed()
             }))
-            .constrained()
-            .with_width(theme.workspace.titlebar.avatar_width)
-            .contained()
-            .with_margin_left(theme.workspace.titlebar.avatar_margin)
+            .with_children((|| {
+                let replica_id = replica_id?;
+                let color = theme.editor.replica_selection_style(replica_id).cursor;
+                Some(
+                    AvatarRibbon::new(color)
+                        .constrained()
+                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
+                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
+                        .aligned()
+                        .bottom()
+                        .boxed(),
+                )
+            })())
             .boxed();
 
-        if let Some((peer_id, peer_github_login, location)) = peer {
+        if let Some(location) = location {
             if let Some(replica_id) = replica_id {
-                MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
+                content =
+                    MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
+                        content
+                    })
                     .with_cursor_style(CursorStyle::PointingHand)
                     .on_click(MouseButton::Left, move |_, cx| {
                         cx.dispatch_action(ToggleFollow(peer_id))
                     })
                     .with_tooltip::<ToggleFollow, _>(
                         peer_id.as_u64() as usize,
-                        if is_followed {
-                            format!("Unfollow {}", peer_github_login)
+                        if is_being_followed {
+                            format!("Unfollow {}", user.github_login)
                         } else {
-                            format!("Follow {}", peer_github_login)
+                            format!("Follow {}", user.github_login)
                         },
                         Some(Box::new(FollowNextCollaborator)),
                         theme.tooltip.clone(),
                         cx,
                     )
-                    .boxed()
+                    .boxed();
             } else if let ParticipantLocation::SharedProject { project_id } = location {
                 let user_id = user.id;
-                MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
-                    content
-                })
+                content = MouseEventHandler::<JoinProject>::new(
+                    peer_id.as_u64() as usize,
+                    cx,
+                    move |_, _| content,
+                )
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, cx| {
                     cx.dispatch_action(JoinProject {
@@ -485,29 +788,63 @@ impl CollabTitlebarItem {
                 })
                 .with_tooltip::<JoinProject, _>(
                     peer_id.as_u64() as usize,
-                    format!("Follow {} into external project", peer_github_login),
+                    format!("Follow {} into external project", user.github_login),
                     Some(Box::new(FollowNextCollaborator)),
                     theme.tooltip.clone(),
                     cx,
                 )
-                .boxed()
+                .boxed();
+            }
+        }
+        content
+    }
+
+    fn location_style(
+        workspace: &ViewHandle<Workspace>,
+        location: Option<ParticipantLocation>,
+        mut style: AvatarStyle,
+        cx: &RenderContext<Self>,
+    ) -> AvatarStyle {
+        if let Some(location) = location {
+            if let ParticipantLocation::SharedProject { project_id } = location {
+                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
+                    style.image.grayscale = true;
+                }
             } else {
-                content
+                style.image.grayscale = true;
             }
-        } else {
-            content
         }
+
+        style
+    }
+
+    fn render_face(
+        avatar: Arc<ImageData>,
+        avatar_style: AvatarStyle,
+        background_color: Color,
+    ) -> ElementBox {
+        Image::from_data(avatar)
+            .with_style(avatar_style.image)
+            .aligned()
+            .contained()
+            .with_background_color(background_color)
+            .with_corner_radius(avatar_style.outer_corner_radius)
+            .constrained()
+            .with_width(avatar_style.outer_width)
+            .with_height(avatar_style.outer_width)
+            .aligned()
+            .boxed()
     }
 
     fn render_connection_status(
         &self,
-        workspace: &ViewHandle<Workspace>,
+        status: &client::Status,
         cx: &mut RenderContext<Self>,
     ) -> Option<ElementBox> {
         enum ConnectionStatusButton {}
 
         let theme = &cx.global::<Settings>().theme.clone();
-        match &*workspace.read(cx).client().status().borrow() {
+        match status {
             client::Status::ConnectionError
             | client::Status::ConnectionLost
             | client::Status::Reauthenticating { .. }
@@ -531,7 +868,7 @@ impl CollabTitlebarItem {
             client::Status::UpgradeRequired => Some(
                 MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
                     Label::new(
-                        "Please update Zed to collaborate".to_string(),
+                        "Please update Zed to collaborate",
                         theme.workspace.titlebar.outdated_warning.text.clone(),
                     )
                     .contained()

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,8 +1,10 @@
 mod collab_titlebar_item;
+mod collaborator_list_popover;
 mod contact_finder;
 mod contact_list;
 mod contact_notification;
 mod contacts_popover;
+mod face_pile;
 mod incoming_call_notification;
 mod notifications;
 mod project_shared_notification;
@@ -10,7 +12,7 @@ mod sharing_status_indicator;
 
 use anyhow::anyhow;
 use call::ActiveCall;
-pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
+pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
 use gpui::{actions, MutableAppContext, Task};
 use std::sync::Arc;
 use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
@@ -84,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
                         0,
                         project,
                         app_state.dock_default_item_factory,
+                        app_state.background_actions,
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -116,7 +119,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
                     });
 
                 if let Some(follow_peer_id) = follow_peer_id {
-                    if !workspace.is_following(follow_peer_id) {
+                    if !workspace.is_being_followed(follow_peer_id) {
                         workspace
                             .toggle_follow(&ToggleFollow(follow_peer_id), cx)
                             .map(|follow| follow.detach_and_log_err(cx));

crates/collab_ui/src/collaborator_list_popover.rs 🔗

@@ -0,0 +1,165 @@
+use call::ActiveCall;
+use client::UserStore;
+use gpui::Action;
+use gpui::{
+    actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
+};
+use settings::Settings;
+
+use crate::collab_titlebar_item::ToggleCollaboratorList;
+
+pub(crate) enum Event {
+    Dismissed,
+}
+
+enum Collaborator {
+    SelfUser { username: String },
+    RemoteUser { username: String },
+}
+
+actions!(collaborator_list_popover, [NoOp]);
+
+pub(crate) struct CollaboratorListPopover {
+    list_state: ListState,
+}
+
+impl Entity for CollaboratorListPopover {
+    type Event = Event;
+}
+
+impl View for CollaboratorListPopover {
+    fn ui_name() -> &'static str {
+        "CollaboratorListPopover"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = cx.global::<Settings>().theme.clone();
+
+        MouseEventHandler::<Self>::new(0, cx, |_, _| {
+            List::new(self.list_state.clone())
+                .contained()
+                .with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
+                .constrained()
+                .with_width(theme.contacts_popover.width)
+                .with_height(theme.contacts_popover.height)
+                .boxed()
+        })
+        .on_down_out(MouseButton::Left, move |_, cx| {
+            cx.dispatch_action(ToggleCollaboratorList);
+        })
+        .boxed()
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
+}
+
+impl CollaboratorListPopover {
+    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let active_call = ActiveCall::global(cx);
+
+        let mut collaborators = user_store
+            .read(cx)
+            .current_user()
+            .map(|u| Collaborator::SelfUser {
+                username: u.github_login.clone(),
+            })
+            .into_iter()
+            .collect::<Vec<_>>();
+
+        //TODO: What should the canonical sort here look like, consult contacts list implementation
+        if let Some(room) = active_call.read(cx).room() {
+            for participant in room.read(cx).remote_participants() {
+                collaborators.push(Collaborator::RemoteUser {
+                    username: participant.1.user.github_login.clone(),
+                });
+            }
+        }
+
+        Self {
+            list_state: ListState::new(
+                collaborators.len(),
+                Orientation::Top,
+                0.,
+                cx,
+                move |_, index, cx| match &collaborators[index] {
+                    Collaborator::SelfUser { username } => render_collaborator_list_entry(
+                        index,
+                        username,
+                        None::<NoOp>,
+                        None,
+                        Svg::new("icons/chevron_right_12.svg"),
+                        NoOp,
+                        "Leave call".to_owned(),
+                        cx,
+                    ),
+
+                    Collaborator::RemoteUser { username } => render_collaborator_list_entry(
+                        index,
+                        username,
+                        Some(NoOp),
+                        Some(format!("Follow {username}")),
+                        Svg::new("icons/x_mark_12.svg"),
+                        NoOp,
+                        format!("Remove {username} from call"),
+                        cx,
+                    ),
+                },
+            ),
+        }
+    }
+}
+
+fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
+    index: usize,
+    username: &str,
+    username_action: Option<UA>,
+    username_tooltip: Option<String>,
+    icon: Svg,
+    icon_action: IA,
+    icon_tooltip: String,
+    cx: &mut RenderContext<CollaboratorListPopover>,
+) -> ElementBox {
+    enum Username {}
+    enum UsernameTooltip {}
+    enum Icon {}
+    enum IconTooltip {}
+
+    let theme = &cx.global::<Settings>().theme;
+    let username_theme = theme.contact_list.contact_username.text.clone();
+    let tooltip_theme = theme.tooltip.clone();
+
+    let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
+        Label::new(username.to_owned(), username_theme.clone()).boxed()
+    })
+    .on_click(MouseButton::Left, move |_, cx| {
+        if let Some(username_action) = username_action.clone() {
+            cx.dispatch_action(username_action);
+        }
+    });
+
+    Flex::row()
+        .with_child(if let Some(username_tooltip) = username_tooltip {
+            username
+                .with_tooltip::<UsernameTooltip, _>(
+                    index,
+                    username_tooltip,
+                    None,
+                    tooltip_theme.clone(),
+                    cx,
+                )
+                .boxed()
+        } else {
+            username.boxed()
+        })
+        .with_child(
+            MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
+                .on_click(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(icon_action.clone())
+                })
+                .with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
+                .boxed(),
+        )
+        .boxed()
+}

crates/collab_ui/src/contact_finder.rs 🔗

@@ -1,7 +1,7 @@
 use client::{ContactRequestStatus, User, UserStore};
 use gpui::{
-    elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
-    Task, View, ViewContext, ViewHandle,
+    elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
+    RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use picker::{Picker, PickerDelegate};
 use settings::Settings;
@@ -33,7 +33,7 @@ impl View for ContactFinder {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -68,7 +68,7 @@ impl PickerDelegate for ContactFinder {
                     this.potential_contacts = potential_contacts.into();
                     cx.notify();
                 });
-                Ok(())
+                anyhow::Ok(())
             }
             .log_err()
             .await;
@@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
             .style_for(mouse_state, selected);
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.contact_finder.contact_avatar)
                     .aligned()
                     .left()
@@ -178,4 +178,14 @@ impl ContactFinder {
             selected_index: 0,
         }
     }
+
+    pub fn editor_text(&self, cx: &AppContext) -> String {
+        self.picker.read(cx).query(cx)
+    }
+
+    pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
+        self.picker
+            .update(cx, |picker, cx| picker.set_query(editor_text, cx));
+        self
+    }
 }

crates/collab_ui/src/contact_list.rs 🔗

@@ -1,3 +1,4 @@
+use super::collab_titlebar_item::LeaveCall;
 use crate::contacts_popover;
 use call::ActiveCall;
 use client::{proto::PeerId, Contact, User, UserStore};
@@ -18,22 +19,20 @@ use serde::Deserialize;
 use settings::Settings;
 use std::{mem, sync::Arc};
 use theme::IconButton;
-use util::ResultExt;
 use workspace::{JoinProject, OpenSharedScreen};
 
 impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
-impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
+impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ContactList::remove_contact);
     cx.add_action(ContactList::respond_to_contact_request);
-    cx.add_action(ContactList::clear_filter);
+    cx.add_action(ContactList::cancel);
     cx.add_action(ContactList::select_next);
     cx.add_action(ContactList::select_prev);
     cx.add_action(ContactList::confirm);
     cx.add_action(ContactList::toggle_expanded);
     cx.add_action(ContactList::call);
-    cx.add_action(ContactList::leave_call);
 }
 
 #[derive(Clone, PartialEq)]
@@ -45,9 +44,6 @@ struct Call {
     initial_project: Option<ModelHandle<Project>>,
 }
 
-#[derive(Copy, Clone, PartialEq)]
-struct LeaveCall;
-
 #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 enum Section {
     ActiveCall,
@@ -145,7 +141,10 @@ impl PartialEq for ContactEntry {
 pub struct RequestContact(pub u64);
 
 #[derive(Clone, Deserialize, PartialEq)]
-pub struct RemoveContact(pub u64);
+pub struct RemoveContact {
+    user_id: u64,
+    github_login: String,
+}
 
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct RespondToContactRequest {
@@ -298,17 +297,39 @@ impl ContactList {
         this
     }
 
+    pub fn editor_text(&self, cx: &AppContext) -> String {
+        self.filter_editor.read(cx).text(cx)
+    }
+
+    pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
+        self.filter_editor
+            .update(cx, |picker, cx| picker.set_text(editor_text, cx));
+        self
+    }
+
     fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
-        let user_id = request.0;
+        let user_id = request.user_id;
+        let github_login = &request.github_login;
         let user_store = self.user_store.clone();
-        let prompt_message = "Are you sure you want to remove this contact?";
-        let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]);
+        let prompt_message = format!(
+            "Are you sure you want to remove \"{}\" from your contacts?",
+            github_login
+        );
+        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+        let window_id = cx.window_id();
         cx.spawn(|_, mut cx| async move {
             if answer.next().await == Some(0) {
-                user_store
+                if let Err(e) = user_store
                     .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
                     .await
-                    .unwrap();
+                {
+                    cx.prompt(
+                        window_id,
+                        PromptLevel::Info,
+                        &format!("Failed to remove contact: {}", e),
+                        &["Ok"],
+                    );
+                }
             }
         })
         .detach();
@@ -326,7 +347,7 @@ impl ContactList {
             .detach();
     }
 
-    fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         let did_clear = self.filter_editor.update(cx, |editor, cx| {
             if editor.buffer().read(cx).len(cx) > 0 {
                 editor.set_text("", cx);
@@ -335,6 +356,7 @@ impl ContactList {
                 false
             }
         });
+
         if !did_clear {
             cx.emit(Event::Dismissed);
         }
@@ -729,7 +751,7 @@ impl ContactList {
     ) -> ElementBox {
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.contact_avatar)
                     .aligned()
                     .left()
@@ -749,7 +771,7 @@ impl ContactList {
             )
             .with_children(if is_pending {
                 Some(
-                    Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
+                    Label::new("Calling", theme.calling_indicator.text.clone())
                         .contained()
                         .with_style(theme.calling_indicator.container)
                         .aligned()
@@ -950,7 +972,7 @@ impl ContactList {
                             .boxed(),
                     )
                     .with_child(
-                        Label::new("Screen".into(), row.name.text.clone())
+                        Label::new("Screen", row.name.text.clone())
                             .aligned()
                             .left()
                             .contained()
@@ -980,6 +1002,7 @@ impl ContactList {
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         enum Header {}
+        enum LeaveCallContactList {}
 
         let header_style = theme
             .header_row
@@ -992,9 +1015,9 @@ impl ContactList {
         };
         let leave_call = if section == Section::ActiveCall {
             Some(
-                MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
+                MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
                     let style = theme.leave_call.style_for(state, false);
-                    Label::new("Leave Session".into(), style.text.clone())
+                    Label::new("Leave Call", style.text.clone())
                         .contained()
                         .with_style(style.container)
                         .boxed()
@@ -1026,7 +1049,7 @@ impl ContactList {
                     .boxed(),
                 )
                 .with_child(
-                    Label::new(text.to_string(), header_style.text.clone())
+                    Label::new(text, header_style.text.clone())
                         .aligned()
                         .left()
                         .contained()
@@ -1059,6 +1082,7 @@ impl ContactList {
         let online = contact.online;
         let busy = contact.busy || calling;
         let user_id = contact.user.id;
+        let github_login = contact.user.github_login.clone();
         let initial_project = project.clone();
         let mut element =
             MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
@@ -1082,7 +1106,7 @@ impl ContactList {
                         };
                         Stack::new()
                             .with_child(
-                                Image::new(avatar)
+                                Image::from_data(avatar)
                                     .with_style(theme.contact_avatar)
                                     .aligned()
                                     .left()
@@ -1119,14 +1143,17 @@ impl ContactList {
                         .with_padding(Padding::uniform(2.))
                         .with_cursor_style(CursorStyle::PointingHand)
                         .on_click(MouseButton::Left, move |_, cx| {
-                            cx.dispatch_action(RemoveContact(user_id))
+                            cx.dispatch_action(RemoveContact {
+                                user_id,
+                                github_login: github_login.clone(),
+                            })
                         })
                         .flex_float()
                         .boxed(),
                     )
                     .with_children(if calling {
                         Some(
-                            Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
+                            Label::new("Calling", theme.calling_indicator.text.clone())
                                 .contained()
                                 .with_style(theme.calling_indicator.container)
                                 .aligned()
@@ -1175,7 +1202,7 @@ impl ContactList {
 
         let mut row = Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.contact_avatar)
                     .aligned()
                     .left()
@@ -1195,6 +1222,7 @@ impl ContactList {
             );
 
         let user_id = user.id;
+        let github_login = user.github_login.clone();
         let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
         let button_spacing = theme.contact_button_spacing;
 
@@ -1256,7 +1284,10 @@ impl ContactList {
                 .with_padding(Padding::uniform(2.))
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, cx| {
-                    cx.dispatch_action(RemoveContact(user_id))
+                    cx.dispatch_action(RemoveContact {
+                        user_id,
+                        github_login: github_login.clone(),
+                    })
                 })
                 .flex_float()
                 .boxed(),
@@ -1283,12 +1314,6 @@ impl ContactList {
             })
             .detach_and_log_err(cx);
     }
-
-    fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.hang_up(cx))
-            .log_err();
-    }
 }
 
 impl Entity for ContactList {
@@ -1302,7 +1327,7 @@ impl View for ContactList {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 
@@ -1314,7 +1339,7 @@ impl View for ContactList {
             .with_child(
                 Flex::row()
                     .with_child(
-                        ChildView::new(self.filter_editor.clone(), cx)
+                        ChildView::new(&self.filter_editor, cx)
                             .contained()
                             .with_style(theme.contact_list.user_query_editor.container)
                             .flex(1., true)
@@ -1334,7 +1359,7 @@ impl View for ContactList {
                         })
                         .with_tooltip::<AddContact, _>(
                             0,
-                            "Add contact".into(),
+                            "Search for new contact".into(),
                             None,
                             theme.tooltip.clone(),
                             cx,

crates/collab_ui/src/contacts_popover.rs 🔗

@@ -1,8 +1,8 @@
-use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
+use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
 use client::UserStore;
 use gpui::{
-    actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
-    MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+    actions, elements::*, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, View,
+    ViewContext, ViewHandle,
 };
 use project::Project;
 use settings::Settings;
@@ -43,19 +43,23 @@ impl ContactsPopover {
             user_store,
             _subscription: None,
         };
-        this.show_contact_list(cx);
+        this.show_contact_list(String::new(), cx);
         this
     }
 
     fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
         match &self.child {
-            Child::ContactList(_) => self.show_contact_finder(cx),
-            Child::ContactFinder(_) => self.show_contact_list(cx),
+            Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
+            Child::ContactFinder(finder) => {
+                self.show_contact_list(finder.read(cx).editor_text(cx), cx)
+            }
         }
     }
 
-    fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
-        let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
+    fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
+        let child = cx.add_view(|cx| {
+            ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
+        });
         cx.focus(&child);
         self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
             crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
@@ -64,9 +68,11 @@ impl ContactsPopover {
         cx.notify();
     }
 
-    fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
-        let child =
-            cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
+    fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
+        let child = cx.add_view(|cx| {
+            ContactList::new(self.project.clone(), self.user_store.clone(), cx)
+                .with_editor_text(editor_text, cx)
+        });
         cx.focus(&child);
         self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
             crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
@@ -92,61 +98,9 @@ impl View for ContactsPopover {
             Child::ContactFinder(child) => ChildView::new(child, cx),
         };
 
-        MouseEventHandler::<ContactsPopover>::new(0, cx, |_, cx| {
+        MouseEventHandler::<ContactsPopover>::new(0, cx, |_, _| {
             Flex::column()
                 .with_child(child.flex(1., true).boxed())
-                .with_children(
-                    self.user_store
-                        .read(cx)
-                        .invite_info()
-                        .cloned()
-                        .and_then(|info| {
-                            enum InviteLink {}
-
-                            if info.count > 0 {
-                                Some(
-                                    MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
-                                        let style = theme
-                                            .contacts_popover
-                                            .invite_row
-                                            .style_for(state, false)
-                                            .clone();
-
-                                        let copied =
-                                            cx.read_from_clipboard().map_or(false, |item| {
-                                                item.text().as_str() == info.url.as_ref()
-                                            });
-
-                                        Label::new(
-                                            format!(
-                                                "{} invite link ({} left)",
-                                                if copied { "Copied" } else { "Copy" },
-                                                info.count
-                                            ),
-                                            style.label.clone(),
-                                        )
-                                        .aligned()
-                                        .left()
-                                        .constrained()
-                                        .with_height(theme.contacts_popover.invite_row_height)
-                                        .contained()
-                                        .with_style(style.container)
-                                        .boxed()
-                                    })
-                                    .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_click(MouseButton::Left, move |_, cx| {
-                                        cx.write_to_clipboard(ClipboardItem::new(
-                                            info.url.to_string(),
-                                        ));
-                                        cx.notify();
-                                    })
-                                    .boxed(),
-                                )
-                            } else {
-                                None
-                            }
-                        }),
-                )
                 .contained()
                 .with_style(theme.contacts_popover.container)
                 .constrained()
@@ -155,7 +109,7 @@ impl View for ContactsPopover {
                 .boxed()
         })
         .on_down_out(MouseButton::Left, move |_, cx| {
-            cx.dispatch_action(ToggleCollaborationMenu);
+            cx.dispatch_action(ToggleContactsMenu);
         })
         .boxed()
     }

crates/collab_ui/src/face_pile.rs 🔗

@@ -0,0 +1,101 @@
+use std::ops::Range;
+
+use gpui::{
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    json::ToJson,
+    serde_json::{self, json},
+    Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
+};
+
+pub(crate) struct FacePile {
+    overlap: f32,
+    faces: Vec<ElementBox>,
+}
+
+impl FacePile {
+    pub fn new(overlap: f32) -> FacePile {
+        FacePile {
+            overlap,
+            faces: Vec::new(),
+        }
+    }
+}
+
+impl Element for FacePile {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        cx: &mut gpui::LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
+
+        let mut width = 0.;
+        for face in &mut self.faces {
+            width += face.layout(constraint, cx).x();
+        }
+        width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
+
+        (Vector2F::new(width, constraint.max.y()), ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        _layout: &mut Self::LayoutState,
+        cx: &mut PaintContext,
+    ) -> Self::PaintState {
+        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+        let origin_y = bounds.upper_right().y();
+        let mut origin_x = bounds.upper_right().x();
+
+        for face in self.faces.iter_mut().rev() {
+            let size = face.size();
+            origin_x -= size.x();
+            cx.paint_layer(None, |cx| {
+                face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
+            });
+            origin_x += self.overlap;
+        }
+
+        ()
+    }
+
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
+    fn debug(
+        &self,
+        bounds: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "FacePile",
+            "bounds": bounds.to_json()
+        })
+    }
+}
+
+impl Extend<ElementBox> for FacePile {
+    fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
+        self.faces.extend(children);
+    }
+}

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -108,7 +108,7 @@ impl IncomingCallNotification {
             .unwrap_or(&default_project);
         Flex::row()
             .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.caller_avatar)
                     .aligned()
                     .boxed()
@@ -172,7 +172,7 @@ impl IncomingCallNotification {
             .with_child(
                 MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
                     let theme = &cx.global::<Settings>().theme.incoming_call_notification;
-                    Label::new("Accept".to_string(), theme.accept_button.text.clone())
+                    Label::new("Accept", theme.accept_button.text.clone())
                         .aligned()
                         .contained()
                         .with_style(theme.accept_button.container)
@@ -188,7 +188,7 @@ impl IncomingCallNotification {
             .with_child(
                 MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
                     let theme = &cx.global::<Settings>().theme.incoming_call_notification;
-                    Label::new("Decline".to_string(), theme.decline_button.text.clone())
+                    Label::new("Decline", theme.decline_button.text.clone())
                         .aligned()
                         .contained()
                         .with_style(theme.decline_button.container)

crates/collab_ui/src/notifications.rs 🔗

@@ -11,8 +11,8 @@ enum Button {}
 
 pub fn render_user_notification<V: View, A: Action + Clone>(
     user: Arc<User>,
-    title: &str,
-    body: Option<&str>,
+    title: &'static str,
+    body: Option<&'static str>,
     dismiss_action: A,
     buttons: Vec<(&'static str, Box<dyn Action>)>,
     cx: &mut RenderContext<V>,
@@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
         .with_child(
             Flex::row()
                 .with_children(user.avatar.clone().map(|avatar| {
-                    Image::new(avatar)
+                    Image::from_data(avatar)
                         .with_style(theme.header_avatar)
                         .aligned()
                         .constrained()
@@ -83,7 +83,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                 .named("contact notification header"),
         )
         .with_children(body.map(|body| {
-            Label::new(body.to_string(), theme.body_message.text.clone())
+            Label::new(body, theme.body_message.text.clone())
                 .contained()
                 .with_style(theme.body_message.container)
                 .boxed()
@@ -97,7 +97,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                         |(ix, (message, action))| {
                             MouseEventHandler::<Button>::new(ix, cx, |state, _| {
                                 let button = theme.button.style_for(state, false);
-                                Label::new(message.to_string(), button.text.clone())
+                                Label::new(message, button.text.clone())
                                     .contained()
                                     .with_style(button.container)
                                     .boxed()

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -108,7 +108,7 @@ impl ProjectSharedNotification {
         let theme = &cx.global::<Settings>().theme.project_shared_notification;
         Flex::row()
             .with_children(self.owner.avatar.clone().map(|avatar| {
-                Image::new(avatar)
+                Image::from_data(avatar)
                     .with_style(theme.owner_avatar)
                     .aligned()
                     .boxed()
@@ -175,7 +175,7 @@ impl ProjectSharedNotification {
             .with_child(
                 MouseEventHandler::<Open>::new(0, cx, |_, cx| {
                     let theme = &cx.global::<Settings>().theme.project_shared_notification;
-                    Label::new("Open".to_string(), theme.open_button.text.clone())
+                    Label::new("Open", theme.open_button.text.clone())
                         .aligned()
                         .contained()
                         .with_style(theme.open_button.container)
@@ -194,7 +194,7 @@ impl ProjectSharedNotification {
             .with_child(
                 MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
                     let theme = &cx.global::<Settings>().theme.project_shared_notification;
-                    Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
+                    Label::new("Dismiss", theme.dismiss_button.text.clone())
                         .aligned()
                         .contained()
                         .with_style(theme.dismiss_button.container)

crates/collab_ui/src/sharing_status_indicator.rs 🔗

@@ -21,6 +21,8 @@ pub fn init(cx: &mut MutableAppContext) {
             } else if let Some((window_id, _)) = status_indicator.take() {
                 cx.remove_status_bar_item(window_id);
             }
+        } else if let Some((window_id, _)) = status_indicator.take() {
+            cx.remove_status_bar_item(window_id);
         }
     })
     .detach();

crates/collections/src/collections.rs 🔗

@@ -24,3 +24,10 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
 pub type HashSet<T> = std::collections::HashSet<T>;
 
 pub use std::collections::*;
+
+// NEW TYPES
+
+#[derive(Default)]
+pub struct CommandPaletteFilter {
+    pub filtered_namespaces: HashSet<&'static str>,
+}

crates/command_palette/Cargo.toml 🔗

@@ -24,7 +24,7 @@ workspace = { path = "../workspace" }
 gpui = { path = "../gpui", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.9"

crates/command_palette/src/command_palette.rs 🔗

@@ -1,4 +1,4 @@
-use collections::HashSet;
+use collections::CommandPaletteFilter;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
@@ -12,11 +12,6 @@ use settings::Settings;
 use std::cmp;
 use workspace::Workspace;
 
-#[derive(Default)]
-pub struct CommandPaletteFilter {
-    pub filtered_namespaces: HashSet<&'static str>,
-}
-
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(CommandPalette::toggle);
     Picker::<CommandPalette>::init(cx);
@@ -90,7 +85,7 @@ impl CommandPalette {
             .unwrap_or_else(|| workspace.id());
 
         cx.as_mut().defer(move |cx| {
-            let this = cx.add_view(workspace.clone(), |cx| Self::new(focused_view_id, cx));
+            let this = cx.add_view(&workspace, |cx| Self::new(focused_view_id, cx));
             workspace.update(cx, |workspace, cx| {
                 workspace.toggle_modal(cx, |_, cx| {
                     cx.subscribe(&this, Self::on_event).detach();
@@ -134,7 +129,7 @@ impl View for CommandPalette {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -257,7 +252,7 @@ impl PickerDelegate for CommandPalette {
                         .filter_map(|(modifier, label)| {
                             if modifier {
                                 Some(
-                                    Label::new(label.into(), key_style.label.clone())
+                                    Label::new(label, key_style.label.clone())
                                         .contained()
                                         .with_style(key_style.container)
                                         .boxed(),
@@ -352,9 +347,7 @@ mod tests {
         });
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let editor = cx.add_view(&workspace, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);
@@ -362,7 +355,7 @@ mod tests {
         });
 
         workspace.update(cx, |workspace, cx| {
-            cx.focus(editor.clone());
+            cx.focus(&editor);
             workspace.add_item(Box::new(editor.clone()), cx)
         });
 

crates/context_menu/src/context_menu.rs 🔗

@@ -1,11 +1,13 @@
 use gpui::{
     elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
     platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
-    MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
+    MouseState, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
 };
 use menu::*;
 use settings::Settings;
-use std::{any::TypeId, time::Duration};
+use std::{any::TypeId, borrow::Cow, time::Duration};
+
+pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
 
 #[derive(Copy, Clone, PartialEq)]
 struct Clicked;
@@ -22,19 +24,71 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ContextMenu::cancel);
 }
 
+type ContextMenuItemBuilder = Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> ElementBox>;
+
+pub enum ContextMenuItemLabel {
+    String(Cow<'static, str>),
+    Element(ContextMenuItemBuilder),
+}
+
+pub enum ContextMenuAction {
+    ParentAction {
+        action: Box<dyn Action>,
+    },
+    ViewAction {
+        action: Box<dyn Action>,
+        for_view: usize,
+    },
+}
+
+impl ContextMenuAction {
+    fn id(&self) -> TypeId {
+        match self {
+            ContextMenuAction::ParentAction { action } => action.id(),
+            ContextMenuAction::ViewAction { action, .. } => action.id(),
+        }
+    }
+}
+
 pub enum ContextMenuItem {
     Item {
-        label: String,
-        action: Box<dyn Action>,
+        label: ContextMenuItemLabel,
+        action: ContextMenuAction,
     },
+    Static(StaticItem),
     Separator,
 }
 
 impl ContextMenuItem {
-    pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
+    pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
         Self::Item {
-            label: label.to_string(),
-            action: Box::new(action),
+            label: ContextMenuItemLabel::Element(label),
+            action: ContextMenuAction::ParentAction {
+                action: Box::new(action),
+            },
+        }
+    }
+
+    pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
+        Self::Item {
+            label: ContextMenuItemLabel::String(label.into()),
+            action: ContextMenuAction::ParentAction {
+                action: Box::new(action),
+            },
+        }
+    }
+
+    pub fn item_for_view(
+        label: impl Into<Cow<'static, str>>,
+        view_id: usize,
+        action: impl 'static + Action,
+    ) -> Self {
+        Self::Item {
+            label: ContextMenuItemLabel::String(label.into()),
+            action: ContextMenuAction::ViewAction {
+                action: Box::new(action),
+                for_view: view_id,
+            },
         }
     }
 
@@ -42,14 +96,14 @@ impl ContextMenuItem {
         Self::Separator
     }
 
-    fn is_separator(&self) -> bool {
-        matches!(self, Self::Separator)
+    fn is_action(&self) -> bool {
+        matches!(self, Self::Item { .. })
     }
 
     fn action_id(&self) -> Option<TypeId> {
         match self {
             ContextMenuItem::Item { action, .. } => Some(action.id()),
-            ContextMenuItem::Separator => None,
+            ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
         }
     }
 }
@@ -58,6 +112,7 @@ pub struct ContextMenu {
     show_count: usize,
     anchor_position: Vector2F,
     anchor_corner: AnchorCorner,
+    position_mode: OverlayPositionMode,
     items: Vec<ContextMenuItem>,
     selected_index: Option<usize>,
     visible: bool,
@@ -78,7 +133,7 @@ impl View for ContextMenu {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 
@@ -105,6 +160,7 @@ impl View for ContextMenu {
             .with_fit_mode(OverlayFitMode::SnapToWindow)
             .with_anchor_position(self.anchor_position)
             .with_anchor_corner(self.anchor_corner)
+            .with_position_mode(self.position_mode)
             .boxed()
     }
 
@@ -121,6 +177,7 @@ impl ContextMenu {
             show_count: 0,
             anchor_position: Default::default(),
             anchor_corner: AnchorCorner::TopLeft,
+            position_mode: OverlayPositionMode::Window,
             items: Default::default(),
             selected_index: Default::default(),
             visible: Default::default(),
@@ -162,7 +219,15 @@ impl ContextMenu {
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
-                cx.dispatch_any_action(action.boxed_clone());
+                match action {
+                    ContextMenuAction::ParentAction { action } => {
+                        cx.dispatch_any_action(action.boxed_clone())
+                    }
+                    ContextMenuAction::ViewAction { action, for_view } => {
+                        let window_id = cx.window_id();
+                        cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
+                    }
+                };
                 self.reset(cx);
             }
         }
@@ -188,13 +253,13 @@ impl ContextMenu {
     }
 
     fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        self.selected_index = self.items.iter().position(|item| !item.is_separator());
+        self.selected_index = self.items.iter().position(|item| item.is_action());
         cx.notify();
     }
 
     fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
         for (ix, item) in self.items.iter().enumerate().rev() {
-            if !item.is_separator() {
+            if item.is_action() {
                 self.selected_index = Some(ix);
                 cx.notify();
                 break;
@@ -205,7 +270,7 @@ impl ContextMenu {
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
-                if !item.is_separator() {
+                if item.is_action() {
                     self.selected_index = Some(ix);
                     cx.notify();
                     break;
@@ -219,7 +284,7 @@ impl ContextMenu {
     fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
-                if !item.is_separator() {
+                if item.is_action() {
                     self.selected_index = Some(ix);
                     cx.notify();
                     break;
@@ -234,7 +299,7 @@ impl ContextMenu {
         &mut self,
         anchor_position: Vector2F,
         anchor_corner: AnchorCorner,
-        items: impl IntoIterator<Item = ContextMenuItem>,
+        items: Vec<ContextMenuItem>,
         cx: &mut ViewContext<Self>,
     ) {
         let mut items = items.into_iter().peekable();
@@ -254,6 +319,10 @@ impl ContextMenu {
         cx.notify();
     }
 
+    pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
+        self.position_mode = mode;
+    }
+
     fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
         let window_id = cx.window_id();
         let style = cx.global::<Settings>().theme.context_menu.clone();
@@ -268,11 +337,21 @@ impl ContextMenu {
                                     Some(ix) == self.selected_index,
                                 );
 
-                                Label::new(label.to_string(), style.label.clone())
-                                    .contained()
-                                    .with_style(style.container)
-                                    .boxed()
+                                match label {
+                                    ContextMenuItemLabel::String(label) => {
+                                        Label::new(label.to_string(), style.label.clone())
+                                            .contained()
+                                            .with_style(style.container)
+                                            .boxed()
+                                    }
+                                    ContextMenuItemLabel::Element(element) => {
+                                        element(&mut Default::default(), style)
+                                    }
+                                }
                             }
+
+                            ContextMenuItem::Static(f) => f(cx),
+
                             ContextMenuItem::Separator => Empty::new()
                                 .collapsed()
                                 .contained()
@@ -293,15 +372,27 @@ impl ContextMenu {
                                     &mut Default::default(),
                                     Some(ix) == self.selected_index,
                                 );
+                                let (action, view_id) = match action {
+                                    ContextMenuAction::ParentAction { action } => {
+                                        (action.boxed_clone(), self.parent_view_id)
+                                    }
+                                    ContextMenuAction::ViewAction { action, for_view } => {
+                                        (action.boxed_clone(), *for_view)
+                                    }
+                                };
+
                                 KeystrokeLabel::new(
                                     window_id,
-                                    self.parent_view_id,
+                                    view_id,
                                     action.boxed_clone(),
                                     style.keystroke.container,
                                     style.keystroke.text.clone(),
                                 )
                                 .boxed()
                             }
+
+                            ContextMenuItem::Static(_) => Empty::new().boxed(),
+
                             ContextMenuItem::Separator => Empty::new()
                                 .collapsed()
                                 .constrained()
@@ -331,22 +422,34 @@ impl ContextMenu {
                 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                     match item {
                         ContextMenuItem::Item { label, action } => {
-                            let action = action.boxed_clone();
+                            let (action, view_id) = match action {
+                                ContextMenuAction::ParentAction { action } => {
+                                    (action.boxed_clone(), self.parent_view_id)
+                                }
+                                ContextMenuAction::ViewAction { action, for_view } => {
+                                    (action.boxed_clone(), *for_view)
+                                }
+                            };
 
                             MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
                                 let style =
                                     style.item.style_for(state, Some(ix) == self.selected_index);
 
                                 Flex::row()
-                                    .with_child(
-                                        Label::new(label.to_string(), style.label.clone())
-                                            .contained()
-                                            .boxed(),
-                                    )
+                                    .with_child(match label {
+                                        ContextMenuItemLabel::String(label) => {
+                                            Label::new(label.clone(), style.label.clone())
+                                                .contained()
+                                                .boxed()
+                                        }
+                                        ContextMenuItemLabel::Element(element) => {
+                                            element(state, style)
+                                        }
+                                    })
                                     .with_child({
                                         KeystrokeLabel::new(
                                             window_id,
-                                            self.parent_view_id,
+                                            view_id,
                                             action.boxed_clone(),
                                             style.keystroke.container,
                                             style.keystroke.text.clone(),
@@ -359,13 +462,19 @@ impl ContextMenu {
                                     .boxed()
                             })
                             .with_cursor_style(CursorStyle::PointingHand)
+                            .on_up(MouseButton::Left, |_, _| {}) // Capture these events
+                            .on_down(MouseButton::Left, |_, _| {}) // Capture these events
                             .on_click(MouseButton::Left, move |_, cx| {
                                 cx.dispatch_action(Clicked);
-                                cx.dispatch_any_action(action.boxed_clone());
+                                let window_id = cx.window_id();
+                                cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
                             })
                             .on_drag(MouseButton::Left, |_, _| {})
                             .boxed()
                         }
+
+                        ContextMenuItem::Static(f) => f(cx),
+
                         ContextMenuItem::Separator => Empty::new()
                             .constrained()
                             .with_height(1.)

crates/copilot/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "copilot"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+lsp = { path = "../lsp" }
+node_runtime = { path = "../node_runtime"}
+util = { path = "../util" }
+client = { path = "../client" }
+async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
+async-tar = "0.4.2"
+anyhow = "1.0"
+log = "0.4"
+serde = { workspace = true }
+serde_derive = { workspace = true }
+smol = "1.2.5"
+futures = "0.3"
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+client = { path = "../client", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/copilot/src/copilot.rs 🔗

@@ -0,0 +1,695 @@
+mod request;
+mod sign_in;
+
+use anyhow::{anyhow, Context, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use client::Client;
+use collections::HashMap;
+use futures::{future::Shared, Future, FutureExt, TryFutureExt};
+use gpui::{
+    actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
+    Task,
+};
+use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16};
+use log::{debug, error};
+use lsp::LanguageServer;
+use node_runtime::NodeRuntime;
+use request::{LogMessage, StatusNotification};
+use settings::Settings;
+use smol::{fs, io::BufReader, stream::StreamExt};
+use std::{
+    ffi::OsString,
+    ops::Range,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{
+    fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
+};
+
+const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
+actions!(copilot_auth, [SignIn, SignOut]);
+
+const COPILOT_NAMESPACE: &'static str = "copilot";
+actions!(
+    copilot,
+    [NextSuggestion, PreviousSuggestion, Toggle, Reinstall]
+);
+
+pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
+    let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
+    cx.set_global(copilot.clone());
+    cx.add_global_action(|_: &SignIn, cx| {
+        let copilot = Copilot::global(cx).unwrap();
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_in(cx))
+            .detach_and_log_err(cx);
+    });
+    cx.add_global_action(|_: &SignOut, cx| {
+        let copilot = Copilot::global(cx).unwrap();
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_out(cx))
+            .detach_and_log_err(cx);
+    });
+
+    cx.add_global_action(|_: &Reinstall, cx| {
+        let copilot = Copilot::global(cx).unwrap();
+        copilot
+            .update(cx, |copilot, cx| copilot.reinstall(cx))
+            .detach();
+    });
+
+    cx.observe(&copilot, |handle, cx| {
+        let status = handle.read(cx).status();
+        cx.update_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);
+}
+
+enum CopilotServer {
+    Disabled,
+    Starting {
+        task: Shared<Task<()>>,
+    },
+    Error(Arc<str>),
+    Started {
+        server: Arc<LanguageServer>,
+        status: SignInStatus,
+        subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>,
+    },
+}
+
+#[derive(Clone, Debug)]
+enum SignInStatus {
+    Authorized {
+        _user: String,
+    },
+    Unauthorized {
+        _user: String,
+    },
+    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)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Completion {
+    pub range: Range<Anchor>,
+    pub text: String,
+}
+
+pub struct Copilot {
+    http: Arc<dyn HttpClient>,
+    node_runtime: Arc<NodeRuntime>,
+    server: CopilotServer,
+}
+
+impl Entity for Copilot {
+    type Event = ();
+}
+
+impl Copilot {
+    pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
+        match self.server {
+            CopilotServer::Starting { ref task } => Some(task.clone()),
+            _ => None,
+        }
+    }
+
+    pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
+        if cx.has_global::<ModelHandle<Self>>() {
+            Some(cx.global::<ModelHandle<Self>>().clone())
+        } else {
+            None
+        }
+    }
+
+    fn start(
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<NodeRuntime>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        cx.observe_global::<Settings, _>({
+            let http = http.clone();
+            let node_runtime = node_runtime.clone();
+            move |this, cx| {
+                if cx.global::<Settings>().enable_copilot_integration {
+                    if matches!(this.server, CopilotServer::Disabled) {
+                        let start_task = cx
+                            .spawn({
+                                let http = http.clone();
+                                let node_runtime = node_runtime.clone();
+                                move |this, cx| {
+                                    Self::start_language_server(http, node_runtime, this, cx)
+                                }
+                            })
+                            .shared();
+                        this.server = CopilotServer::Starting { task: start_task };
+                        cx.notify();
+                    }
+                } else {
+                    this.server = CopilotServer::Disabled;
+                    cx.notify();
+                }
+            }
+        })
+        .detach();
+
+        if cx.global::<Settings>().enable_copilot_integration {
+            let start_task = cx
+                .spawn({
+                    let http = http.clone();
+                    let node_runtime = node_runtime.clone();
+                    move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
+                })
+                .shared();
+
+            Self {
+                http,
+                node_runtime,
+                server: CopilotServer::Starting { task: start_task },
+            }
+        } else {
+            Self {
+                http,
+                node_runtime,
+                server: CopilotServer::Disabled,
+            }
+        }
+    }
+
+    fn start_language_server(
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<NodeRuntime>,
+        this: ModelHandle<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: &[OsString] = &[server_path.into(), "--stdio".into()];
+                let server = LanguageServer::new(
+                    0,
+                    &node_path,
+                    arguments,
+                    Path::new("/"),
+                    None,
+                    cx.clone(),
+                )?;
+
+                let server = server.initialize(Default::default()).await?;
+                let status = server
+                    .request::<request::CheckStatus>(request::CheckStatusParams {
+                        local_checks_only: false,
+                    })
+                    .await?;
+
+                server
+                    .on_notification::<LogMessage, _>(|params, _cx| {
+                        match params.level {
+                            // Copilot is pretty agressive about logging
+                            0 => debug!("copilot: {}", params.message),
+                            1 => debug!("copilot: {}", params.message),
+                            _ => error!("copilot: {}", params.message),
+                        }
+
+                        debug!("copilot metadata: {}", params.metadata_str);
+                        debug!("copilot extra: {:?}", params.extra);
+                    })
+                    .detach();
+
+                server
+                    .on_notification::<StatusNotification, _>(
+                        |_, _| { /* Silence the notification */ },
+                    )
+                    .detach();
+
+                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::Started {
+                            server,
+                            status: SignInStatus::SignedOut,
+                            subscriptions_by_buffer_id: Default::default(),
+                        };
+                        this.update_sign_in_status(status, cx);
+                    }
+                    Err(error) => {
+                        this.server = CopilotServer::Error(error.to_string().into());
+                        cx.notify()
+                    }
+                }
+            })
+        }
+    }
+
+    fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        if let CopilotServer::Started { server, status, .. } = &mut self.server {
+            let task = match status {
+                SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
+                    Task::ready(Ok(())).shared()
+                }
+                SignInStatus::SigningIn { task, .. } => {
+                    cx.notify();
+                    task.clone()
+                }
+                SignInStatus::SignedOut => {
+                    let server = server.clone();
+                    let task = cx
+                        .spawn(|this, mut cx| async move {
+                            let sign_in = async {
+                                let sign_in = server
+                                    .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::Started { status, .. } =
+                                                &mut this.server
+                                            {
+                                                if let SignInStatus::SigningIn {
+                                                    prompt: prompt_flow,
+                                                    ..
+                                                } = status
+                                                {
+                                                    *prompt_flow = Some(flow.clone());
+                                                    cx.notify();
+                                                }
+                                            }
+                                        });
+                                        let response = server
+                                            .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();
+                    *status = SignInStatus::SigningIn {
+                        prompt: None,
+                        task: task.clone(),
+                    };
+                    cx.notify();
+                    task
+                }
+            };
+
+            cx.foreground()
+                .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")))
+        }
+    }
+
+    fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        if let CopilotServer::Started { server, status, .. } = &mut self.server {
+            *status = SignInStatus::SignedOut;
+            cx.notify();
+
+            let server = server.clone();
+            cx.background().spawn(async move {
+                server
+                    .request::<request::SignOut>(request::SignOutParams {})
+                    .await?;
+                anyhow::Ok(())
+            })
+        } else {
+            Task::ready(Err(anyhow!("copilot hasn't started yet")))
+        }
+    }
+
+    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();
+                move |this, cx| async move {
+                    clear_copilot_dir().await;
+                    Self::start_language_server(http, node_runtime, this, cx).await
+                }
+            })
+            .shared();
+
+        self.server = CopilotServer::Starting {
+            task: start_task.clone(),
+        };
+
+        cx.notify();
+
+        cx.foreground().spawn(start_task)
+    }
+
+    pub fn completions<T>(
+        &mut self,
+        buffer: &ModelHandle<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: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        T: ToPointUtf16,
+    {
+        self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
+    }
+
+    fn request_completions<R, T>(
+        &mut self,
+        buffer: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    where
+        R: lsp::request::Request<
+            Params = request::GetCompletionsParams,
+            Result = request::GetCompletionsResult,
+        >,
+        T: ToPointUtf16,
+    {
+        let buffer_id = buffer.id();
+        let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
+        let snapshot = buffer.read(cx).snapshot();
+        let server = match &mut self.server {
+            CopilotServer::Starting { .. } => {
+                return Task::ready(Err(anyhow!("copilot is still starting")))
+            }
+            CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
+            CopilotServer::Error(error) => {
+                return Task::ready(Err(anyhow!(
+                    "copilot was not started because of an error: {}",
+                    error
+                )))
+            }
+            CopilotServer::Started {
+                server,
+                status,
+                subscriptions_by_buffer_id,
+            } => {
+                if matches!(status, SignInStatus::Authorized { .. }) {
+                    subscriptions_by_buffer_id
+                        .entry(buffer_id)
+                        .or_insert_with(|| {
+                            server
+                                .notify::<lsp::notification::DidOpenTextDocument>(
+                                    lsp::DidOpenTextDocumentParams {
+                                        text_document: lsp::TextDocumentItem {
+                                            uri: uri.clone(),
+                                            language_id: id_for_language(
+                                                buffer.read(cx).language(),
+                                            ),
+                                            version: 0,
+                                            text: snapshot.text(),
+                                        },
+                                    },
+                                )
+                                .log_err();
+
+                            let uri = uri.clone();
+                            cx.observe_release(buffer, move |this, _, _| {
+                                if let CopilotServer::Started {
+                                    server,
+                                    subscriptions_by_buffer_id,
+                                    ..
+                                } = &mut this.server
+                                {
+                                    server
+                                        .notify::<lsp::notification::DidCloseTextDocument>(
+                                            lsp::DidCloseTextDocumentParams {
+                                                text_document: lsp::TextDocumentIdentifier::new(
+                                                    uri.clone(),
+                                                ),
+                                            },
+                                        )
+                                        .log_err();
+                                    subscriptions_by_buffer_id.remove(&buffer_id);
+                                }
+                            })
+                        });
+
+                    server.clone()
+                } else {
+                    return Task::ready(Err(anyhow!("must sign in before using copilot")));
+                }
+            }
+        };
+
+        let settings = cx.global::<Settings>();
+        let position = position.to_point_utf16(&snapshot);
+        let language = snapshot.language_at(position);
+        let language_name = language.map(|language| language.name());
+        let language_name = language_name.as_deref();
+        let tab_size = settings.tab_size(language_name);
+        let hard_tabs = settings.hard_tabs(language_name);
+        let language_id = id_for_language(language);
+
+        let path;
+        let relative_path;
+        if let Some(file) = snapshot.file() {
+            if let Some(file) = file.as_local() {
+                path = file.abs_path(cx);
+            } else {
+                path = file.full_path(cx);
+            }
+            relative_path = file.path().to_path_buf();
+        } else {
+            path = PathBuf::new();
+            relative_path = PathBuf::new();
+        }
+
+        cx.background().spawn(async move {
+            let result = server
+                .request::<R>(request::GetCompletionsParams {
+                    doc: request::GetCompletionsDocument {
+                        source: snapshot.text(),
+                        tab_size: tab_size.into(),
+                        indent_size: 1,
+                        insert_spaces: !hard_tabs,
+                        uri,
+                        path: path.to_string_lossy().into(),
+                        relative_path: relative_path.to_string_lossy().into(),
+                        language_id,
+                        position: point_to_lsp(position),
+                        version: 0,
+                    },
+                })
+                .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 {
+                        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::Started { status, .. } => match 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>,
+    ) {
+        if let CopilotServer::Started { status, .. } = &mut self.server {
+            *status = match lsp_status {
+                request::SignInStatus::Ok { user }
+                | request::SignInStatus::MaybeOk { user }
+                | request::SignInStatus::AlreadySignedIn { user } => {
+                    SignInStatus::Authorized { _user: user }
+                }
+                request::SignInStatus::NotAuthorized { user } => {
+                    SignInStatus::Unauthorized { _user: user }
+                }
+                request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
+            };
+            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(),
+    }
+}
+
+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", 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
+        }
+    }
+}

crates/copilot/src/request.rs 🔗

@@ -0,0 +1,171 @@
+use serde::{Deserialize, Serialize};
+
+pub enum CheckStatus {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CheckStatusParams {
+    pub local_checks_only: bool,
+}
+
+impl lsp::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 lsp::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 lsp::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 lsp::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 source: String,
+    pub tab_size: u32,
+    pub indent_size: u32,
+    pub insert_spaces: bool,
+    pub uri: lsp::Url,
+    pub path: String,
+    pub relative_path: String,
+    pub language_id: String,
+    pub position: lsp::Position,
+    pub version: usize,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GetCompletionsResult {
+    pub completions: Vec<Completion>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Completion {
+    pub text: String,
+    pub position: lsp::Position,
+    pub uuid: String,
+    pub range: lsp::Range,
+    pub display_text: String,
+}
+
+impl lsp::request::Request for GetCompletions {
+    type Params = GetCompletionsParams;
+    type Result = GetCompletionsResult;
+    const METHOD: &'static str = "getCompletions";
+}
+
+pub enum GetCompletionsCycling {}
+
+impl lsp::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 message: String,
+    pub level: u8,
+    pub metadata_str: String,
+    pub extra: Vec<String>,
+}
+
+impl lsp::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 lsp::notification::Notification for StatusNotification {
+    type Params = StatusNotificationParams;
+    const METHOD: &'static str = "statusNotification";
+}

crates/copilot/src/sign_in.rs 🔗

@@ -0,0 +1,344 @@
+use crate::{request::PromptUserDeviceFlow, Copilot, Status};
+use gpui::{
+    elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View,
+    ViewContext, ViewHandle, WindowKind, WindowOptions,
+};
+use settings::Settings;
+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 MutableAppContext) {
+    let copilot = Copilot::global(cx).unwrap();
+
+    let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
+    cx.observe(&copilot, move |copilot, cx| {
+        let status = copilot.read(cx).status();
+
+        match &status {
+            crate::Status::SigningIn { prompt } => {
+                if let Some(code_verification) = code_verification.as_ref() {
+                    code_verification.update(cx, |code_verification, cx| {
+                        code_verification.set_status(status, cx)
+                    });
+                    cx.activate_window(code_verification.window_id());
+                } else if let Some(_prompt) = prompt {
+                    let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+                    let window_options = WindowOptions {
+                        bounds: gpui::WindowBounds::Fixed(RectF::new(
+                            Default::default(),
+                            window_size,
+                        )),
+                        titlebar: None,
+                        center: true,
+                        focus: true,
+                        kind: WindowKind::Normal,
+                        is_movable: true,
+                        screen: None,
+                    };
+                    let (_, view) =
+                        cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
+                    code_verification = Some(view);
+                }
+            }
+            Status::Authorized | Status::Unauthorized => {
+                if let Some(code_verification) = code_verification.as_ref() {
+                    code_verification.update(cx, |code_verification, cx| {
+                        code_verification.set_status(status, cx)
+                    });
+
+                    cx.platform().activate(true);
+                    cx.activate_window(code_verification.window_id());
+                }
+            }
+            _ => {
+                if let Some(code_verification) = code_verification.take() {
+                    cx.remove_window(code_verification.window_id());
+                }
+            }
+        }
+    })
+    .detach();
+}
+
+pub struct CopilotCodeVerification {
+    status: Status,
+}
+
+impl CopilotCodeVerification {
+    pub fn new(status: Status) -> Self {
+        Self { status }
+    }
+
+    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 gpui::RenderContext<Self>,
+    ) -> ElementBox {
+        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::<Self>::new(0, cx, |state, _cx| {
+            Flex::row()
+                .with_children([
+                    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)
+                        .boxed(),
+                    Label::new(
+                        if copied { "Copied!" } else { "Copy" },
+                        device_code_style.cta.style_for(state, false).text.clone(),
+                    )
+                    .aligned()
+                    .contained()
+                    .with_style(*device_code_style.right_container.style_for(state, false))
+                    .constrained()
+                    .with_width(device_code_style.right)
+                    .boxed(),
+                ])
+                .contained()
+                .with_style(device_code_style.cta.style_for(state, false).container)
+                .boxed()
+        })
+        .on_click(gpui::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::CursorStyle::PointingHand)
+        .boxed()
+    }
+
+    fn render_prompting_modal(
+        data: &PromptUserDeviceFlow,
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> ElementBox {
+        Flex::column()
+            .with_children([
+                Flex::column()
+                    .with_children([
+                        Label::new(
+                            "Enable Copilot by connecting",
+                            style.auth.prompting.subheading.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                        Label::new(
+                            "your existing license.",
+                            style.auth.prompting.subheading.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                    ])
+                    .align_children_center()
+                    .contained()
+                    .with_style(style.auth.prompting.subheading.container)
+                    .boxed(),
+                Self::render_device_code(data, &style, cx),
+                Flex::column()
+                    .with_children([
+                        Label::new(
+                            "Paste this code into GitHub after",
+                            style.auth.prompting.hint.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                        Label::new(
+                            "clicking the button below.",
+                            style.auth.prompting.hint.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                    ])
+                    .align_children_center()
+                    .contained()
+                    .with_style(style.auth.prompting.hint.container.clone())
+                    .boxed(),
+                theme::ui::cta_button_with_click(
+                    "Connect to GitHub",
+                    style.auth.content_width,
+                    &style.auth.cta_button,
+                    cx,
+                    {
+                        let verification_uri = data.verification_uri.clone();
+                        move |_, cx| cx.platform().open_url(&verification_uri)
+                    },
+                )
+                .boxed(),
+            ])
+            .align_children_center()
+            .boxed()
+    }
+    fn render_enabled_modal(
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> ElementBox {
+        let enabled_style = &style.auth.authorized;
+        Flex::column()
+            .with_children([
+                Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
+                    .contained()
+                    .with_style(enabled_style.subheading.container)
+                    .aligned()
+                    .boxed(),
+                Flex::column()
+                    .with_children([
+                        Label::new(
+                            "You can update your settings or",
+                            enabled_style.hint.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                        Label::new(
+                            "sign out from the Copilot menu in",
+                            enabled_style.hint.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                        Label::new("the status bar.", enabled_style.hint.text.clone())
+                            .aligned()
+                            .boxed(),
+                    ])
+                    .align_children_center()
+                    .contained()
+                    .with_style(enabled_style.hint.container)
+                    .boxed(),
+                theme::ui::cta_button_with_click(
+                    "Done",
+                    style.auth.content_width,
+                    &style.auth.cta_button,
+                    cx,
+                    |_, cx| {
+                        let window_id = cx.window_id();
+                        cx.remove_window(window_id)
+                    },
+                )
+                .boxed(),
+            ])
+            .align_children_center()
+            .boxed()
+    }
+    fn render_unauthorized_modal(
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> ElementBox {
+        let unauthorized_style = &style.auth.not_authorized;
+
+        Flex::column()
+            .with_children([
+                Flex::column()
+                    .with_children([
+                        Label::new(
+                            "Enable Copilot by connecting",
+                            unauthorized_style.subheading.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                        Label::new(
+                            "your existing license.",
+                            unauthorized_style.subheading.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                    ])
+                    .align_children_center()
+                    .contained()
+                    .with_style(unauthorized_style.subheading.container)
+                    .boxed(),
+                Flex::column()
+                    .with_children([
+                        Label::new(
+                            "You must have an active copilot",
+                            unauthorized_style.warning.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                        Label::new(
+                            "license to use it in Zed.",
+                            unauthorized_style.warning.text.clone(),
+                        )
+                        .aligned()
+                        .boxed(),
+                    ])
+                    .align_children_center()
+                    .contained()
+                    .with_style(unauthorized_style.warning.container)
+                    .boxed(),
+                theme::ui::cta_button_with_click(
+                    "Subscribe on GitHub",
+                    style.auth.content_width,
+                    &style.auth.cta_button,
+                    cx,
+                    |_, cx| {
+                        let window_id = cx.window_id();
+                        cx.remove_window(window_id);
+                        cx.platform().open_url(COPILOT_SIGN_UP_URL)
+                    },
+                )
+                .boxed(),
+            ])
+            .align_children_center()
+            .boxed()
+    }
+}
+
+impl Entity for CopilotCodeVerification {
+    type Event = ();
+}
+
+impl View for CopilotCodeVerification {
+    fn ui_name() -> &'static str {
+        "CopilotCodeVerification"
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
+        cx.notify()
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
+        cx.notify()
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        let style = cx.global::<Settings>().theme.clone();
+
+        modal("Connect Copilot to Zed", &style.copilot.modal, cx, |cx| {
+            Flex::column()
+                .with_children([
+                    theme::ui::icon(&style.copilot.auth.header).boxed(),
+                    match &self.status {
+                        Status::SigningIn {
+                            prompt: Some(prompt),
+                        } => Self::render_prompting_modal(&prompt, &style.copilot, cx),
+                        Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
+                        Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
+                        _ => Empty::new().boxed(),
+                    },
+                ])
+                .align_children_center()
+                .boxed()
+        })
+    }
+}

crates/copilot_button/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "copilot_button"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot_button.rs"
+doctest = false
+
+[dependencies]
+copilot = { path = "../copilot" }
+editor = { path = "../editor" }
+context_menu = { path = "../context_menu" }
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+anyhow = "1.0"
+smol = "1.2.5"
+futures = "0.3"

crates/copilot_button/src/copilot_button.rs 🔗

@@ -0,0 +1,360 @@
+use std::sync::Arc;
+
+use context_menu::{ContextMenu, ContextMenuItem};
+use editor::Editor;
+use gpui::{
+    elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
+    MouseState, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
+};
+use settings::{settings_file::SettingsFile, Settings};
+use workspace::{
+    item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
+    StatusItemView,
+};
+
+use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
+
+const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
+const COPILOT_STARTING_TOAST_ID: usize = 1337;
+const COPILOT_ERROR_TOAST_ID: usize = 1338;
+
+#[derive(Clone, PartialEq)]
+pub struct DeployCopilotMenu;
+
+#[derive(Clone, PartialEq)]
+pub struct ToggleCopilotForLanguage {
+    language: Arc<str>,
+}
+
+#[derive(Clone, PartialEq)]
+pub struct ToggleCopilotGlobally;
+
+// TODO: Make the other code path use `get_or_insert` logic for this modal
+#[derive(Clone, PartialEq)]
+pub struct DeployCopilotModal;
+
+impl_internal_actions!(
+    copilot,
+    [
+        DeployCopilotMenu,
+        DeployCopilotModal,
+        ToggleCopilotForLanguage,
+        ToggleCopilotGlobally,
+    ]
+);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(CopilotButton::deploy_copilot_menu);
+    cx.add_action(
+        |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
+            let language = action.language.to_owned();
+
+            let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
+
+            SettingsFile::update(cx, move |file_contents| {
+                file_contents.languages.insert(
+                    language.to_owned(),
+                    settings::EditorSettings {
+                        copilot: Some((!current_langauge).into()),
+                        ..Default::default()
+                    },
+                );
+            })
+        },
+    );
+
+    cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
+        let copilot_on = cx.global::<Settings>().copilot_on(None);
+
+        SettingsFile::update(cx, move |file_contents| {
+            file_contents.editor.copilot = Some((!copilot_on).into())
+        })
+    });
+}
+
+pub struct CopilotButton {
+    popup_menu: ViewHandle<ContextMenu>,
+    editor_subscription: Option<(Subscription, usize)>,
+    editor_enabled: Option<bool>,
+    language: Option<Arc<str>>,
+}
+
+impl Entity for CopilotButton {
+    type Event = ();
+}
+
+impl View for CopilotButton {
+    fn ui_name() -> &'static str {
+        "CopilotButton"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
+        let settings = cx.global::<Settings>();
+
+        if !settings.enable_copilot_integration {
+            return Empty::new().boxed();
+        }
+
+        let theme = settings.theme.clone();
+        let active = self.popup_menu.read(cx).visible();
+        let Some(copilot) = Copilot::global(cx) else {
+            return Empty::new().boxed();
+        };
+        let status = copilot.read(cx).status();
+
+        let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
+
+        let view_id = cx.view_id();
+
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<Self>::new(0, cx, {
+                    let theme = theme.clone();
+                    let status = status.clone();
+                    move |state, _cx| {
+                        let style = theme
+                            .workspace
+                            .status_bar
+                            .sidebar_buttons
+                            .item
+                            .style_for(state, active);
+
+                        Flex::row()
+                            .with_child(
+                                Svg::new({
+                                    match status {
+                                        Status::Error(_) => "icons/copilot_error_16.svg",
+                                        Status::Authorized => {
+                                            if enabled {
+                                                "icons/copilot_16.svg"
+                                            } else {
+                                                "icons/copilot_disabled_16.svg"
+                                            }
+                                        }
+                                        _ => "icons/copilot_init_16.svg",
+                                    }
+                                })
+                                .with_color(style.icon_color)
+                                .constrained()
+                                .with_width(style.icon_size)
+                                .aligned()
+                                .named("copilot-icon"),
+                            )
+                            .constrained()
+                            .with_height(style.icon_size)
+                            .contained()
+                            .with_style(style.container)
+                            .boxed()
+                    }
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, {
+                    let status = status.clone();
+                    move |_, cx| match status {
+                        Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
+                        Status::Starting { ref task } => {
+                            cx.dispatch_action(workspace::Toast::new(
+                                COPILOT_STARTING_TOAST_ID,
+                                "Copilot is starting...",
+                            ));
+                            let window_id = cx.window_id();
+                            let task = task.to_owned();
+                            cx.spawn(|mut cx| async move {
+                                task.await;
+                                cx.update(|cx| {
+                                    if let Some(copilot) = Copilot::global(cx) {
+                                        let status = copilot.read(cx).status();
+                                        match status {
+                                            Status::Authorized => cx.dispatch_action_at(
+                                                window_id,
+                                                view_id,
+                                                workspace::Toast::new(
+                                                    COPILOT_STARTING_TOAST_ID,
+                                                    "Copilot has started!",
+                                                ),
+                                            ),
+                                            _ => {
+                                                cx.dispatch_action_at(
+                                                    window_id,
+                                                    view_id,
+                                                    DismissToast::new(COPILOT_STARTING_TOAST_ID),
+                                                );
+                                                cx.dispatch_global_action(SignIn)
+                                            }
+                                        }
+                                    }
+                                })
+                            })
+                            .detach();
+                        }
+                        Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
+                            COPILOT_ERROR_TOAST_ID,
+                            format!("Copilot can't be started: {}", e),
+                            "Reinstall Copilot",
+                            Reinstall,
+                        )),
+                        _ => cx.dispatch_action(SignIn),
+                    }
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "GitHub Copilot".into(),
+                    None,
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .boxed(),
+            )
+            .with_child(
+                ChildView::new(&self.popup_menu, cx)
+                    .aligned()
+                    .top()
+                    .right()
+                    .boxed(),
+            )
+            .boxed()
+    }
+}
+
+impl CopilotButton {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        let menu = cx.add_view(|cx| {
+            let mut menu = ContextMenu::new(cx);
+            menu.set_position_mode(OverlayPositionMode::Local);
+            menu
+        });
+
+        cx.observe(&menu, |_, _, cx| cx.notify()).detach();
+
+        Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
+
+        let this_handle = cx.handle().downgrade();
+        cx.observe_global::<Settings, _>(move |cx| {
+            if let Some(handle) = this_handle.upgrade(cx) {
+                handle.update(cx, |_, cx| cx.notify())
+            }
+        })
+        .detach();
+
+        Self {
+            popup_menu: menu,
+            editor_subscription: None,
+            editor_enabled: None,
+            language: None,
+        }
+    }
+
+    pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
+        let settings = cx.global::<Settings>();
+
+        let mut menu_options = Vec::with_capacity(6);
+
+        if let Some((_, view_id)) = self.editor_subscription.as_ref() {
+            let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
+            menu_options.push(ContextMenuItem::item_for_view(
+                if locally_enabled {
+                    "Pause Copilot for this file"
+                } else {
+                    "Resume Copilot for this file"
+                },
+                *view_id,
+                copilot::Toggle,
+            ));
+        }
+
+        if let Some(language) = &self.language {
+            let language_enabled = settings.copilot_on(Some(language.as_ref()));
+
+            menu_options.push(ContextMenuItem::item(
+                format!(
+                    "{} Copilot for {}",
+                    if language_enabled {
+                        "Disable"
+                    } else {
+                        "Enable"
+                    },
+                    language
+                ),
+                ToggleCopilotForLanguage {
+                    language: language.to_owned(),
+                },
+            ));
+        }
+
+        let globally_enabled = cx.global::<Settings>().copilot_on(None);
+        menu_options.push(ContextMenuItem::item(
+            if globally_enabled {
+                "Disable Copilot Globally"
+            } else {
+                "Enable Copilot Globally"
+            },
+            ToggleCopilotGlobally,
+        ));
+
+        menu_options.push(ContextMenuItem::Separator);
+
+        let icon_style = settings.theme.copilot.out_link_icon.clone();
+        menu_options.push(ContextMenuItem::element_item(
+            Box::new(
+                move |state: &mut MouseState, style: &theme::ContextMenuItem| {
+                    Flex::row()
+                        .with_children([
+                            Label::new("Copilot Settings", style.label.clone()).boxed(),
+                            theme::ui::icon(icon_style.style_for(state, false)).boxed(),
+                        ])
+                        .align_children_center()
+                        .boxed()
+                },
+            ),
+            OsOpen::new(COPILOT_SETTINGS_URL),
+        ));
+
+        menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
+
+        self.popup_menu.update(cx, |menu, cx| {
+            menu.show(
+                Default::default(),
+                AnchorCorner::BottomRight,
+                menu_options,
+                cx,
+            );
+        });
+    }
+
+    pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+        let settings = cx.global::<Settings>();
+        let suggestion_anchor = editor.selections.newest_anchor().start;
+
+        let language_name = snapshot
+            .language_at(suggestion_anchor)
+            .map(|language| language.name());
+
+        self.language = language_name.clone();
+
+        if let Some(enabled) = editor.copilot_state.user_enabled {
+            self.editor_enabled = Some(enabled);
+        } else {
+            self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
+        }
+
+        cx.notify()
+    }
+}
+
+impl StatusItemView for CopilotButton {
+    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
+            self.editor_subscription =
+                Some((cx.observe(&editor, Self::update_enabled), editor.id()));
+            self.update_enabled(editor, cx);
+        } else {
+            self.language = None;
+            self.editor_subscription = None;
+            self.editor_enabled = None;
+        }
+        cx.notify();
+    }
+}

crates/db/Cargo.toml 🔗

@@ -23,7 +23,8 @@ async-trait = "0.1"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-serde = { version = "1.0", features = ["derive"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 smol = "1.2"
 
 [dev-dependencies]

crates/db/src/db.rs 🔗

@@ -4,6 +4,7 @@ pub mod query;
 // Re-export
 pub use anyhow;
 use anyhow::Context;
+use gpui::MutableAppContext;
 pub use indoc::indoc;
 pub use lazy_static;
 use parking_lot::{Mutex, RwLock};
@@ -17,6 +18,7 @@ use sqlez::domain::Migrator;
 use sqlez::thread_safe_connection::ThreadSafeConnection;
 use sqlez_macros::sql;
 use std::fs::create_dir_all;
+use std::future::Future;
 use std::path::{Path, PathBuf};
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::time::{SystemTime, UNIX_EPOCH};
@@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
 const DB_FILE_NAME: &'static str = "db.sqlite";
 
 lazy_static::lazy_static! {
+    // !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
     static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
     static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
     pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
@@ -63,11 +66,11 @@ pub async fn open_db<M: Migrator + 'static>(
     let connection = async_iife!({
         // Note: This still has a race condition where 1 set of migrations succeeds
         // (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
-        // This will cause the first connection to have the database taken out 
+        // This will cause the first connection to have the database taken out
         // from under it. This *should* be fine though. The second dabatase failure will
         // cause errors in the log and so should be observed by developers while writing
         // soon-to-be good migrations. If user databases are corrupted, we toss them out
-        // and try again from a blank. As long as running all migrations from start to end 
+        // and try again from a blank. As long as running all migrations from start to end
         // on a blank database is ok, this race condition will never be triggered.
         //
         // Basically: Don't ever push invalid migrations to stable or everyone will have
@@ -85,7 +88,7 @@ pub async fn open_db<M: Migrator + 'static>(
             };
         }
 
-        // Take a lock in the failure case so that we move the db once per process instead 
+        // Take a lock in the failure case so that we move the db once per process instead
         // of potentially multiple times from different threads. This shouldn't happen in the
         // normal path
         let _lock = DB_FILE_OPERATIONS.lock();
@@ -236,6 +239,15 @@ macro_rules! define_connection {
     };
 }
 
+pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
+where
+    F: Future<Output = anyhow::Result<()>> + Send,
+{
+    cx.background()
+        .spawn(async move { db_write().await.log_err() })
+        .detach()
+}
+
 #[cfg(test)]
 mod tests {
     use std::{fs, thread};

crates/diagnostics/Cargo.toml 🔗

@@ -20,7 +20,7 @@ settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 
 [dev-dependencies]
 unindent = "0.1"
@@ -29,4 +29,4 @@ editor = { path = "../editor", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
-serde_json = { version = "1", features = ["preserve_order"] }
+serde_json = { workspace = true }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -90,14 +90,11 @@ impl View for ProjectDiagnosticsEditor {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         if self.path_states.is_empty() {
             let theme = &cx.global::<Settings>().theme.project_diagnostics;
-            Label::new(
-                "No problems in workspace".to_string(),
-                theme.empty_message.clone(),
-            )
-            .aligned()
-            .contained()
-            .with_style(theme.container)
-            .boxed()
+            Label::new("No problems in workspace", theme.empty_message.clone())
+                .aligned()
+                .contained()
+                .with_style(theme.container)
+                .boxed()
         } else {
             ChildView::new(&self.editor, cx).boxed()
         }
@@ -605,16 +602,16 @@ impl Item for ProjectDiagnosticsEditor {
         ))
     }
 
-    fn act_as_type(
-        &self,
+    fn act_as_type<'a>(
+        &'a self,
         type_id: TypeId,
-        self_handle: &ViewHandle<Self>,
-        _: &AppContext,
-    ) -> Option<AnyViewHandle> {
+        self_handle: &'a ViewHandle<Self>,
+        _: &'a AppContext,
+    ) -> Option<&AnyViewHandle> {
         if type_id == TypeId::of::<Self>() {
-            Some(self_handle.into())
+            Some(self_handle)
         } else if type_id == TypeId::of::<Editor>() {
-            Some((&self.editor).into())
+            Some(&self.editor)
         } else {
             None
         }
@@ -697,7 +694,7 @@ pub(crate) fn render_summary(
     theme: &theme::ProjectDiagnostics,
 ) -> ElementBox {
     if summary.error_count == 0 && summary.warning_count == 0 {
-        Label::new("No problems".to_string(), text_style.clone()).boxed()
+        Label::new("No problems", text_style.clone()).boxed()
     } else {
         let icon_width = theme.tab_icon_width;
         let icon_spacing = theme.tab_icon_spacing;
@@ -808,15 +805,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {

crates/diagnostics/src/items.rs 🔗

@@ -178,14 +178,11 @@ impl View for DiagnosticIndicator {
 
         if in_progress {
             element.add_child(
-                Label::new(
-                    "Checking…".into(),
-                    style.diagnostic_message.default.text.clone(),
-                )
-                .aligned()
-                .contained()
-                .with_margin_left(item_spacing)
-                .boxed(),
+                Label::new("Checking…", style.diagnostic_message.default.text.clone())
+                    .aligned()
+                    .contained()
+                    .with_margin_left(item_spacing)
+                    .boxed(),
             );
         } else if let Some(diagnostic) = &self.current_diagnostic {
             let message_style = style.diagnostic_message.clone();

crates/editor/Cargo.toml 🔗

@@ -22,10 +22,10 @@ test-support = [
 ]
 
 [dependencies]
-drag_and_drop = { path = "../drag_and_drop" }
-text = { path = "../text" }
 clock = { path = "../clock" }
+copilot = { path = "../copilot" }
 db = { path = "../db" }
+drag_and_drop = { path = "../drag_and_drop" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
 fuzzy = { path = "../fuzzy" }
@@ -38,10 +38,12 @@ rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 sqlez = { path = "../sqlez" }
 workspace = { path = "../workspace" }
+
 aho-corasick = "0.7"
 anyhow = "1.0"
 futures = "0.3"
@@ -51,9 +53,10 @@ lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 ordered-float = "2.1.1"
 parking_lot = "0.11"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 serde = { workspace = true }
+serde_derive = { workspace = true }
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 tree-sitter-rust = { version = "*", optional = true }

crates/editor/src/display_map.rs 🔗

@@ -1,19 +1,23 @@
 mod block_map;
 mod fold_map;
+mod suggestion_map;
 mod tab_map;
 mod wrap_map;
 
 use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
-use block_map::{BlockMap, BlockPoint};
+pub use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
+    color::Color,
     fonts::{FontId, HighlightStyle},
     Entity, ModelContext, ModelHandle,
 };
 use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
 use settings::Settings;
 use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+pub use suggestion_map::Suggestion;
+use suggestion_map::SuggestionMap;
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
 use wrap_map::WrapMap;
@@ -23,6 +27,12 @@ pub use block_map::{
     BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum FoldStatus {
+    Folded,
+    Foldable,
+}
+
 pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
 }
@@ -33,6 +43,7 @@ pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
     buffer_subscription: BufferSubscription,
     fold_map: FoldMap,
+    suggestion_map: SuggestionMap,
     tab_map: TabMap,
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
@@ -58,6 +69,7 @@ impl DisplayMap {
 
         let tab_size = Self::tab_size(&buffer, cx);
         let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
+        let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
         let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
         let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -66,6 +78,7 @@ impl DisplayMap {
             buffer,
             buffer_subscription,
             fold_map,
+            suggestion_map,
             tab_map,
             wrap_map,
             block_map,
@@ -77,21 +90,25 @@ impl DisplayMap {
     pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
         let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
-        let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
+        let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
+        let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
 
         let tab_size = Self::tab_size(&self.buffer, cx);
-        let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits, tab_size);
-        let (wraps_snapshot, edits) = self
+        let (tab_snapshot, edits) = self
+            .tab_map
+            .sync(suggestion_snapshot.clone(), edits, tab_size);
+        let (wrap_snapshot, edits) = self
             .wrap_map
-            .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
-        let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits);
+            .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
+        let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits);
 
         DisplaySnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(cx),
-            folds_snapshot,
-            tabs_snapshot,
-            wraps_snapshot,
-            blocks_snapshot,
+            fold_snapshot,
+            suggestion_snapshot,
+            tab_snapshot,
+            wrap_snapshot,
+            block_snapshot,
             text_highlights: self.text_highlights.clone(),
             clip_at_line_ends: self.clip_at_line_ends,
         }
@@ -115,12 +132,14 @@ impl DisplayMap {
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
+        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.fold(ranges);
+        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -138,12 +157,14 @@ impl DisplayMap {
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
+        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
+        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -160,6 +181,7 @@ impl DisplayMap {
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -177,6 +199,7 @@ impl DisplayMap {
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -207,11 +230,38 @@ impl DisplayMap {
         self.text_highlights.remove(&Some(type_id))
     }
 
+    pub fn has_suggestion(&self) -> bool {
+        self.suggestion_map.has_suggestion()
+    }
+
+    pub fn replace_suggestion<T>(
+        &self,
+        new_suggestion: Option<Suggestion<T>>,
+        cx: &mut ModelContext<Self>,
+    ) where
+        T: ToPoint,
+    {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.suggestion_map.replace(new_suggestion, snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+    }
+
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_font(font_id, font_size, cx))
     }
 
+    pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
+        self.fold_map.set_ellipses_color(color)
+    }
+
     pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -235,10 +285,11 @@ impl DisplayMap {
 
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
-    folds_snapshot: fold_map::FoldSnapshot,
-    tabs_snapshot: tab_map::TabSnapshot,
-    wraps_snapshot: wrap_map::WrapSnapshot,
-    blocks_snapshot: block_map::BlockSnapshot,
+    fold_snapshot: fold_map::FoldSnapshot,
+    suggestion_snapshot: suggestion_map::SuggestionSnapshot,
+    tab_snapshot: tab_map::TabSnapshot,
+    wrap_snapshot: wrap_map::WrapSnapshot,
+    block_snapshot: block_map::BlockSnapshot,
     text_highlights: TextHighlights,
     clip_at_line_ends: bool,
 }
@@ -246,7 +297,7 @@ pub struct DisplaySnapshot {
 impl DisplaySnapshot {
     #[cfg(test)]
     pub fn fold_count(&self) -> usize {
-        self.folds_snapshot.fold_count()
+        self.fold_snapshot.fold_count()
     }
 
     pub fn is_empty(&self) -> bool {
@@ -254,7 +305,7 @@ impl DisplaySnapshot {
     }
 
     pub fn buffer_rows(&self, start_row: u32) -> DisplayBufferRows {
-        self.blocks_snapshot.buffer_rows(start_row)
+        self.block_snapshot.buffer_rows(start_row)
     }
 
     pub fn max_buffer_row(&self) -> u32 {
@@ -263,9 +314,9 @@ impl DisplaySnapshot {
 
     pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left);
+            let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
             *fold_point.column_mut() = 0;
-            point = fold_point.to_buffer_point(&self.folds_snapshot);
+            point = fold_point.to_buffer_point(&self.fold_snapshot);
 
             let mut display_point = self.point_to_display_point(point, Bias::Left);
             *display_point.column_mut() = 0;
@@ -279,9 +330,9 @@ impl DisplaySnapshot {
 
     pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right);
-            *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
-            point = fold_point.to_buffer_point(&self.folds_snapshot);
+            let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
+            *fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
+            point = fold_point.to_buffer_point(&self.fold_snapshot);
 
             let mut display_point = self.point_to_display_point(point, Bias::Right);
             *display_point.column_mut() = self.line_len(display_point.row());
@@ -311,37 +362,39 @@ impl DisplaySnapshot {
     }
 
     fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
-        let fold_point = self.folds_snapshot.to_fold_point(point, bias);
-        let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
-        let wrap_point = self.wraps_snapshot.tab_point_to_wrap_point(tab_point);
-        let block_point = self.blocks_snapshot.to_block_point(wrap_point);
+        let fold_point = self.fold_snapshot.to_fold_point(point, bias);
+        let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
+        let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
+        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
+        let block_point = self.block_snapshot.to_block_point(wrap_point);
         DisplayPoint(block_point)
     }
 
     fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
         let block_point = point.0;
-        let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
-        let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
-        let fold_point = self.tabs_snapshot.to_fold_point(tab_point, bias).0;
-        fold_point.to_buffer_point(&self.folds_snapshot)
+        let wrap_point = self.block_snapshot.to_wrap_point(block_point);
+        let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
+        let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
+        let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
+        fold_point.to_buffer_point(&self.fold_snapshot)
     }
 
     pub fn max_point(&self) -> DisplayPoint {
-        DisplayPoint(self.blocks_snapshot.max_point())
+        DisplayPoint(self.block_snapshot.max_point())
     }
 
     /// Returns text chunks starting at the given display row until the end of the file
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
-        self.blocks_snapshot
-            .chunks(display_row..self.max_point().row() + 1, false, None)
+        self.block_snapshot
+            .chunks(display_row..self.max_point().row() + 1, false, None, None)
             .map(|h| h.text)
     }
 
     /// Returns text chunks starting at the end of the given display row in reverse until the start of the file
     pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         (0..=display_row).into_iter().rev().flat_map(|row| {
-            self.blocks_snapshot
-                .chunks(row..row + 1, false, None)
+            self.block_snapshot
+                .chunks(row..row + 1, false, None, None)
                 .map(|h| h.text)
                 .collect::<Vec<_>>()
                 .into_iter()
@@ -349,16 +402,25 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
-        self.blocks_snapshot
-            .chunks(display_rows, language_aware, Some(&self.text_highlights))
+    pub fn chunks(
+        &self,
+        display_rows: Range<u32>,
+        language_aware: bool,
+        suggestion_highlight: Option<HighlightStyle>,
+    ) -> DisplayChunks<'_> {
+        self.block_snapshot.chunks(
+            display_rows,
+            language_aware,
+            Some(&self.text_highlights),
+            suggestion_highlight,
+        )
     }
 
     pub fn chars_at(
         &self,
         mut point: DisplayPoint,
     ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
-        point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
+        point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
         self.text_chunks(point.row())
             .flat_map(str::chars)
             .skip_while({
@@ -385,7 +447,7 @@ impl DisplaySnapshot {
         &self,
         mut point: DisplayPoint,
     ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
-        point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
+        point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
         self.reverse_text_chunks(point.row())
             .flat_map(|chunk| chunk.chars().rev())
             .skip_while({
@@ -499,7 +561,7 @@ impl DisplaySnapshot {
     }
 
     pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
-        let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
+        let mut clipped = self.block_snapshot.clip_point(point.0, bias);
         if self.clip_at_line_ends {
             clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
         }
@@ -510,7 +572,7 @@ impl DisplaySnapshot {
         let mut point = point.0;
         if point.column == self.line_len(point.row) {
             point.column = point.column.saturating_sub(1);
-            point = self.blocks_snapshot.clip_point(point, Bias::Left);
+            point = self.block_snapshot.clip_point(point, Bias::Left);
         }
         DisplayPoint(point)
     }
@@ -519,37 +581,34 @@ impl DisplaySnapshot {
     where
         T: ToOffset,
     {
-        self.folds_snapshot.folds_in_range(range)
+        self.fold_snapshot.folds_in_range(range)
     }
 
     pub fn blocks_in_range(
         &self,
         rows: Range<u32>,
     ) -> impl Iterator<Item = (u32, &TransformBlock)> {
-        self.blocks_snapshot.blocks_in_range(rows)
+        self.block_snapshot.blocks_in_range(rows)
     }
 
     pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
-        self.folds_snapshot.intersects_fold(offset)
+        self.fold_snapshot.intersects_fold(offset)
     }
 
-    pub fn is_line_folded(&self, display_row: u32) -> bool {
-        let block_point = BlockPoint(Point::new(display_row, 0));
-        let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
-        let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
-        self.folds_snapshot.is_line_folded(tab_point.row())
+    pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+        self.fold_snapshot.is_line_folded(buffer_row)
     }
 
     pub fn is_block_line(&self, display_row: u32) -> bool {
-        self.blocks_snapshot.is_block_line(display_row)
+        self.block_snapshot.is_block_line(display_row)
     }
 
     pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
         let wrap_row = self
-            .blocks_snapshot
+            .block_snapshot
             .to_wrap_point(BlockPoint::new(display_row, 0))
             .row();
-        self.wraps_snapshot.soft_wrap_indent(wrap_row)
+        self.wrap_snapshot.soft_wrap_indent(wrap_row)
     }
 
     pub fn text(&self) -> String {
@@ -583,12 +642,92 @@ impl DisplaySnapshot {
         (indent, is_blank)
     }
 
+    pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
+        let (buffer, range) = self
+            .buffer_snapshot
+            .buffer_line_for_row(buffer_row)
+            .unwrap();
+
+        let mut indent_size = 0;
+        let mut is_blank = false;
+        for c in buffer.chars_at(Point::new(range.start.row, 0)) {
+            if c == ' ' || c == '\t' {
+                indent_size += 1;
+            } else {
+                if c == '\n' {
+                    is_blank = true;
+                }
+                break;
+            }
+        }
+
+        (indent_size, is_blank)
+    }
+
     pub fn line_len(&self, row: u32) -> u32 {
-        self.blocks_snapshot.line_len(row)
+        self.block_snapshot.line_len(row)
     }
 
     pub fn longest_row(&self) -> u32 {
-        self.blocks_snapshot.longest_row()
+        self.block_snapshot.longest_row()
+    }
+
+    pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option<FoldStatus> {
+        if self.is_line_folded(buffer_row) {
+            Some(FoldStatus::Folded)
+        } else if self.is_foldable(buffer_row) {
+            Some(FoldStatus::Foldable)
+        } else {
+            None
+        }
+    }
+
+    pub fn is_foldable(self: &Self, buffer_row: u32) -> bool {
+        let max_row = self.buffer_snapshot.max_buffer_row();
+        if buffer_row >= max_row {
+            return false;
+        }
+
+        let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
+        if is_blank {
+            return false;
+        }
+
+        for next_row in (buffer_row + 1)..=max_row {
+            let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row);
+            if next_indent_size > indent_size {
+                return true;
+            } else if !next_line_is_blank {
+                break;
+            }
+        }
+
+        false
+    }
+
+    pub fn foldable_range(self: &Self, buffer_row: u32) -> Option<Range<Point>> {
+        let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row));
+        if self.is_foldable(start.row) && !self.is_line_folded(start.row) {
+            let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
+            let max_point = self.buffer_snapshot.max_point();
+            let mut end = None;
+
+            for row in (buffer_row + 1)..=max_point.row {
+                let (indent, is_blank) = self.line_indent_for_buffer_row(row);
+                if !is_blank && indent <= start_indent {
+                    let prev_row = row - 1;
+                    end = Some(Point::new(
+                        prev_row,
+                        self.buffer_snapshot.line_len(prev_row),
+                    ));
+                    break;
+                }
+            }
+            let end = end.unwrap_or(max_point);
+            Some(start..end)
+        } else {
+            None
+        }
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -647,10 +786,11 @@ impl DisplayPoint {
     }
 
     pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
-        let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0);
-        let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point);
-        let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
-        unexpanded_point.to_buffer_offset(&map.folds_snapshot)
+        let wrap_point = map.block_snapshot.to_wrap_point(self.0);
+        let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
+        let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
+        let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
+        fold_point.to_buffer_offset(&map.fold_snapshot)
     }
 }
 
@@ -678,6 +818,24 @@ impl ToDisplayPoint for Anchor {
     }
 }
 
+pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
+    let max_row = display_map.max_point().row();
+    let start_row = display_row + 1;
+    let mut current = None;
+    std::iter::from_fn(move || {
+        if current == None {
+            current = Some(start_row);
+        } else {
+            current = Some(current.unwrap() + 1)
+        }
+        if current.unwrap() > max_row {
+            None
+        } else {
+            current
+        }
+    })
+}
+
 #[cfg(test)]
 pub mod tests {
     use super::*;
@@ -703,7 +861,9 @@ pub mod tests {
         let mut tab_size = rng.gen_range(1..=4);
         let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
         let excerpt_header_height = rng.gen_range(1..=5);
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -753,10 +913,10 @@ pub mod tests {
 
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
-        log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
-        log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
-        log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
-        log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
+        log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+        log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+        log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+        log::info!("block text: {:?}", snapshot.block_snapshot.text());
         log::info!("display text: {:?}", snapshot.text());
 
         for _i in 0..operations {
@@ -861,10 +1021,10 @@ pub mod tests {
             let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
             fold_count = snapshot.fold_count();
             log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
-            log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
-            log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
-            log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
-            log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
+            log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+            log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+            log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+            log::info!("block text: {:?}", snapshot.block_snapshot.text());
             log::info!("display text: {:?}", snapshot.text());
 
             // Line boundaries
@@ -960,7 +1120,9 @@ pub mod tests {
 
         let font_cache = cx.font_cache();
 
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1049,7 +1211,10 @@ pub mod tests {
         cx.set_global(Settings::test(cx));
         let text = sample_text(6, 6, 'a');
         let buffer = MultiBuffer::build_simple(&text, cx);
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1132,7 +1297,9 @@ pub mod tests {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1167,7 +1334,7 @@ pub mod tests {
             vec![
                 ("fn ".to_string(), None),
                 ("out".to_string(), Some(Color::blue())),
-                ("…".to_string(), None),
+                ("⋯".to_string(), None),
                 ("  fn ".to_string(), Some(Color::red())),
                 ("inner".to_string(), Some(Color::blue())),
                 ("() {}\n}".to_string(), Some(Color::red())),
@@ -1220,7 +1387,9 @@ pub mod tests {
 
         let font_cache = cx.font_cache();
 
-        let family_id = font_cache.load_family(&["Courier"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1248,7 +1417,7 @@ pub mod tests {
             cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
             [
                 ("out".to_string(), Some(Color::blue())),
-                ("…\n".to_string(), None),
+                ("⋯\n".to_string(), None),
                 ("  \nfn ".to_string(), Some(Color::red())),
                 ("i\n".to_string(), Some(Color::blue()))
             ]
@@ -1292,7 +1461,9 @@ pub mod tests {
         let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
 
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Courier"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1408,7 +1579,9 @@ pub mod tests {
         let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
         let buffer = MultiBuffer::build_simple(text, cx);
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1466,7 +1639,9 @@ pub mod tests {
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
         let font_cache = cx.font_cache();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1525,7 +1700,7 @@ pub mod tests {
     ) -> Vec<(String, Option<Color>, Option<Color>)> {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
-        for chunk in snapshot.chunks(rows, true) {
+        for chunk in snapshot.chunks(rows, true, None) {
             let syntax_color = chunk
                 .syntax_highlight_id
                 .and_then(|id| id.style(theme)?.color);

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

@@ -4,7 +4,7 @@ use super::{
 };
 use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
-use gpui::{ElementBox, RenderContext};
+use gpui::{fonts::HighlightStyle, ElementBox, RenderContext};
 use language::{BufferSnapshot, Chunk, Patch, Point};
 use parking_lot::Mutex;
 use std::{
@@ -572,7 +572,7 @@ impl<'a> BlockMapWriter<'a> {
 impl BlockSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(0..self.transforms.summary().output_rows, false, None)
+        self.chunks(0..self.transforms.summary().output_rows, false, None, None)
             .map(|chunk| chunk.text)
             .collect()
     }
@@ -582,6 +582,7 @@ impl BlockSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -614,6 +615,7 @@ impl BlockSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
+                suggestion_highlight,
             ),
             input_chunk: Default::default(),
             transforms: cursor,
@@ -989,6 +991,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::display_map::suggestion_map::SuggestionMap;
     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
     use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
@@ -1015,7 +1018,10 @@ mod tests {
     fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
 
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1026,9 +1032,10 @@ mod tests {
         let buffer = MultiBuffer::build_simple(text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
-        let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot, 1.try_into().unwrap());
-        let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
+        let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+        let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
 
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1170,12 +1177,14 @@ mod tests {
             buffer.snapshot(cx)
         });
 
-        let (folds_snapshot, fold_edits) =
+        let (fold_snapshot, fold_edits) =
             fold_map.read(buffer_snapshot, subscription.consume().into_inner());
-        let (tabs_snapshot, tab_edits) =
-            tab_map.sync(folds_snapshot, fold_edits, 4.try_into().unwrap());
+        let (suggestion_snapshot, suggestion_edits) =
+            suggestion_map.sync(fold_snapshot, fold_edits);
+        let (tab_snapshot, tab_edits) =
+            tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-            wrap_map.sync(tabs_snapshot, tab_edits, cx)
+            wrap_map.sync(tab_snapshot, tab_edits, cx)
         });
         let snapshot = block_map.read(wraps_snapshot, wrap_edits);
         assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
@@ -1185,7 +1194,10 @@ mod tests {
     fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
 
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1195,9 +1207,10 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, tabs_snapshot) = TabMap::new(folds_snapshot, 1.try_into().unwrap());
-        let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+        let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
 
         let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1241,7 +1254,10 @@ mod tests {
             Some(rng.gen_range(0.0..=100.0))
         };
         let tab_size = 1.try_into().unwrap();
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
@@ -1263,10 +1279,11 @@ mod tests {
         };
 
         let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot, tab_size);
+        let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
         let (wrap_map, wraps_snapshot) =
-            WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
+            WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
         let mut block_map = BlockMap::new(
             wraps_snapshot,
             buffer_start_header_height,
@@ -1317,12 +1334,14 @@ mod tests {
                         })
                         .collect::<Vec<_>>();
 
-                    let (folds_snapshot, fold_edits) =
+                    let (fold_snapshot, fold_edits) =
                         fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (tabs_snapshot, tab_edits) =
-                        tab_map.sync(folds_snapshot, fold_edits, tab_size);
+                    let (suggestion_snapshot, suggestion_edits) =
+                        suggestion_map.sync(fold_snapshot, fold_edits);
+                    let (tab_snapshot, tab_edits) =
+                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-                        wrap_map.sync(tabs_snapshot, tab_edits, cx)
+                        wrap_map.sync(tab_snapshot, tab_edits, cx)
                     });
                     let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
                     let block_ids = block_map.insert(block_properties.clone());
@@ -1340,12 +1359,14 @@ mod tests {
                         })
                         .collect();
 
-                    let (folds_snapshot, fold_edits) =
+                    let (fold_snapshot, fold_edits) =
                         fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (tabs_snapshot, tab_edits) =
-                        tab_map.sync(folds_snapshot, fold_edits, tab_size);
+                    let (suggestion_snapshot, suggestion_edits) =
+                        suggestion_map.sync(fold_snapshot, fold_edits);
+                    let (tab_snapshot, tab_edits) =
+                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-                        wrap_map.sync(tabs_snapshot, tab_edits, cx)
+                        wrap_map.sync(tab_snapshot, tab_edits, cx)
                     });
                     let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
                     block_map.remove(block_ids_to_remove);
@@ -1362,10 +1383,13 @@ mod tests {
                 }
             }
 
-            let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
+            let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
+            let (suggestion_snapshot, suggestion_edits) =
+                suggestion_map.sync(fold_snapshot, fold_edits);
+            let (tab_snapshot, tab_edits) =
+                tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-                wrap_map.sync(tabs_snapshot, tab_edits, cx)
+                wrap_map.sync(tab_snapshot, tab_edits, cx)
             });
             let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
             assert_eq!(
@@ -1476,6 +1500,7 @@ mod tests {
                         start_row as u32..blocks_snapshot.max_point().row + 1,
                         false,
                         None,
+                        None,
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

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

@@ -4,7 +4,7 @@ use crate::{
     ToOffset,
 };
 use collections::BTreeMap;
-use gpui::fonts::HighlightStyle;
+use gpui::{color::Color, fonts::HighlightStyle};
 use language::{Chunk, Edit, Point, TextSummary};
 use parking_lot::Mutex;
 use std::{
@@ -29,10 +29,6 @@ impl FoldPoint {
         self.0.row
     }
 
-    pub fn column(self) -> u32 {
-        self.0.column
-    }
-
     pub fn row_mut(&mut self) -> &mut u32 {
         &mut self.0.row
     }
@@ -133,6 +129,7 @@ impl<'a> FoldMapWriter<'a> {
             folds: self.0.folds.clone(),
             buffer_snapshot: buffer,
             version: self.0.version.load(SeqCst),
+            ellipses_color: self.0.ellipses_color,
         };
         (snapshot, edits)
     }
@@ -182,6 +179,7 @@ impl<'a> FoldMapWriter<'a> {
             folds: self.0.folds.clone(),
             buffer_snapshot: buffer,
             version: self.0.version.load(SeqCst),
+            ellipses_color: self.0.ellipses_color,
         };
         (snapshot, edits)
     }
@@ -192,6 +190,7 @@ pub struct FoldMap {
     transforms: Mutex<SumTree<Transform>>,
     folds: SumTree<Fold>,
     version: AtomicUsize,
+    ellipses_color: Option<Color>,
 }
 
 impl FoldMap {
@@ -209,6 +208,7 @@ impl FoldMap {
                 },
                 &(),
             )),
+            ellipses_color: None,
             version: Default::default(),
         };
 
@@ -217,6 +217,7 @@ impl FoldMap {
             folds: this.folds.clone(),
             buffer_snapshot: this.buffer.lock().clone(),
             version: this.version.load(SeqCst),
+            ellipses_color: None,
         };
         (this, snapshot)
     }
@@ -233,6 +234,7 @@ impl FoldMap {
             folds: self.folds.clone(),
             buffer_snapshot: self.buffer.lock().clone(),
             version: self.version.load(SeqCst),
+            ellipses_color: self.ellipses_color,
         };
         (snapshot, edits)
     }
@@ -246,6 +248,15 @@ impl FoldMap {
         (FoldMapWriter(self), snapshot, edits)
     }
 
+    pub fn set_ellipses_color(&mut self, color: Color) -> bool {
+        if self.ellipses_color != Some(color) {
+            self.ellipses_color = Some(color);
+            true
+        } else {
+            false
+        }
+    }
+
     fn check_invariants(&self) {
         if cfg!(test) {
             assert_eq!(
@@ -370,7 +381,7 @@ impl FoldMap {
                     }
 
                     if fold.end > fold.start {
-                        let output_text = "…";
+                        let output_text = "⋯";
                         new_transforms.push(
                             Transform {
                                 summary: TransformSummary {
@@ -477,6 +488,7 @@ pub struct FoldSnapshot {
     folds: SumTree<Fold>,
     buffer_snapshot: MultiBufferSnapshot,
     pub version: usize,
+    pub ellipses_color: Option<Color>,
 }
 
 impl FoldSnapshot {
@@ -623,14 +635,14 @@ impl FoldSnapshot {
         cursor.item().map_or(false, |t| t.output_text.is_some())
     }
 
-    pub fn is_line_folded(&self, output_row: u32) -> bool {
-        let mut cursor = self.transforms.cursor::<FoldPoint>();
-        cursor.seek(&FoldPoint::new(output_row, 0), Bias::Right, &());
+    pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+        let mut cursor = self.transforms.cursor::<Point>();
+        cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
         while let Some(transform) = cursor.item() {
             if transform.output_text.is_some() {
                 return true;
             }
-            if cursor.end(&()).row() == output_row {
+            if cursor.end(&()).row == buffer_row {
                 cursor.next(&())
             } else {
                 break;
@@ -639,12 +651,6 @@ impl FoldSnapshot {
         false
     }
 
-    pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
-        let start = start.to_offset(self);
-        self.chunks(start..self.len(), false, None)
-            .flat_map(|chunk| chunk.text.chars())
-    }
-
     pub fn chunks<'a>(
         &'a self,
         range: Range<FoldOffset>,
@@ -739,6 +745,7 @@ impl FoldSnapshot {
             max_output_offset: range.end.0,
             highlight_endpoints: highlight_endpoints.into_iter().peekable(),
             active_highlights: Default::default(),
+            ellipses_color: self.ellipses_color,
         }
     }
 
@@ -1029,6 +1036,7 @@ pub struct FoldChunks<'a> {
     max_output_offset: usize,
     highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
     active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+    ellipses_color: Option<Color>,
 }
 
 impl<'a> Iterator for FoldChunks<'a> {
@@ -1058,7 +1066,10 @@ impl<'a> Iterator for FoldChunks<'a> {
             return Some(Chunk {
                 text: output_text,
                 syntax_highlight_id: None,
-                highlight_style: None,
+                highlight_style: self.ellipses_color.map(|color| HighlightStyle {
+                    color: Some(color),
+                    ..Default::default()
+                }),
                 diagnostic_severity: None,
                 is_unnecessary: false,
             });
@@ -1193,6 +1204,7 @@ pub type FoldEdit = Edit<FoldOffset>;
 mod tests {
     use super::*;
     use crate::{MultiBuffer, ToPoint};
+    use collections::HashSet;
     use rand::prelude::*;
     use settings::Settings;
     use std::{cmp::Reverse, env, mem, sync::Arc};
@@ -1214,7 +1226,7 @@ mod tests {
             Point::new(0, 2)..Point::new(2, 2),
             Point::new(2, 4)..Point::new(4, 1),
         ]);
-        assert_eq!(snapshot2.text(), "aa…cc…eeeee");
+        assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
         assert_eq!(
             edits,
             &[
@@ -1241,7 +1253,7 @@ mod tests {
             buffer.snapshot(cx)
         });
         let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner());
-        assert_eq!(snapshot3.text(), "123a…c123c…eeeee");
+        assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee");
         assert_eq!(
             edits,
             &[
@@ -1261,12 +1273,12 @@ mod tests {
             buffer.snapshot(cx)
         });
         let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
-        assert_eq!(snapshot4.text(), "123a…c123456eee");
+        assert_eq!(snapshot4.text(), "123a⋯c123456eee");
 
         let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
         writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
         let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
-        assert_eq!(snapshot5.text(), "123a…c123456eee");
+        assert_eq!(snapshot5.text(), "123a⋯c123456eee");
 
         let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
         writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
@@ -1287,19 +1299,19 @@ mod tests {
             let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
             writer.fold(vec![5..8]);
             let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
-            assert_eq!(snapshot.text(), "abcde…ijkl");
+            assert_eq!(snapshot.text(), "abcde⋯ijkl");
 
             // Create an fold adjacent to the start of the first fold.
             let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
             writer.fold(vec![0..1, 2..5]);
             let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
-            assert_eq!(snapshot.text(), "…b…ijkl");
+            assert_eq!(snapshot.text(), "⋯b⋯ijkl");
 
             // Create an fold adjacent to the end of the first fold.
             let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
             writer.fold(vec![11..11, 8..10]);
             let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
-            assert_eq!(snapshot.text(), "…b…kl");
+            assert_eq!(snapshot.text(), "⋯b⋯kl");
         }
 
         {
@@ -1309,7 +1321,7 @@ mod tests {
             let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
             writer.fold(vec![0..2, 2..5]);
             let (snapshot, _) = map.read(buffer_snapshot, vec![]);
-            assert_eq!(snapshot.text(), "…fghijkl");
+            assert_eq!(snapshot.text(), "⋯fghijkl");
 
             // Edit within one of the folds.
             let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -1317,7 +1329,7 @@ mod tests {
                 buffer.snapshot(cx)
             });
             let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
-            assert_eq!(snapshot.text(), "12345…fghijkl");
+            assert_eq!(snapshot.text(), "12345⋯fghijkl");
         }
     }
 
@@ -1334,7 +1346,7 @@ mod tests {
             Point::new(3, 1)..Point::new(4, 1),
         ]);
         let (snapshot, _) = map.read(buffer_snapshot, vec![]);
-        assert_eq!(snapshot.text(), "aa…eeeee");
+        assert_eq!(snapshot.text(), "aa⋯eeeee");
     }
 
     #[gpui::test]
@@ -1351,14 +1363,14 @@ mod tests {
             Point::new(3, 1)..Point::new(4, 1),
         ]);
         let (snapshot, _) = map.read(buffer_snapshot, vec![]);
-        assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee");
+        assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
 
         let buffer_snapshot = buffer.update(cx, |buffer, cx| {
             buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
             buffer.snapshot(cx)
         });
         let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
-        assert_eq!(snapshot.text(), "aa…eeeee");
+        assert_eq!(snapshot.text(), "aa⋯eeeee");
     }
 
     #[gpui::test]
@@ -1450,7 +1462,7 @@ mod tests {
 
             let mut expected_text: String = buffer_snapshot.text().to_string();
             for fold_range in map.merged_fold_ranges().into_iter().rev() {
-                expected_text.replace_range(fold_range.start..fold_range.end, "…");
+                expected_text.replace_range(fold_range.start..fold_range.end, "⋯");
             }
 
             assert_eq!(snapshot.text(), expected_text);
@@ -1572,10 +1584,13 @@ mod tests {
                 fold_row += 1;
             }
 
-            for fold_range in map.merged_fold_ranges() {
-                let fold_point =
-                    snapshot.to_fold_point(fold_range.start.to_point(&buffer_snapshot), Right);
-                assert!(snapshot.is_line_folded(fold_point.row()));
+            let fold_start_rows = map
+                .merged_fold_ranges()
+                .iter()
+                .map(|range| range.start.to_point(&buffer_snapshot).row)
+                .collect::<HashSet<_>>();
+            for row in fold_start_rows {
+                assert!(snapshot.is_line_folded(row));
             }
 
             for _ in 0..5 {
@@ -1655,7 +1670,7 @@ mod tests {
         ]);
 
         let (snapshot, _) = map.read(buffer_snapshot, vec![]);
-        assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee\nffffff\n");
+        assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n");
         assert_eq!(
             snapshot.buffer_rows(0).collect::<Vec<_>>(),
             [Some(0), Some(3), Some(5), Some(6)]

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

@@ -0,0 +1,860 @@
+use super::{
+    fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
+    TextHighlights,
+};
+use crate::{MultiBufferSnapshot, ToPoint};
+use gpui::fonts::HighlightStyle;
+use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
+use parking_lot::Mutex;
+use std::{
+    cmp,
+    ops::{Add, AddAssign, Range, Sub},
+};
+use util::post_inc;
+
+pub type SuggestionEdit = Edit<SuggestionOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct SuggestionOffset(pub usize);
+
+impl Add for SuggestionOffset {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for SuggestionOffset {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl AddAssign for SuggestionOffset {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += rhs.0;
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct SuggestionPoint(pub Point);
+
+impl SuggestionPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct Suggestion<T> {
+    pub position: T,
+    pub text: Rope,
+}
+
+pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
+
+impl SuggestionMap {
+    pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
+        let snapshot = SuggestionSnapshot {
+            fold_snapshot,
+            suggestion: None,
+            version: 0,
+        };
+        (Self(Mutex::new(snapshot.clone())), snapshot)
+    }
+
+    pub fn replace<T>(
+        &self,
+        new_suggestion: Option<Suggestion<T>>,
+        fold_snapshot: FoldSnapshot,
+        fold_edits: Vec<FoldEdit>,
+    ) -> (SuggestionSnapshot, Vec<SuggestionEdit>)
+    where
+        T: ToPoint,
+    {
+        let new_suggestion = new_suggestion.map(|new_suggestion| {
+            let buffer_point = new_suggestion
+                .position
+                .to_point(fold_snapshot.buffer_snapshot());
+            let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
+            let fold_offset = fold_point.to_offset(&fold_snapshot);
+            Suggestion {
+                position: fold_offset,
+                text: new_suggestion.text,
+            }
+        });
+
+        let (_, edits) = self.sync(fold_snapshot, fold_edits);
+        let mut snapshot = self.0.lock();
+
+        let mut patch = Patch::new(edits);
+        if let Some(suggestion) = snapshot.suggestion.take() {
+            patch = patch.compose([SuggestionEdit {
+                old: SuggestionOffset(suggestion.position.0)
+                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
+                new: SuggestionOffset(suggestion.position.0)
+                    ..SuggestionOffset(suggestion.position.0),
+            }]);
+        }
+
+        if let Some(suggestion) = new_suggestion.as_ref() {
+            patch = patch.compose([SuggestionEdit {
+                old: SuggestionOffset(suggestion.position.0)
+                    ..SuggestionOffset(suggestion.position.0),
+                new: SuggestionOffset(suggestion.position.0)
+                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
+            }]);
+        }
+
+        snapshot.suggestion = new_suggestion;
+        snapshot.version += 1;
+        (snapshot.clone(), patch.into_inner())
+    }
+
+    pub fn sync(
+        &self,
+        fold_snapshot: FoldSnapshot,
+        fold_edits: Vec<FoldEdit>,
+    ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
+        let mut snapshot = self.0.lock();
+
+        if snapshot.fold_snapshot.version != fold_snapshot.version {
+            snapshot.version += 1;
+        }
+
+        let mut suggestion_edits = Vec::new();
+
+        let mut suggestion_old_len = 0;
+        let mut suggestion_new_len = 0;
+        for fold_edit in fold_edits {
+            let start = fold_edit.new.start;
+            let end = FoldOffset(start.0 + fold_edit.old_len().0);
+            if let Some(suggestion) = snapshot.suggestion.as_mut() {
+                if end <= suggestion.position {
+                    suggestion.position.0 += fold_edit.new_len().0;
+                    suggestion.position.0 -= fold_edit.old_len().0;
+                } else if start > suggestion.position {
+                    suggestion_old_len = suggestion.text.len();
+                    suggestion_new_len = suggestion_old_len;
+                } else {
+                    suggestion_old_len = suggestion.text.len();
+                    snapshot.suggestion.take();
+                    suggestion_edits.push(SuggestionEdit {
+                        old: SuggestionOffset(fold_edit.old.start.0)
+                            ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
+                        new: SuggestionOffset(fold_edit.new.start.0)
+                            ..SuggestionOffset(fold_edit.new.end.0),
+                    });
+                    continue;
+                }
+            }
+
+            suggestion_edits.push(SuggestionEdit {
+                old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
+                    ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
+                new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
+                    ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
+            });
+        }
+        snapshot.fold_snapshot = fold_snapshot;
+
+        (snapshot.clone(), suggestion_edits)
+    }
+
+    pub fn has_suggestion(&self) -> bool {
+        let snapshot = self.0.lock();
+        snapshot.suggestion.is_some()
+    }
+}
+
+#[derive(Clone)]
+pub struct SuggestionSnapshot {
+    pub fold_snapshot: FoldSnapshot,
+    pub suggestion: Option<Suggestion<FoldOffset>>,
+    pub version: usize,
+}
+
+impl SuggestionSnapshot {
+    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
+        self.fold_snapshot.buffer_snapshot()
+    }
+
+    pub fn max_point(&self) -> SuggestionPoint {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
+            let mut max_point = suggestion_point.0;
+            max_point += suggestion.text.max_point();
+            max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
+            SuggestionPoint(max_point)
+        } else {
+            SuggestionPoint(self.fold_snapshot.max_point().0)
+        }
+    }
+
+    pub fn len(&self) -> SuggestionOffset {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let mut len = suggestion.position.0;
+            len += suggestion.text.len();
+            len += self.fold_snapshot.len().0 - suggestion.position.0;
+            SuggestionOffset(len)
+        } else {
+            SuggestionOffset(self.fold_snapshot.len().0)
+        }
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        if let Some(suggestion) = &self.suggestion {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let suggestion_end = suggestion_start + suggestion.text.max_point();
+
+            if row < suggestion_start.row {
+                self.fold_snapshot.line_len(row)
+            } else if row > suggestion_end.row {
+                self.fold_snapshot
+                    .line_len(suggestion_start.row + (row - suggestion_end.row))
+            } else {
+                let mut result = suggestion.text.line_len(row - suggestion_start.row);
+                if row == suggestion_start.row {
+                    result += suggestion_start.column;
+                }
+                if row == suggestion_end.row {
+                    result +=
+                        self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
+                }
+                result
+            }
+        } else {
+            self.fold_snapshot.line_len(row)
+        }
+    }
+
+    pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let suggestion_end = suggestion_start + suggestion.text.max_point();
+            if point.0 <= suggestion_start {
+                SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
+            } else if point.0 > suggestion_end {
+                let fold_point = self.fold_snapshot.clip_point(
+                    FoldPoint(suggestion_start + (point.0 - suggestion_end)),
+                    bias,
+                );
+                let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
+                if bias == Bias::Left && suggestion_point == suggestion_end {
+                    SuggestionPoint(suggestion_start)
+                } else {
+                    SuggestionPoint(suggestion_point)
+                }
+            } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
+                SuggestionPoint(suggestion_start)
+            } else {
+                let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
+                    > suggestion_start.column
+                {
+                    FoldPoint(suggestion_start + Point::new(0, 1))
+                } else {
+                    FoldPoint(suggestion_start + Point::new(1, 0))
+                };
+                let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
+                SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
+            }
+        } else {
+            SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
+        }
+    }
+
+    pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let suggestion_end = suggestion_start + suggestion.text.max_point();
+
+            if point.0 <= suggestion_start {
+                SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
+            } else if point.0 > suggestion_end {
+                let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
+                    .to_offset(&self.fold_snapshot);
+                SuggestionOffset(fold_offset.0 + suggestion.text.len())
+            } else {
+                let offset_in_suggestion =
+                    suggestion.text.point_to_offset(point.0 - suggestion_start);
+                SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
+            }
+        } else {
+            SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
+        }
+    }
+
+    pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            if offset.0 <= suggestion.position.0 {
+                SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
+            } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
+                let fold_point = FoldOffset(offset.0 - suggestion.text.len())
+                    .to_point(&self.fold_snapshot)
+                    .0;
+
+                SuggestionPoint(
+                    suggestion_point_start
+                        + suggestion.text.max_point()
+                        + (fold_point - suggestion_point_start),
+                )
+            } else {
+                let point_in_suggestion = suggestion
+                    .text
+                    .offset_to_point(offset.0 - suggestion.position.0);
+                SuggestionPoint(suggestion_point_start + point_in_suggestion)
+            }
+        } else {
+            SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
+        }
+    }
+
+    pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let suggestion_end = suggestion_start + suggestion.text.max_point();
+
+            if point.0 <= suggestion_start {
+                FoldPoint(point.0)
+            } else if point.0 > suggestion_end {
+                FoldPoint(suggestion_start + (point.0 - suggestion_end))
+            } else {
+                FoldPoint(suggestion_start)
+            }
+        } else {
+            FoldPoint(point.0)
+        }
+    }
+
+    pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+
+            if point.0 <= suggestion_start {
+                SuggestionPoint(point.0)
+            } else {
+                let suggestion_end = suggestion_start + suggestion.text.max_point();
+                SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
+            }
+        } else {
+            SuggestionPoint(point.0)
+        }
+    }
+
+    pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let suggestion_end = suggestion_start + suggestion.text.max_point();
+            let mut summary = TextSummary::default();
+
+            let prefix_range =
+                cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
+            if prefix_range.start < prefix_range.end {
+                summary += self.fold_snapshot.text_summary_for_range(
+                    FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
+                );
+            }
+
+            let suggestion_range =
+                cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
+            if suggestion_range.start < suggestion_range.end {
+                let point_range = suggestion_range.start - suggestion_start
+                    ..suggestion_range.end - suggestion_start;
+                let offset_range = suggestion.text.point_to_offset(point_range.start)
+                    ..suggestion.text.point_to_offset(point_range.end);
+                summary += suggestion
+                    .text
+                    .cursor(offset_range.start)
+                    .summary::<TextSummary>(offset_range.end);
+            }
+
+            let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
+            if suffix_range.start < suffix_range.end {
+                let start = suggestion_start + (suffix_range.start - suggestion_end);
+                let end = suggestion_start + (suffix_range.end - suggestion_end);
+                summary += self
+                    .fold_snapshot
+                    .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
+            }
+
+            summary
+        } else {
+            self.fold_snapshot
+                .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
+        }
+    }
+
+    pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
+        let start = self.to_offset(start);
+        self.chunks(start..self.len(), false, None, None)
+            .flat_map(|chunk| chunk.text.chars())
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<SuggestionOffset>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
+    ) -> SuggestionChunks<'a> {
+        if let Some(suggestion) = self.suggestion.as_ref() {
+            let suggestion_range =
+                suggestion.position.0..suggestion.position.0 + suggestion.text.len();
+
+            let prefix_chunks = if range.start.0 < suggestion_range.start {
+                Some(self.fold_snapshot.chunks(
+                    FoldOffset(range.start.0)
+                        ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
+                    language_aware,
+                    text_highlights,
+                ))
+            } else {
+                None
+            };
+
+            let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
+                ..cmp::min(range.end.0, suggestion_range.end);
+            let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
+            {
+                let start = clipped_suggestion_range.start - suggestion_range.start;
+                let end = clipped_suggestion_range.end - suggestion_range.start;
+                Some(suggestion.text.chunks_in_range(start..end))
+            } else {
+                None
+            };
+
+            let suffix_chunks = if range.end.0 > suggestion_range.end {
+                let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
+                let end = range.end.0 - suggestion_range.len();
+                Some(self.fold_snapshot.chunks(
+                    FoldOffset(start)..FoldOffset(end),
+                    language_aware,
+                    text_highlights,
+                ))
+            } else {
+                None
+            };
+
+            SuggestionChunks {
+                prefix_chunks,
+                suggestion_chunks,
+                suffix_chunks,
+                highlight_style: suggestion_highlight,
+            }
+        } else {
+            SuggestionChunks {
+                prefix_chunks: Some(self.fold_snapshot.chunks(
+                    FoldOffset(range.start.0)..FoldOffset(range.end.0),
+                    language_aware,
+                    text_highlights,
+                )),
+                suggestion_chunks: None,
+                suffix_chunks: None,
+                highlight_style: None,
+            }
+        }
+    }
+
+    pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
+        let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
+            let start = suggestion.position.to_point(&self.fold_snapshot).0;
+            let end = start + suggestion.text.max_point();
+            start.row..end.row
+        } else {
+            u32::MAX..u32::MAX
+        };
+
+        let fold_buffer_rows = if row <= suggestion_range.start {
+            self.fold_snapshot.buffer_rows(row)
+        } else if row > suggestion_range.end {
+            self.fold_snapshot
+                .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
+        } else {
+            let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
+            rows.next();
+            rows
+        };
+
+        SuggestionBufferRows {
+            current_row: row,
+            suggestion_row_start: suggestion_range.start,
+            suggestion_row_end: suggestion_range.end,
+            fold_buffer_rows,
+        }
+    }
+
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(Default::default()..self.len(), false, None, None)
+            .map(|chunk| chunk.text)
+            .collect()
+    }
+}
+
+pub struct SuggestionChunks<'a> {
+    prefix_chunks: Option<FoldChunks<'a>>,
+    suggestion_chunks: Option<text::Chunks<'a>>,
+    suffix_chunks: Option<FoldChunks<'a>>,
+    highlight_style: Option<HighlightStyle>,
+}
+
+impl<'a> Iterator for SuggestionChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(chunks) = self.prefix_chunks.as_mut() {
+            if let Some(chunk) = chunks.next() {
+                return Some(chunk);
+            } else {
+                self.prefix_chunks = None;
+            }
+        }
+
+        if let Some(chunks) = self.suggestion_chunks.as_mut() {
+            if let Some(chunk) = chunks.next() {
+                return Some(Chunk {
+                    text: chunk,
+                    syntax_highlight_id: None,
+                    highlight_style: self.highlight_style,
+                    diagnostic_severity: None,
+                    is_unnecessary: false,
+                });
+            } else {
+                self.suggestion_chunks = None;
+            }
+        }
+
+        if let Some(chunks) = self.suffix_chunks.as_mut() {
+            if let Some(chunk) = chunks.next() {
+                return Some(chunk);
+            } else {
+                self.suffix_chunks = None;
+            }
+        }
+
+        None
+    }
+}
+
+#[derive(Clone)]
+pub struct SuggestionBufferRows<'a> {
+    current_row: u32,
+    suggestion_row_start: u32,
+    suggestion_row_end: u32,
+    fold_buffer_rows: FoldBufferRows<'a>,
+}
+
+impl<'a> Iterator for SuggestionBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let row = post_inc(&mut self.current_row);
+        if row <= self.suggestion_row_start || row > self.suggestion_row_end {
+            self.fold_buffer_rows.next()
+        } else {
+            Some(None)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{display_map::fold_map::FoldMap, MultiBuffer};
+    use gpui::MutableAppContext;
+    use rand::{prelude::StdRng, Rng};
+    use settings::Settings;
+    use std::{
+        env,
+        ops::{Bound, RangeBounds},
+    };
+
+    #[gpui::test]
+    fn test_basic(cx: &mut MutableAppContext) {
+        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
+        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
+        assert_eq!(suggestion_snapshot.text(), "abcdefghi");
+
+        let (suggestion_snapshot, _) = suggestion_map.replace(
+            Some(Suggestion {
+                position: 3,
+                text: "123\n456".into(),
+            }),
+            fold_snapshot,
+            Default::default(),
+        );
+        assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
+
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
+                None,
+                cx,
+            )
+        });
+        let (fold_snapshot, fold_edits) = fold_map.read(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
+        assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
+
+        let (mut fold_map_writer, _, _) =
+            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
+        let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
+        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
+        assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
+
+        let (mut fold_map_writer, _, _) =
+            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
+        let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
+        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
+        assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_suggestions(cx: &mut MutableAppContext, mut rng: StdRng) {
+        cx.set_global(Settings::test(cx));
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let len = rng.gen_range(0..30);
+        let buffer = if rng.gen() {
+            let text = util::RandomCharIter::new(&mut rng)
+                .take(len)
+                .collect::<String>();
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+        log::info!("buffer text: {:?}", buffer_snapshot.text());
+
+        let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
+
+        for _ in 0..operations {
+            let mut suggestion_edits = Patch::default();
+
+            let mut prev_suggestion_text = suggestion_snapshot.text();
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=29 => {
+                    let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
+                    suggestion_edits = suggestion_edits.compose(edits);
+                }
+                30..=59 => {
+                    for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+                        fold_snapshot = new_fold_snapshot;
+                        let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
+                        suggestion_edits = suggestion_edits.compose(edits);
+                    }
+                }
+                _ => buffer.update(cx, |buffer, cx| {
+                    let subscription = buffer.subscribe();
+                    let edit_count = rng.gen_range(1..=5);
+                    buffer.randomly_mutate(&mut rng, edit_count, cx);
+                    buffer_snapshot = buffer.snapshot(cx);
+                    let edits = subscription.consume().into_inner();
+                    log::info!("editing {:?}", edits);
+                    buffer_edits.extend(edits);
+                }),
+            };
+
+            let (new_fold_snapshot, fold_edits) =
+                fold_map.read(buffer_snapshot.clone(), buffer_edits);
+            fold_snapshot = new_fold_snapshot;
+            let (new_suggestion_snapshot, edits) =
+                suggestion_map.sync(fold_snapshot.clone(), fold_edits);
+            suggestion_snapshot = new_suggestion_snapshot;
+            suggestion_edits = suggestion_edits.compose(edits);
+
+            log::info!("buffer text: {:?}", buffer_snapshot.text());
+            log::info!("folds text: {:?}", fold_snapshot.text());
+            log::info!("suggestions text: {:?}", suggestion_snapshot.text());
+
+            let mut expected_text = Rope::from(fold_snapshot.text().as_str());
+            let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
+                expected_text.replace(
+                    suggestion.position.0..suggestion.position.0,
+                    &suggestion.text.to_string(),
+                );
+                let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
+                let suggestion_end = suggestion_start + suggestion.text.max_point();
+                expected_buffer_rows.splice(
+                    (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
+                    (0..suggestion_end.row - suggestion_start.row).map(|_| None),
+                );
+            }
+            assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
+            for row_start in 0..expected_buffer_rows.len() {
+                assert_eq!(
+                    suggestion_snapshot
+                        .buffer_rows(row_start as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[row_start..],
+                    "incorrect buffer rows starting at {}",
+                    row_start
+                );
+            }
+
+            for _ in 0..5 {
+                let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
+                end = expected_text.clip_offset(end, Bias::Right);
+                let mut start = rng.gen_range(0..=end);
+                start = expected_text.clip_offset(start, Bias::Right);
+
+                let actual_text = suggestion_snapshot
+                    .chunks(
+                        SuggestionOffset(start)..SuggestionOffset(end),
+                        false,
+                        None,
+                        None,
+                    )
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text,
+                    expected_text.slice(start..end).to_string(),
+                    "incorrect text in range {:?}",
+                    start..end
+                );
+
+                let start_point = SuggestionPoint(expected_text.offset_to_point(start));
+                let end_point = SuggestionPoint(expected_text.offset_to_point(end));
+                assert_eq!(
+                    suggestion_snapshot.text_summary_for_range(start_point..end_point),
+                    expected_text.slice(start..end).summary()
+                );
+            }
+
+            for edit in suggestion_edits.into_inner() {
+                prev_suggestion_text.replace_range(
+                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+                    &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
+                );
+            }
+            assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
+
+            assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
+            assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
+
+            let mut suggestion_point = SuggestionPoint::default();
+            let mut suggestion_offset = SuggestionOffset::default();
+            for ch in expected_text.chars() {
+                assert_eq!(
+                    suggestion_snapshot.to_offset(suggestion_point),
+                    suggestion_offset,
+                    "invalid to_offset({:?})",
+                    suggestion_point
+                );
+                assert_eq!(
+                    suggestion_snapshot.to_point(suggestion_offset),
+                    suggestion_point,
+                    "invalid to_point({:?})",
+                    suggestion_offset
+                );
+                assert_eq!(
+                    suggestion_snapshot
+                        .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
+                    suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
+                );
+
+                let mut bytes = [0; 4];
+                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+                    suggestion_offset.0 += 1;
+                    if *byte == b'\n' {
+                        suggestion_point.0 += Point::new(1, 0);
+                    } else {
+                        suggestion_point.0 += Point::new(0, 1);
+                    }
+
+                    let clipped_left_point =
+                        suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
+                    let clipped_right_point =
+                        suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
+                    assert!(
+                        clipped_left_point <= clipped_right_point,
+                        "clipped left point {:?} is greater than clipped right point {:?}",
+                        clipped_left_point,
+                        clipped_right_point
+                    );
+                    assert_eq!(
+                        clipped_left_point.0,
+                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
+                    );
+                    assert_eq!(
+                        clipped_right_point.0,
+                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
+                    );
+                    assert!(clipped_left_point <= suggestion_snapshot.max_point());
+                    assert!(clipped_right_point <= suggestion_snapshot.max_point());
+
+                    if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
+                        let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
+                        let suggestion_end = suggestion_start + suggestion.text.max_point();
+                        let invalid_range = (
+                            Bound::Excluded(suggestion_start),
+                            Bound::Included(suggestion_end),
+                        );
+                        assert!(
+                            !invalid_range.contains(&clipped_left_point.0),
+                            "clipped left point {:?} is inside invalid suggestion range {:?}",
+                            clipped_left_point,
+                            invalid_range
+                        );
+                        assert!(
+                            !invalid_range.contains(&clipped_right_point.0),
+                            "clipped right point {:?} is inside invalid suggestion range {:?}",
+                            clipped_right_point,
+                            invalid_range
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    impl SuggestionMap {
+        pub fn randomly_mutate(
+            &self,
+            rng: &mut impl Rng,
+        ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
+            let fold_snapshot = self.0.lock().fold_snapshot.clone();
+            let new_suggestion = if rng.gen_bool(0.3) {
+                None
+            } else {
+                let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
+                let len = rng.gen_range(0..30);
+                Some(Suggestion {
+                    position: index,
+                    text: util::RandomCharIter::new(rng)
+                        .take(len)
+                        .filter(|ch| *ch != '\r')
+                        .collect::<String>()
+                        .as_str()
+                        .into(),
+                })
+            };
+
+            log::info!("replacing suggestion with {:?}", new_suggestion);
+            self.replace(new_suggestion, fold_snapshot, Default::default())
+        }
+    }
+}

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

@@ -1,86 +1,140 @@
 use super::{
-    fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot},
+    suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
     TextHighlights,
 };
 use crate::MultiBufferSnapshot;
+use gpui::fonts::HighlightStyle;
 use language::{Chunk, Point};
 use parking_lot::Mutex;
 use std::{cmp, mem, num::NonZeroU32, ops::Range};
 use sum_tree::Bias;
 
+const MAX_EXPANSION_COLUMN: u32 = 256;
+
 pub struct TabMap(Mutex<TabSnapshot>);
 
 impl TabMap {
-    pub fn new(input: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+    pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
         let snapshot = TabSnapshot {
-            fold_snapshot: input,
+            suggestion_snapshot: input,
             tab_size,
+            max_expansion_column: MAX_EXPANSION_COLUMN,
             version: 0,
         };
         (Self(Mutex::new(snapshot.clone())), snapshot)
     }
 
+    #[cfg(test)]
+    pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
+        self.0.lock().max_expansion_column = column;
+        self.0.lock().clone()
+    }
+
     pub fn sync(
         &self,
-        fold_snapshot: FoldSnapshot,
-        mut fold_edits: Vec<FoldEdit>,
+        suggestion_snapshot: SuggestionSnapshot,
+        mut suggestion_edits: Vec<SuggestionEdit>,
         tab_size: NonZeroU32,
     ) -> (TabSnapshot, Vec<TabEdit>) {
         let mut old_snapshot = self.0.lock();
         let mut new_snapshot = TabSnapshot {
-            fold_snapshot,
+            suggestion_snapshot,
             tab_size,
+            max_expansion_column: old_snapshot.max_expansion_column,
             version: old_snapshot.version,
         };
 
-        if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
+        if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
             new_snapshot.version += 1;
         }
 
-        let old_max_offset = old_snapshot.fold_snapshot.len();
-        let mut tab_edits = Vec::with_capacity(fold_edits.len());
+        let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
 
         if old_snapshot.tab_size == new_snapshot.tab_size {
-            for fold_edit in &mut fold_edits {
-                let mut delta = 0;
-                for chunk in old_snapshot.fold_snapshot.chunks(
-                    fold_edit.old.end..old_max_offset,
+            // Expand each edit to include the next tab on the same line as the edit,
+            // and any subsequent tabs on that line that moved across the tab expansion
+            // boundary.
+            for suggestion_edit in &mut suggestion_edits {
+                let old_end = old_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.old.end);
+                let old_end_row_successor_offset =
+                    old_snapshot.suggestion_snapshot.to_offset(cmp::min(
+                        SuggestionPoint::new(old_end.row() + 1, 0),
+                        old_snapshot.suggestion_snapshot.max_point(),
+                    ));
+                let new_end = new_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.new.end);
+
+                let mut offset_from_edit = 0;
+                let mut first_tab_offset = None;
+                let mut last_tab_with_changed_expansion_offset = None;
+                'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
+                    suggestion_edit.old.end..old_end_row_successor_offset,
                     false,
                     None,
+                    None,
                 ) {
-                    let patterns: &[_] = &['\t', '\n'];
-                    if let Some(ix) = chunk.text.find(patterns) {
-                        if &chunk.text[ix..ix + 1] == "\t" {
-                            fold_edit.old.end.0 += delta + ix + 1;
-                            fold_edit.new.end.0 += delta + ix + 1;
+                    for (ix, _) in chunk.text.match_indices('\t') {
+                        let offset_from_edit = offset_from_edit + (ix as u32);
+                        if first_tab_offset.is_none() {
+                            first_tab_offset = Some(offset_from_edit);
+                        }
+
+                        let old_column = old_end.column() + offset_from_edit;
+                        let new_column = new_end.column() + offset_from_edit;
+                        let was_expanded = old_column < old_snapshot.max_expansion_column;
+                        let is_expanded = new_column < new_snapshot.max_expansion_column;
+                        if was_expanded != is_expanded {
+                            last_tab_with_changed_expansion_offset = Some(offset_from_edit);
+                        } else if !was_expanded && !is_expanded {
+                            break 'outer;
                         }
+                    }
 
+                    offset_from_edit += chunk.text.len() as u32;
+                    if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
+                        && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
+                    {
                         break;
                     }
+                }
 
-                    delta += chunk.text.len();
+                if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
+                    suggestion_edit.old.end.0 += offset as usize + 1;
+                    suggestion_edit.new.end.0 += offset as usize + 1;
                 }
             }
 
+            // Combine any edits that overlap due to the expansion.
             let mut ix = 1;
-            while ix < fold_edits.len() {
-                let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
+            while ix < suggestion_edits.len() {
+                let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
                 let prev_edit = prev_edits.last_mut().unwrap();
                 let edit = &next_edits[0];
                 if prev_edit.old.end >= edit.old.start {
                     prev_edit.old.end = edit.old.end;
                     prev_edit.new.end = edit.new.end;
-                    fold_edits.remove(ix);
+                    suggestion_edits.remove(ix);
                 } else {
                     ix += 1;
                 }
             }
 
-            for fold_edit in fold_edits {
-                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
-                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
-                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
-                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+            for suggestion_edit in suggestion_edits {
+                let old_start = old_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.old.start);
+                let old_end = old_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.old.end);
+                let new_start = new_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.new.start);
+                let new_end = new_snapshot
+                    .suggestion_snapshot
+                    .to_point(suggestion_edit.new.end);
                 tab_edits.push(TabEdit {
                     old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
                     new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
@@ -101,27 +155,26 @@ impl TabMap {
 
 #[derive(Clone)]
 pub struct TabSnapshot {
-    pub fold_snapshot: FoldSnapshot,
+    pub suggestion_snapshot: SuggestionSnapshot,
     pub tab_size: NonZeroU32,
+    pub max_expansion_column: u32,
     pub version: usize,
 }
 
 impl TabSnapshot {
     pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        self.fold_snapshot.buffer_snapshot()
+        self.suggestion_snapshot.buffer_snapshot()
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
         let max_point = self.max_point();
         if row < max_point.row() {
-            self.chunks(
-                TabPoint::new(row, 0)..TabPoint::new(row + 1, 0),
-                false,
-                None,
-            )
-            .map(|chunk| chunk.text.len() as u32)
-            .sum::<u32>()
-                - 1
+            self.to_tab_point(SuggestionPoint::new(
+                row,
+                self.suggestion_snapshot.line_len(row),
+            ))
+            .0
+            .column
         } else {
             max_point.column()
         }
@@ -132,10 +185,10 @@ impl TabSnapshot {
     }
 
     pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
-        let input_start = self.to_fold_point(range.start, Bias::Left).0;
-        let input_end = self.to_fold_point(range.end, Bias::Right).0;
+        let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
+        let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
         let input_summary = self
-            .fold_snapshot
+            .suggestion_snapshot
             .text_summary_for_range(input_start..input_end);
 
         let mut first_line_chars = 0;
@@ -145,7 +198,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false, None)
+            .chunks(range.start..line_end, false, None, None)
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -159,7 +212,12 @@ impl TabSnapshot {
             last_line_chars = first_line_chars;
         } else {
             for _ in self
-                .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None)
+                .chunks(
+                    TabPoint::new(range.end.row(), 0)..range.end,
+                    false,
+                    None,
+                    None,
+                )
                 .flat_map(|chunk| chunk.text.chars())
             {
                 last_line_chars += 1;
@@ -180,120 +238,133 @@ impl TabSnapshot {
         range: Range<TabPoint>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
-            self.to_fold_point(range.start, Bias::Left);
-        let input_start = input_start.to_offset(&self.fold_snapshot);
+            self.to_suggestion_point(range.start, Bias::Left);
+        let input_column = input_start.column();
+        let input_start = self.suggestion_snapshot.to_offset(input_start);
         let input_end = self
-            .to_fold_point(range.end, Bias::Right)
-            .0
-            .to_offset(&self.fold_snapshot);
-        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
-            (range.end.column() - range.start.column()) as usize
+            .suggestion_snapshot
+            .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
+        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
+            range.end.column() - range.start.column()
         } else {
             to_next_stop
         };
 
         TabChunks {
-            fold_chunks: self.fold_snapshot.chunks(
+            suggestion_chunks: self.suggestion_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
                 text_highlights,
+                suggestion_highlight,
             ),
+            input_column,
             column: expanded_char_column,
+            max_expansion_column: self.max_expansion_column,
             output_position: range.start.0,
             max_output_position: range.end.0,
             tab_size: self.tab_size,
             chunk: Chunk {
-                text: &SPACES[0..to_next_stop],
+                text: &SPACES[0..(to_next_stop as usize)],
                 ..Default::default()
             },
-            skip_leading_tab: to_next_stop > 0,
+            inside_leading_tab: to_next_stop > 0,
         }
     }
 
-    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows {
-        self.fold_snapshot.buffer_rows(row)
+    pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
+        self.suggestion_snapshot.buffer_rows(row)
     }
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false, None)
+        self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
             .map(|chunk| chunk.text)
             .collect()
     }
 
     pub fn max_point(&self) -> TabPoint {
-        self.to_tab_point(self.fold_snapshot.max_point())
+        self.to_tab_point(self.suggestion_snapshot.max_point())
     }
 
     pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
         self.to_tab_point(
-            self.fold_snapshot
-                .clip_point(self.to_fold_point(point, bias).0, bias),
+            self.suggestion_snapshot
+                .clip_point(self.to_suggestion_point(point, bias).0, bias),
         )
     }
 
-    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
-        let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
-        let expanded = Self::expand_tabs(chars, input.column() as usize, self.tab_size);
-        TabPoint::new(input.row(), expanded as u32)
+    pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
+        let chars = self
+            .suggestion_snapshot
+            .chars_at(SuggestionPoint::new(input.row(), 0));
+        let expanded = self.expand_tabs(chars, input.column());
+        TabPoint::new(input.row(), expanded)
     }
 
-    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
-        let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
-        let expanded = output.column() as usize;
+    pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
+        let chars = self
+            .suggestion_snapshot
+            .chars_at(SuggestionPoint::new(output.row(), 0));
+        let expanded = output.column();
         let (collapsed, expanded_char_column, to_next_stop) =
-            Self::collapse_tabs(chars, expanded, bias, self.tab_size);
+            self.collapse_tabs(chars, expanded, bias);
         (
-            FoldPoint::new(output.row(), collapsed as u32),
+            SuggestionPoint::new(output.row(), collapsed as u32),
             expanded_char_column,
             to_next_stop,
         )
     }
 
     pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
-        self.to_tab_point(self.fold_snapshot.to_fold_point(point, bias))
+        let fold_point = self
+            .suggestion_snapshot
+            .fold_snapshot
+            .to_fold_point(point, bias);
+        let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
+        self.to_tab_point(suggestion_point)
     }
 
     pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
-        self.to_fold_point(point, bias)
-            .0
-            .to_buffer_point(&self.fold_snapshot)
+        let suggestion_point = self.to_suggestion_point(point, bias).0;
+        let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
+        fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
     }
 
-    fn expand_tabs(
-        chars: impl Iterator<Item = char>,
-        column: usize,
-        tab_size: NonZeroU32,
-    ) -> usize {
+    fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
+        let tab_size = self.tab_size.get();
+
         let mut expanded_chars = 0;
         let mut expanded_bytes = 0;
         let mut collapsed_bytes = 0;
+        let end_column = column.min(self.max_expansion_column);
         for c in chars {
-            if collapsed_bytes == column {
+            if collapsed_bytes >= end_column {
                 break;
             }
             if c == '\t' {
-                let tab_size = tab_size.get() as usize;
                 let tab_len = tab_size - expanded_chars % tab_size;
                 expanded_bytes += tab_len;
                 expanded_chars += tab_len;
             } else {
-                expanded_bytes += c.len_utf8();
+                expanded_bytes += c.len_utf8() as u32;
                 expanded_chars += 1;
             }
-            collapsed_bytes += c.len_utf8();
+            collapsed_bytes += c.len_utf8() as u32;
         }
-        expanded_bytes
+        expanded_bytes + column.saturating_sub(collapsed_bytes)
     }
 
     fn collapse_tabs(
+        &self,
         chars: impl Iterator<Item = char>,
-        column: usize,
+        column: u32,
         bias: Bias,
-        tab_size: NonZeroU32,
-    ) -> (usize, usize, usize) {
+    ) -> (u32, u32, u32) {
+        let tab_size = self.tab_size.get();
+
         let mut expanded_bytes = 0;
         let mut expanded_chars = 0;
         let mut collapsed_bytes = 0;
@@ -301,9 +372,11 @@ impl TabSnapshot {
             if expanded_bytes >= column {
                 break;
             }
+            if collapsed_bytes >= self.max_expansion_column {
+                break;
+            }
 
             if c == '\t' {
-                let tab_size = tab_size.get() as usize;
                 let tab_len = tab_size - (expanded_chars % tab_size);
                 expanded_chars += tab_len;
                 expanded_bytes += tab_len;
@@ -316,7 +389,7 @@ impl TabSnapshot {
                 }
             } else {
                 expanded_chars += 1;
-                expanded_bytes += c.len_utf8();
+                expanded_bytes += c.len_utf8() as u32;
             }
 
             if expanded_bytes > column && matches!(bias, Bias::Left) {
@@ -324,9 +397,13 @@ impl TabSnapshot {
                 break;
             }
 
-            collapsed_bytes += c.len_utf8();
+            collapsed_bytes += c.len_utf8() as u32;
         }
-        (collapsed_bytes, expanded_chars, 0)
+        (
+            collapsed_bytes + column.saturating_sub(expanded_bytes),
+            expanded_chars,
+            0,
+        )
     }
 }
 
@@ -412,13 +489,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
 const SPACES: &str = "                ";
 
 pub struct TabChunks<'a> {
-    fold_chunks: fold_map::FoldChunks<'a>,
+    suggestion_chunks: SuggestionChunks<'a>,
     chunk: Chunk<'a>,
-    column: usize,
+    column: u32,
+    max_expansion_column: u32,
     output_position: Point,
+    input_column: u32,
     max_output_position: Point,
     tab_size: NonZeroU32,
-    skip_leading_tab: bool,
+    inside_leading_tab: bool,
 }
 
 impl<'a> Iterator for TabChunks<'a> {
@@ -426,11 +505,12 @@ impl<'a> Iterator for TabChunks<'a> {
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.chunk.text.is_empty() {
-            if let Some(chunk) = self.fold_chunks.next() {
+            if let Some(chunk) = self.suggestion_chunks.next() {
                 self.chunk = chunk;
-                if self.skip_leading_tab {
+                if self.inside_leading_tab {
                     self.chunk.text = &self.chunk.text[1..];
-                    self.skip_leading_tab = false;
+                    self.inside_leading_tab = false;
+                    self.input_column += 1;
                 }
             } else {
                 return None;
@@ -449,27 +529,36 @@ impl<'a> Iterator for TabChunks<'a> {
                         });
                     } else {
                         self.chunk.text = &self.chunk.text[1..];
-                        let tab_size = self.tab_size.get() as u32;
-                        let mut len = tab_size - self.column as u32 % tab_size;
+                        let tab_size = if self.input_column < self.max_expansion_column {
+                            self.tab_size.get() as u32
+                        } else {
+                            1
+                        };
+                        let mut len = tab_size - self.column % tab_size;
                         let next_output_position = cmp::min(
                             self.output_position + Point::new(0, len),
                             self.max_output_position,
                         );
                         len = next_output_position.column - self.output_position.column;
-                        self.column += len as usize;
+                        self.column += len;
+                        self.input_column += 1;
                         self.output_position = next_output_position;
                         return Some(Chunk {
-                            text: &SPACES[0..len as usize],
+                            text: &SPACES[..len as usize],
                             ..self.chunk
                         });
                     }
                 }
                 '\n' => {
                     self.column = 0;
+                    self.input_column = 0;
                     self.output_position += Point::new(1, 0);
                 }
                 _ => {
                     self.column += 1;
+                    if !self.inside_leading_tab {
+                        self.input_column += c.len_utf8() as u32;
+                    }
                     self.output_position.column += c.len_utf8() as u32;
                 }
             }
@@ -482,23 +571,89 @@ impl<'a> Iterator for TabChunks<'a> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{display_map::fold_map::FoldMap, MultiBuffer};
+    use crate::{
+        display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
+        MultiBuffer,
+    };
     use rand::{prelude::StdRng, Rng};
 
-    #[test]
-    fn test_expand_tabs() {
-        assert_eq!(
-            TabSnapshot::expand_tabs("\t".chars(), 0, 4.try_into().unwrap()),
-            0
-        );
-        assert_eq!(
-            TabSnapshot::expand_tabs("\t".chars(), 1, 4.try_into().unwrap()),
-            4
-        );
-        assert_eq!(
-            TabSnapshot::expand_tabs("\ta".chars(), 2, 4.try_into().unwrap()),
-            5
-        );
+    #[gpui::test]
+    fn test_expand_tabs(cx: &mut gpui::MutableAppContext) {
+        let buffer = MultiBuffer::build_simple("", cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
+        assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
+        assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
+    }
+
+    #[gpui::test]
+    fn test_long_lines(cx: &mut gpui::MutableAppContext) {
+        let max_expansion_column = 12;
+        let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
+        let output = "A   BC  DEF G   HI J K L M";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        tab_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(tab_snapshot.text(), output);
+
+        for (ix, c) in input.char_indices() {
+            assert_eq!(
+                tab_snapshot
+                    .chunks(
+                        TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
+                        false,
+                        None,
+                        None,
+                    )
+                    .map(|c| c.text)
+                    .collect::<String>(),
+                &output[ix..],
+                "text from index {ix}"
+            );
+
+            if c != '\t' {
+                let input_point = Point::new(0, ix as u32);
+                let output_point = Point::new(0, output.find(c).unwrap() as u32);
+                assert_eq!(
+                    tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
+                    TabPoint(output_point),
+                    "to_tab_point({input_point:?})"
+                );
+                assert_eq!(
+                    tab_snapshot
+                        .to_suggestion_point(TabPoint(output_point), Bias::Left)
+                        .0,
+                    SuggestionPoint(input_point),
+                    "to_suggestion_point({output_point:?})"
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    fn test_long_lines_with_character_spanning_max_expansion_column(
+        cx: &mut gpui::MutableAppContext,
+    ) {
+        let max_expansion_column = 8;
+        let input = "abcdefg⋯hij";
+
+        let buffer = MultiBuffer::build_simple(input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        tab_snapshot.max_expansion_column = max_expansion_column;
+        assert_eq!(tab_snapshot.text(), input);
     }
 
     #[gpui::test(iterations = 100)]
@@ -518,10 +673,15 @@ mod tests {
 
         let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
         fold_map.randomly_mutate(&mut rng);
-        let (folds_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
-        log::info!("FoldMap text: {:?}", folds_snapshot.text());
+        let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
+        log::info!("FoldMap text: {:?}", fold_snapshot.text());
+        let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
+        let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
+        log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+
+        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
 
-        let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
         let text = text::Rope::from(tabs_snapshot.text().as_str());
         log::info!(
             "TabMap text (tab size: {}): {:?}",
@@ -546,18 +706,18 @@ mod tests {
                 .collect::<String>();
             let expected_summary = TextSummary::from(expected_text.as_str());
             assert_eq!(
-                expected_text,
                 tabs_snapshot
-                    .chunks(start..end, false, None)
+                    .chunks(start..end, false, None, None)
                     .map(|c| c.text)
                     .collect::<String>(),
+                expected_text,
                 "chunks({:?}..{:?})",
                 start,
                 end
             );
 
             let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
-            if tab_size.get() > 1 && folds_snapshot.text().contains('\t') {
+            if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
                 actual_summary.longest_row = expected_summary.longest_row;
                 actual_summary.longest_row_chars = expected_summary.longest_row_chars;
             }
@@ -565,7 +725,11 @@ mod tests {
         }
 
         for row in 0..=text.max_point().row {
-            assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
+            assert_eq!(
+                tabs_snapshot.line_len(row),
+                text.line_len(row),
+                "line_len({row})"
+            );
         }
     }
 }

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

@@ -1,12 +1,13 @@
 use super::{
-    fold_map,
+    suggestion_map::SuggestionBufferRows,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
     TextHighlights,
 };
 use crate::MultiBufferSnapshot;
 use gpui::{
-    fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
-    Task,
+    fonts::{FontId, HighlightStyle},
+    text_layout::LineWrapper,
+    Entity, ModelContext, ModelHandle, MutableAppContext, Task,
 };
 use language::{Chunk, Point};
 use lazy_static::lazy_static;
@@ -64,7 +65,7 @@ pub struct WrapChunks<'a> {
 
 #[derive(Clone)]
 pub struct WrapBufferRows<'a> {
-    input_buffer_rows: fold_map::FoldBufferRows<'a>,
+    input_buffer_rows: SuggestionBufferRows<'a>,
     input_buffer_row: Option<u32>,
     output_row: u32,
     soft_wrapped: bool,
@@ -444,6 +445,7 @@ impl WrapSnapshot {
                     TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
                     false,
                     None,
+                    None,
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -573,6 +575,7 @@ impl WrapSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
@@ -590,6 +593,7 @@ impl WrapSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
+                suggestion_highlight,
             ),
             input_chunk: Default::default(),
             output_position: output_start,
@@ -755,16 +759,24 @@ impl WrapSnapshot {
             let text = language::Rope::from(self.text().as_str());
             let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
             let mut expected_buffer_rows = Vec::new();
-            let mut prev_tab_row = 0;
+            let mut prev_fold_row = 0;
             for display_row in 0..=self.max_point().row() {
                 let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
-                if tab_point.row() == prev_tab_row && display_row != 0 {
+                let suggestion_point = self
+                    .tab_snapshot
+                    .to_suggestion_point(tab_point, Bias::Left)
+                    .0;
+                let fold_point = self
+                    .tab_snapshot
+                    .suggestion_snapshot
+                    .to_fold_point(suggestion_point);
+                if fold_point.row() == prev_fold_row && display_row != 0 {
                     expected_buffer_rows.push(None);
                 } else {
-                    let fold_point = self.tab_snapshot.to_fold_point(tab_point, Bias::Left).0;
-                    let buffer_point = fold_point.to_buffer_point(&self.tab_snapshot.fold_snapshot);
+                    let buffer_point = fold_point
+                        .to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
                     expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
-                    prev_tab_row = tab_point.row();
+                    prev_fold_row = fold_point.row();
                 }
 
                 assert_eq!(self.line_len(display_row), text.line_len(display_row));
@@ -1026,7 +1038,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
 mod tests {
     use super::*;
     use crate::{
-        display_map::{fold_map::FoldMap, tab_map::TabMap},
+        display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
         MultiBuffer,
     };
     use gpui::test::observe;
@@ -1053,7 +1065,9 @@ mod tests {
             Some(rng.gen_range(0.0..=1000.0))
         };
         let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
-        let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family_id = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -1074,14 +1088,14 @@ mod tests {
             }
         });
         let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
-        let (mut fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
-        log::info!("Unwrapped text (no folds): {:?}", buffer_snapshot.text());
-        log::info!(
-            "Unwrapped text (unexpanded tabs): {:?}",
-            folds_snapshot.text()
-        );
-        log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
+        log::info!("Buffer text: {:?}", buffer_snapshot.text());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        log::info!("FoldMap text: {:?}", fold_snapshot.text());
+        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
+        log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let tabs_snapshot = tab_map.set_max_expansion_column(32);
+        log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
         let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
         let unwrapped_text = tabs_snapshot.text();
@@ -1124,9 +1138,11 @@ mod tests {
                     wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
                 }
                 20..=39 => {
-                    for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+                    for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+                        let (suggestion_snapshot, suggestion_edits) =
+                            suggestion_map.sync(fold_snapshot, fold_edits);
                         let (tabs_snapshot, tab_edits) =
-                            tab_map.sync(folds_snapshot, fold_edits, tab_size);
+                            tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
                         let (mut snapshot, wrap_edits) =
                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
@@ -1134,6 +1150,17 @@ mod tests {
                         edits.push((snapshot, wrap_edits));
                     }
                 }
+                40..=59 => {
+                    let (suggestion_snapshot, suggestion_edits) =
+                        suggestion_map.randomly_mutate(&mut rng);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                    let (mut snapshot, wrap_edits) =
+                        wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+                    snapshot.check_invariants();
+                    snapshot.verify_chunks(&mut rng);
+                    edits.push((snapshot, wrap_edits));
+                }
                 _ => {
                     buffer.update(cx, |buffer, cx| {
                         let subscription = buffer.subscribe();
@@ -1145,14 +1172,15 @@ mod tests {
                 }
             }
 
-            log::info!("Unwrapped text (no folds): {:?}", buffer_snapshot.text());
-            let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            log::info!(
-                "Unwrapped text (unexpanded tabs): {:?}",
-                folds_snapshot.text()
-            );
-            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
-            log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
+            log::info!("Buffer text: {:?}", buffer_snapshot.text());
+            let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
+            log::info!("FoldMap text: {:?}", fold_snapshot.text());
+            let (suggestion_snapshot, suggestion_edits) =
+                suggestion_map.sync(fold_snapshot, fold_edits);
+            log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+            let (tabs_snapshot, tab_edits) =
+                tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+            log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();
             let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
@@ -1199,7 +1227,7 @@ mod tests {
                 if tab_size.get() == 1
                     || !wrapped_snapshot
                         .tab_snapshot
-                        .fold_snapshot
+                        .suggestion_snapshot
                         .text()
                         .contains('\t')
                 {
@@ -1292,7 +1320,7 @@ mod tests {
         }
 
         pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-            self.chunks(wrap_row..self.max_point().row() + 1, false, None)
+            self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
                 .map(|h| h.text)
         }
 
@@ -1316,7 +1344,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true, None)
+                    .chunks(start_row..end_row, true, None, None)
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/editor.rs 🔗

@@ -1,6 +1,7 @@
 mod blink_manager;
 pub mod display_map;
 mod element;
+
 mod git;
 mod highlight_matching_bracket;
 mod hover_popover;
@@ -23,6 +24,7 @@ use anyhow::Result;
 use blink_manager::BlinkManager;
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
@@ -38,7 +40,7 @@ use gpui::{
     impl_actions, impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::CursorStyle,
-    serde_json::json,
+    serde_json::{self, json},
     AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
@@ -84,7 +86,7 @@ use std::{
 };
 pub use sum_tree::Bias;
 use theme::{DiagnosticStyle, Theme};
-use util::{post_inc, ResultExt, TryFutureExt, RangeExt};
+use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
 
 use crate::git::diff_hunk_to_display;
@@ -160,6 +162,21 @@ pub struct ToggleComments {
     pub advance_downwards: bool,
 }
 
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct FoldAt {
+    pub buffer_row: u32,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct UnfoldAt {
+    pub buffer_row: u32,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct GutterHover {
+    pub hovered: bool,
+}
+
 actions!(
     editor,
     [
@@ -241,7 +258,9 @@ actions!(
         RestartLanguageServer,
         Hover,
         Format,
-        ToggleSoftWrap
+        ToggleSoftWrap,
+        RevealInFinder,
+        CopyHighlightJson
     ]
 );
 
@@ -257,6 +276,9 @@ impl_actions!(
         ConfirmCompletion,
         ConfirmCodeAction,
         ToggleComments,
+        FoldAt,
+        UnfoldAt,
+        GutterHover
     ]
 );
 
@@ -347,13 +369,18 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::go_to_definition);
     cx.add_action(Editor::go_to_type_definition);
     cx.add_action(Editor::fold);
+    cx.add_action(Editor::fold_at);
     cx.add_action(Editor::unfold_lines);
+    cx.add_action(Editor::unfold_at);
+    cx.add_action(Editor::gutter_hover);
     cx.add_action(Editor::fold_selected_ranges);
     cx.add_action(Editor::show_completions);
     cx.add_action(Editor::toggle_code_actions);
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::jump);
     cx.add_action(Editor::toggle_soft_wrap);
+    cx.add_action(Editor::reveal_in_finder);
+    cx.add_action(Editor::copy_highlight_json);
     cx.add_async_action(Editor::format);
     cx.add_action(Editor::restart_language_server);
     cx.add_action(Editor::show_character_palette);
@@ -362,6 +389,9 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Editor::rename);
     cx.add_async_action(Editor::confirm_rename);
     cx.add_async_action(Editor::find_all_references);
+    cx.add_action(Editor::next_copilot_suggestion);
+    cx.add_action(Editor::previous_copilot_suggestion);
+    cx.add_action(Editor::toggle_copilot_suggestions);
 
     hover_popover::init(cx);
     link_go_to_definition::init(cx);
@@ -478,7 +508,9 @@ pub struct Editor {
     leader_replica_id: Option<u16>,
     remote_id: Option<ViewId>,
     hover_state: HoverState,
+    gutter_hovered: bool,
     link_go_to_definition_state: LinkGoToDefinitionState,
+    pub copilot_state: CopilotState,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -976,6 +1008,76 @@ impl CodeActionsMenu {
     }
 }
 
+pub struct CopilotState {
+    excerpt_id: Option<ExcerptId>,
+    pending_refresh: Task<Option<()>>,
+    completions: Vec<copilot::Completion>,
+    active_completion_index: usize,
+    pub user_enabled: Option<bool>,
+}
+
+impl Default for CopilotState {
+    fn default() -> Self {
+        Self {
+            excerpt_id: None,
+            pending_refresh: Task::ready(Some(())),
+            completions: Default::default(),
+            active_completion_index: 0,
+            user_enabled: None,
+        }
+    }
+}
+
+impl CopilotState {
+    fn text_for_active_completion(
+        &self,
+        cursor: Anchor,
+        buffer: &MultiBufferSnapshot,
+    ) -> Option<&str> {
+        use language::ToOffset as _;
+
+        let completion = self.completions.get(self.active_completion_index)?;
+        let excerpt_id = self.excerpt_id?;
+        let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
+
+        let mut completion_range = completion.range.to_offset(&completion_buffer);
+        let prefix_len = Self::common_prefix(
+            completion_buffer.chars_for_range(completion_range.clone()),
+            completion.text.chars(),
+        );
+        completion_range.start += prefix_len;
+        let suffix_len = Self::common_prefix(
+            completion_buffer.reversed_chars_for_range(completion_range.clone()),
+            completion.text[prefix_len..].chars().rev(),
+        );
+        completion_range.end = completion_range.end.saturating_sub(suffix_len);
+
+        if completion_range.is_empty()
+            && completion_range.start == cursor.text_anchor.to_offset(&completion_buffer)
+        {
+            Some(&completion.text[prefix_len..completion.text.len() - suffix_len])
+        } else {
+            None
+        }
+    }
+
+    fn push_completion(&mut self, new_completion: copilot::Completion) {
+        for completion in &self.completions {
+            if *completion == new_completion {
+                return;
+            }
+        }
+        self.completions.push(new_completion);
+    }
+
+    fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
+        a.zip(b)
+            .take_while(|(a, b)| a == b)
+            .map(|(a, _)| a.len_utf8())
+            .sum()
+    }
+}
+
 #[derive(Debug)]
 struct ActiveDiagnosticGroup {
     primary_range: Range<Anchor>,
@@ -1149,6 +1251,8 @@ impl Editor {
             remote_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
+            copilot_state: Default::default(),
+            gutter_hovered: false,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
                 cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1228,6 +1332,15 @@ impl Editor {
         self.buffer.read(cx).language_at(point, cx)
     }
 
+    pub fn active_excerpt(
+        &self,
+        cx: &AppContext,
+    ) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
+        self.buffer
+            .read(cx)
+            .excerpt_containing(self.selections.newest_anchor().head(), cx)
+    }
+
     fn style(&self, cx: &AppContext) -> EditorStyle {
         build_style(
             cx.global::<Settings>(),
@@ -1348,6 +1461,7 @@ impl Editor {
             self.refresh_code_actions(cx);
             self.refresh_document_highlights(cx);
             refresh_matching_bracket_highlights(self, cx);
+            self.refresh_copilot_suggestions(cx);
         }
 
         self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -1721,6 +1835,10 @@ impl Editor {
             return;
         }
 
+        if self.clear_copilot_suggestions(cx) {
+            return;
+        }
+
         if self.snippet_stack.pop().is_some() {
             return;
         }
@@ -1761,8 +1879,8 @@ impl Editor {
                 // bracket of any of this language's bracket pairs.
                 let mut bracket_pair = None;
                 let mut is_bracket_pair_start = false;
-                for pair in language.brackets() {
-                    if pair.close && pair.start.ends_with(text.as_ref()) {
+                for (pair, enabled) in language.brackets() {
+                    if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
                         bracket_pair = Some(pair.clone());
                         is_bracket_pair_start = true;
                         break;
@@ -1930,11 +2048,12 @@ impl Editor {
                                 .map(|c| c.len_utf8())
                                 .sum::<usize>();
 
-                            insert_extra_newline = language.brackets().iter().any(|pair| {
+                            insert_extra_newline = language.brackets().any(|(pair, enabled)| {
                                 let pair_start = pair.start.trim_end();
                                 let pair_end = pair.end.trim_start();
 
-                                pair.newline
+                                enabled
+                                    && pair.newline
                                     && buffer
                                         .contains_str_at(end + trailing_whitespace_len, pair_end)
                                     && buffer.contains_str_at(
@@ -2023,6 +2142,21 @@ impl Editor {
     }
 
     pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+        self.insert_with_autoindent_mode(
+            text,
+            Some(AutoindentMode::Block {
+                original_indent_columns: Vec::new(),
+            }),
+            cx,
+        );
+    }
+
+    fn insert_with_autoindent_mode(
+        &mut self,
+        text: &str,
+        autoindent_mode: Option<AutoindentMode>,
+        cx: &mut ViewContext<Self>,
+    ) {
         let text: Arc<str> = text.into();
         self.transact(cx, |this, cx| {
             let old_selections = this.selections.all_adjusted(cx);
@@ -2041,9 +2175,7 @@ impl Editor {
                     old_selections
                         .iter()
                         .map(|s| (s.start..s.end, text.clone())),
-                    Some(AutoindentMode::Block {
-                        original_indent_columns: Vec::new(),
-                    }),
+                    autoindent_mode,
                     cx,
                 );
                 anchors
@@ -2056,7 +2188,9 @@ impl Editor {
     }
 
     fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
-        if !cx.global::<Settings>().show_completions_on_input {
+        if !cx.global::<Settings>().show_completions_on_input
+            || self.has_active_copilot_suggestion(cx)
+        {
             return;
         }
 
@@ -2241,11 +2375,11 @@ impl Editor {
                         }
 
                         this.completion_tasks.retain(|(id, _)| *id > menu.id);
-                        if this.focused {
+                        if this.focused && !menu.matches.is_empty() {
                             this.show_context_menu(ContextMenu::Completions(menu), cx);
+                        } else {
+                            this.hide_context_menu(cx);
                         }
-
-                        cx.notify();
                     });
                 }
                 Ok::<_, anyhow::Error>(())
@@ -2639,17 +2773,224 @@ impl Editor {
         None
     }
 
+    fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let copilot = Copilot::global(cx)?;
+
+        if self.mode != EditorMode::Full {
+            return None;
+        }
+
+        let settings = cx.global::<Settings>();
+
+        if !self
+            .copilot_state
+            .user_enabled
+            .unwrap_or_else(|| settings.copilot_on(None))
+        {
+            return None;
+        }
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let selection = self.selections.newest_anchor();
+
+        if !self.copilot_state.user_enabled.is_some() {
+            let language_name = snapshot
+                .language_at(selection.start)
+                .map(|language| language.name());
+
+            let copilot_enabled = settings.copilot_on(language_name.as_deref());
+
+            if !copilot_enabled {
+                return None;
+            }
+        }
+
+        let cursor = if selection.start == selection.end {
+            selection.start.bias_left(&snapshot)
+        } else {
+            return None;
+        };
+        self.refresh_active_copilot_suggestion(cx);
+
+        if !copilot.read(cx).status().is_authorized() {
+            return None;
+        }
+
+        let (buffer, buffer_position) =
+            self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+        self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
+            let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
+                (
+                    copilot.completions(&buffer, buffer_position, cx),
+                    copilot.completions_cycling(&buffer, buffer_position, cx),
+                )
+            });
+
+            let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
+            let mut completions = Vec::new();
+            completions.extend(completion.log_err().into_iter().flatten());
+            completions.extend(completions_cycling.log_err().into_iter().flatten());
+            this.upgrade(&cx)?.update(&mut cx, |this, cx| {
+                if !completions.is_empty() {
+                    this.copilot_state.completions.clear();
+                    this.copilot_state.active_completion_index = 0;
+                    this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
+                    for completion in completions {
+                        this.copilot_state.push_completion(completion);
+                    }
+                    this.refresh_active_copilot_suggestion(cx);
+                }
+            });
+
+            Some(())
+        });
+
+        Some(())
+    }
+
+    fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+        // Auto re-enable copilot if you're asking for a suggestion
+        if self.copilot_state.user_enabled == Some(false) {
+            cx.notify();
+            self.copilot_state.user_enabled = Some(true);
+        }
+
+        if self.copilot_state.completions.is_empty() {
+            self.refresh_copilot_suggestions(cx);
+            return;
+        }
+
+        self.copilot_state.active_completion_index =
+            (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
+
+        self.refresh_active_copilot_suggestion(cx);
+    }
+
+    fn previous_copilot_suggestion(
+        &mut self,
+        _: &copilot::PreviousSuggestion,
+        cx: &mut ViewContext<Self>,
+    ) {
+        // Auto re-enable copilot if you're asking for a suggestion
+        if self.copilot_state.user_enabled == Some(false) {
+            cx.notify();
+            self.copilot_state.user_enabled = Some(true);
+        }
+
+        if self.copilot_state.completions.is_empty() {
+            self.refresh_copilot_suggestions(cx);
+            return;
+        }
+
+        self.copilot_state.active_completion_index =
+            if self.copilot_state.active_completion_index == 0 {
+                self.copilot_state.completions.len() - 1
+            } else {
+                self.copilot_state.active_completion_index - 1
+            };
+
+        self.refresh_active_copilot_suggestion(cx);
+    }
+
+    fn toggle_copilot_suggestions(&mut self, _: &copilot::Toggle, cx: &mut ViewContext<Self>) {
+        self.copilot_state.user_enabled = match self.copilot_state.user_enabled {
+            Some(enabled) => Some(!enabled),
+            None => {
+                let selection = self.selections.newest_anchor().start;
+
+                let language_name = self
+                    .snapshot(cx)
+                    .language_at(selection)
+                    .map(|language| language.name());
+
+                let copilot_enabled = cx.global::<Settings>().copilot_on(language_name.as_deref());
+
+                Some(!copilot_enabled)
+            }
+        };
+
+        // We know this can't be None, as we just set it to Some above
+        if self.copilot_state.user_enabled == Some(true) {
+            self.refresh_copilot_suggestions(cx);
+        } else {
+            self.clear_copilot_suggestions(cx);
+        }
+
+        cx.notify();
+    }
+
+    fn refresh_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let cursor = self.selections.newest_anchor().head();
+
+        if self.context_menu.is_some() {
+            self.display_map
+                .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+            cx.notify();
+        } else if let Some(text) = self
+            .copilot_state
+            .text_for_active_completion(cursor, &snapshot)
+        {
+            self.display_map.update(cx, |map, cx| {
+                map.replace_suggestion(
+                    Some(Suggestion {
+                        position: cursor,
+                        text: text.into(),
+                    }),
+                    cx,
+                )
+            });
+            cx.notify();
+        } else if self.has_active_copilot_suggestion(cx) {
+            self.display_map
+                .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+            cx.notify();
+        }
+    }
+
+    fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let cursor = self.selections.newest_anchor().head();
+        if let Some(text) = self
+            .copilot_state
+            .text_for_active_completion(cursor, &snapshot)
+        {
+            self.insert_with_autoindent_mode(&text.to_string(), None, cx);
+            self.clear_copilot_suggestions(cx);
+            true
+        } else {
+            false
+        }
+    }
+
+    fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.display_map
+            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+        let was_empty = self.copilot_state.completions.is_empty();
+        self.copilot_state.completions.clear();
+        self.copilot_state.active_completion_index = 0;
+        self.copilot_state.pending_refresh = Task::ready(None);
+        self.copilot_state.excerpt_id = None;
+        cx.notify();
+        !was_empty
+    }
+
+    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+        self.display_map.read(cx).has_suggestion()
+    }
+
     pub fn render_code_actions_indicator(
         &self,
         style: &EditorStyle,
+        active: bool,
         cx: &mut RenderContext<Self>,
     ) -> Option<ElementBox> {
         if self.available_code_actions.is_some() {
-            enum Tag {}
+            enum CodeActions {}
             Some(
-                MouseEventHandler::<Tag>::new(0, cx, |_, _| {
+                MouseEventHandler::<CodeActions>::new(0, cx, |state, _| {
                     Svg::new("icons/bolt_8.svg")
-                        .with_color(style.code_actions.indicator)
+                        .with_color(style.code_actions.indicator.style_for(state, active).color)
                         .boxed()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
@@ -2666,6 +3007,71 @@ impl Editor {
         }
     }
 
+    pub fn render_fold_indicators(
+        &self,
+        fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
+        style: &EditorStyle,
+        gutter_hovered: bool,
+        line_height: f32,
+        gutter_margin: f32,
+        cx: &mut RenderContext<Self>,
+    ) -> Vec<Option<ElementBox>> {
+        enum FoldIndicators {}
+
+        let style = style.folds.clone();
+
+        fold_data
+            .iter()
+            .enumerate()
+            .map(|(ix, fold_data)| {
+                fold_data
+                    .map(|(fold_status, buffer_row, active)| {
+                        (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
+                            MouseEventHandler::<FoldIndicators>::new(
+                                ix as usize,
+                                cx,
+                                |mouse_state, _| -> ElementBox {
+                                    Svg::new(match fold_status {
+                                        FoldStatus::Folded => style.folded_icon.clone(),
+                                        FoldStatus::Foldable => style.foldable_icon.clone(),
+                                    })
+                                    .with_color(
+                                        style
+                                            .indicator
+                                            .style_for(
+                                                mouse_state,
+                                                fold_status == FoldStatus::Folded,
+                                            )
+                                            .color,
+                                    )
+                                    .constrained()
+                                    .with_width(gutter_margin * style.icon_margin_scale)
+                                    .aligned()
+                                    .constrained()
+                                    .with_height(line_height)
+                                    .with_width(gutter_margin)
+                                    .aligned()
+                                    .boxed()
+                                },
+                            )
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .with_padding(Padding::uniform(3.))
+                            .on_click(MouseButton::Left, {
+                                move |_, cx| {
+                                    cx.dispatch_any_action(match fold_status {
+                                        FoldStatus::Folded => Box::new(UnfoldAt { buffer_row }),
+                                        FoldStatus::Foldable => Box::new(FoldAt { buffer_row }),
+                                    });
+                                }
+                            })
+                            .boxed()
+                        })
+                    })
+                    .flatten()
+            })
+            .collect()
+    }
+
     pub fn context_menu_visible(&self) -> bool {
         self.context_menu
             .as_ref()
@@ -2688,13 +3094,16 @@ impl Editor {
             self.completion_tasks.clear();
         }
         self.context_menu = Some(menu);
+        self.refresh_active_copilot_suggestion(cx);
         cx.notify();
     }
 
     fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<ContextMenu> {
         cx.notify();
         self.completion_tasks.clear();
-        self.context_menu.take()
+        let context_menu = self.context_menu.take();
+        self.refresh_active_copilot_suggestion(cx);
+        context_menu
     }
 
     pub fn insert_snippet(
@@ -2880,6 +3289,10 @@ impl Editor {
     }
 
     pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        if self.accept_copilot_suggestion(cx) {
+            return;
+        }
+
         if self.move_to_next_snippet_tabstop(cx) {
             return;
         }
@@ -3248,26 +3661,12 @@ impl Editor {
 
         while let Some(selection) = selections.next() {
             // Find all the selections that span a contiguous row range
-            contiguous_row_selections.push(selection.clone());
-            let start_row = selection.start.row;
-            let mut end_row = if selection.end.column > 0 || selection.is_empty() {
-                display_map.next_line_boundary(selection.end).0.row + 1
-            } else {
-                selection.end.row
-            };
-
-            while let Some(next_selection) = selections.peek() {
-                if next_selection.start.row <= end_row {
-                    end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
-                        display_map.next_line_boundary(next_selection.end).0.row + 1
-                    } else {
-                        next_selection.end.row
-                    };
-                    contiguous_row_selections.push(selections.next().unwrap().clone());
-                } else {
-                    break;
-                }
-            }
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
 
             // Move the text spanned by the row range to be before the line preceding the row range
             if start_row > 0 {
@@ -3332,13 +3731,13 @@ impl Editor {
         }
 
         self.transact(cx, |this, cx| {
-            this.unfold_ranges(unfold_ranges, true, cx);
+            this.unfold_ranges(unfold_ranges, true, true, cx);
             this.buffer.update(cx, |buffer, cx| {
                 for (range, text) in edits {
                     buffer.edit([(range, text)], None, cx);
                 }
             });
-            this.fold_ranges(refold_ranges, cx);
+            this.fold_ranges(refold_ranges, true, cx);
             this.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.select(new_selections);
             })
@@ -3360,26 +3759,12 @@ impl Editor {
 
         while let Some(selection) = selections.next() {
             // Find all the selections that span a contiguous row range
-            contiguous_row_selections.push(selection.clone());
-            let start_row = selection.start.row;
-            let mut end_row = if selection.end.column > 0 || selection.is_empty() {
-                display_map.next_line_boundary(selection.end).0.row + 1
-            } else {
-                selection.end.row
-            };
-
-            while let Some(next_selection) = selections.peek() {
-                if next_selection.start.row <= end_row {
-                    end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
-                        display_map.next_line_boundary(next_selection.end).0.row + 1
-                    } else {
-                        next_selection.end.row
-                    };
-                    contiguous_row_selections.push(selections.next().unwrap().clone());
-                } else {
-                    break;
-                }
-            }
+            let (start_row, end_row) = consume_contiguous_rows(
+                &mut contiguous_row_selections,
+                selection,
+                &display_map,
+                &mut selections,
+            );
 
             // Move the text spanned by the row range to be after the last line of the row range
             if end_row <= buffer.max_point().row {
@@ -3437,13 +3822,13 @@ impl Editor {
         }
 
         self.transact(cx, |this, cx| {
-            this.unfold_ranges(unfold_ranges, true, cx);
+            this.unfold_ranges(unfold_ranges, true, true, cx);
             this.buffer.update(cx, |buffer, cx| {
                 for (range, text) in edits {
                     buffer.edit([(range, text)], None, cx);
                 }
             });
-            this.fold_ranges(refold_ranges, cx);
+            this.fold_ranges(refold_ranges, true, cx);
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
         });
     }
@@ -4271,7 +4656,7 @@ impl Editor {
                 to_unfold.push(selection.start..selection.end);
             }
         }
-        self.unfold_ranges(to_unfold, true, cx);
+        self.unfold_ranges(to_unfold, true, true, cx);
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.select_ranges(new_selection_ranges);
         });
@@ -4420,7 +4805,7 @@ impl Editor {
                 }
 
                 if let Some(next_selected_range) = next_selected_range {
-                    self.unfold_ranges([next_selected_range.clone()], false, cx);
+                    self.unfold_ranges([next_selected_range.clone()], false, true, cx);
                     self.change_selections(Some(Autoscroll::newest()), cx, |s| {
                         if action.replace_newest {
                             s.delete(s.newest_anchor().id);
@@ -4453,7 +4838,7 @@ impl Editor {
                     wordwise: true,
                     done: false,
                 };
-                self.unfold_ranges([selection.start..selection.end], false, cx);
+                self.unfold_ranges([selection.start..selection.end], false, true, cx);
                 self.change_selections(Some(Autoscroll::newest()), cx, |s| {
                     s.select(selections);
                 });
@@ -4790,8 +5175,10 @@ impl Editor {
     ) {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_offsets_with(|snapshot, selection| {
-                let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { return; };
-                
+                let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else {
+                    return;
+                };
+
                 let mut best_length = usize::MAX;
                 let mut best_inside = false;
                 let mut best_in_bracket_range = false;
@@ -4801,17 +5188,17 @@ impl Editor {
                     let length = close.end() - open.start;
                     let inside = selection.start >= open.end && selection.end <= *close.start();
                     let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head());
-                    
+
                     // If best is next to a bracket and current isn't, skip
                     if !in_bracket_range && best_in_bracket_range {
                         continue;
                     }
-                    
+
                     // Prefer smaller lengths unless best is inside and current isn't
                     if length > best_length && (best_inside || !inside) {
                         continue;
                     }
-                    
+
                     best_length = length;
                     best_inside = inside;
                     best_in_bracket_range = in_bracket_range;
@@ -4829,7 +5216,7 @@ impl Editor {
                         }
                     });
                 }
-                
+
                 if let Some(destination) = best_destination {
                     selection.collapse_to(destination, SelectionGoal::None);
                 }
@@ -5066,7 +5453,7 @@ impl Editor {
             GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
         });
 
-        cx.spawn(|workspace, mut cx| async move {
+        cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
             let definitions = definitions.await?;
             workspace.update(&mut cx, |workspace, cx| {
                 Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
@@ -5146,31 +5533,36 @@ impl Editor {
 
         let project = workspace.project().clone();
         let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
-        Some(cx.spawn(|workspace, mut cx| async move {
-            let locations = references.await?;
-            if locations.is_empty() {
-                return Ok(());
-            }
+        Some(cx.spawn_labeled(
+            "Finding All References...",
+            |workspace, mut cx| async move {
+                let locations = references.await?;
+                if locations.is_empty() {
+                    return Ok(());
+                }
 
-            workspace.update(&mut cx, |workspace, cx| {
-                let title = locations
-                    .first()
-                    .as_ref()
-                    .map(|location| {
-                        let buffer = location.buffer.read(cx);
-                        format!(
-                            "References to `{}`",
-                            buffer
-                                .text_for_range(location.range.clone())
-                                .collect::<String>()
-                        )
-                    })
-                    .unwrap();
-                Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
-            });
+                workspace.update(&mut cx, |workspace, cx| {
+                    let title = locations
+                        .first()
+                        .as_ref()
+                        .map(|location| {
+                            let buffer = location.buffer.read(cx);
+                            format!(
+                                "References to `{}`",
+                                buffer
+                                    .text_for_range(location.range.clone())
+                                    .collect::<String>()
+                            )
+                        })
+                        .unwrap();
+                    Self::open_locations_in_multibuffer(
+                        workspace, locations, replica_id, title, cx,
+                    );
+                });
 
-            Ok(())
-        }))
+                Ok(())
+            },
+        ))
     }
 
     /// Opens a multibuffer with the given project locations in it
@@ -5338,7 +5730,7 @@ impl Editor {
                             render: Arc::new({
                                 let editor = rename_editor.clone();
                                 move |cx: &mut BlockContext| {
-                                    ChildView::new(editor.clone(), cx)
+                                    ChildView::new(&editor, cx)
                                         .contained()
                                         .with_padding_left(cx.anchor_x)
                                         .boxed()
@@ -5449,21 +5841,20 @@ impl Editor {
             None => return None,
         };
 
-        Some(self.perform_format(project, cx))
+        Some(self.perform_format(project, FormatTrigger::Manual, cx))
     }
 
     fn perform_format(
         &mut self,
         project: ModelHandle<Project>,
+        trigger: FormatTrigger,
         cx: &mut ViewContext<'_, Self>,
     ) -> Task<Result<()>> {
         let buffer = self.buffer().clone();
         let buffers = buffer.read(cx).all_buffers();
 
         let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
-        let format = project.update(cx, |project, cx| {
-            project.format(buffers, true, FormatTrigger::Manual, cx)
-        });
+        let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
 
         cx.spawn(|_, mut cx| async move {
             let transaction = futures::select_biased! {
@@ -5667,17 +6058,19 @@ impl Editor {
         let mut fold_ranges = Vec::new();
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
         let selections = self.selections.all::<Point>(cx);
         for selection in selections {
-            let range = selection.display_range(&display_map).sorted();
-            let buffer_start_row = range.start.to_point(&display_map).row;
+            let range = selection.range().sorted();
+            let buffer_start_row = range.start.row;
+
+            for row in (0..=range.end.row).rev() {
+                let fold_range = display_map.foldable_range(row);
 
-            for row in (0..=range.end.row()).rev() {
-                if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) {
-                    let fold_range = self.foldable_range_for_line(&display_map, row);
+                if let Some(fold_range) = fold_range {
                     if fold_range.end.row >= buffer_start_row {
                         fold_ranges.push(fold_range);
-                        if row <= range.start.row() {
+                        if row <= range.start.row {
                             break;
                         }
                     }
@@ -5685,7 +6078,22 @@ impl Editor {
             }
         }
 
-        self.fold_ranges(fold_ranges, cx);
+        self.fold_ranges(fold_ranges, true, cx);
+    }
+
+    pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
+        let buffer_row = fold_at.buffer_row;
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        if let Some(fold_range) = display_map.foldable_range(buffer_row) {
+            let autoscroll = self
+                .selections
+                .all::<Point>(cx)
+                .iter()
+                .any(|selection| fold_range.overlaps(&selection.range()));
+
+            self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
+        }
     }
 
     pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
@@ -5703,85 +6111,80 @@ impl Editor {
                 start..end
             })
             .collect::<Vec<_>>();
-        self.unfold_ranges(ranges, true, cx);
-    }
 
-    fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool {
-        let max_point = display_map.max_point();
-        if display_row >= max_point.row() {
-            false
-        } else {
-            let (start_indent, is_blank) = display_map.line_indent(display_row);
-            if is_blank {
-                false
-            } else {
-                for display_row in display_row + 1..=max_point.row() {
-                    let (indent, is_blank) = display_map.line_indent(display_row);
-                    if !is_blank {
-                        return indent > start_indent;
-                    }
-                }
-                false
-            }
-        }
+        self.unfold_ranges(ranges, true, true, cx);
     }
 
-    fn foldable_range_for_line(
-        &self,
-        display_map: &DisplaySnapshot,
-        start_row: u32,
-    ) -> Range<Point> {
-        let max_point = display_map.max_point();
-
-        let (start_indent, _) = display_map.line_indent(start_row);
-        let start = DisplayPoint::new(start_row, display_map.line_len(start_row));
-        let mut end = None;
-        for row in start_row + 1..=max_point.row() {
-            let (indent, is_blank) = display_map.line_indent(row);
-            if !is_blank && indent <= start_indent {
-                end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1)));
-                break;
-            }
-        }
+    pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        let intersection_range = Point::new(unfold_at.buffer_row, 0)
+            ..Point::new(
+                unfold_at.buffer_row,
+                display_map.buffer_snapshot.line_len(unfold_at.buffer_row),
+            );
+
+        let autoscroll = self
+            .selections
+            .all::<Point>(cx)
+            .iter()
+            .any(|selection| selection.range().overlaps(&intersection_range));
 
-        let end = end.unwrap_or(max_point);
-        start.to_point(display_map)..end.to_point(display_map)
+        self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
     }
 
     pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
         let selections = self.selections.all::<Point>(cx);
         let ranges = selections.into_iter().map(|s| s.start..s.end);
-        self.fold_ranges(ranges, cx);
+        self.fold_ranges(ranges, true, cx);
     }
 
-    pub fn fold_ranges<T: ToOffset>(
+    pub fn fold_ranges<T: ToOffset + Clone>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,
+        auto_scroll: bool,
         cx: &mut ViewContext<Self>,
     ) {
         let mut ranges = ranges.into_iter().peekable();
         if ranges.peek().is_some() {
             self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
-            self.request_autoscroll(Autoscroll::fit(), cx);
+
+            if auto_scroll {
+                self.request_autoscroll(Autoscroll::fit(), cx);
+            }
+
             cx.notify();
         }
     }
 
-    pub fn unfold_ranges<T: ToOffset>(
+    pub fn unfold_ranges<T: ToOffset + Clone>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,
         inclusive: bool,
+        auto_scroll: bool,
         cx: &mut ViewContext<Self>,
     ) {
         let mut ranges = ranges.into_iter().peekable();
         if ranges.peek().is_some() {
             self.display_map
                 .update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
-            self.request_autoscroll(Autoscroll::fit(), cx);
+            if auto_scroll {
+                self.request_autoscroll(Autoscroll::fit(), cx);
+            }
+
             cx.notify();
         }
     }
 
+    pub fn gutter_hover(
+        &mut self,
+        GutterHover { hovered }: &GutterHover,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.gutter_hovered = *hovered;
+        cx.notify();
+    }
+
     pub fn insert_blocks(
         &mut self,
         blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,

crates/editor/src/editor_tests.rs 🔗

@@ -1,22 +1,23 @@
-use drag_and_drop::DragAndDrop;
-use futures::StreamExt;
-use indoc::indoc;
-use std::{cell::RefCell, rc::Rc, time::Instant};
-use unindent::Unindent;
-
 use super::*;
 use crate::test::{
     assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
     editor_test_context::EditorTestContext, select_ranges,
 };
+use drag_and_drop::DragAndDrop;
+use futures::StreamExt;
 use gpui::{
     executor::Deterministic,
     geometry::{rect::RectF, vector::vec2f},
     platform::{WindowBounds, WindowOptions},
+    serde_json,
 };
-use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
+use indoc::indoc;
+use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
+use parking_lot::Mutex;
 use project::FakeFs;
 use settings::EditorSettings;
+use std::{cell::RefCell, rc::Rc, time::Instant};
+use unindent::Unindent;
 use util::{
     assert_set_eq,
     test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
@@ -446,6 +447,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
                 Point::new(1, 0)..Point::new(2, 0),
                 Point::new(3, 0)..Point::new(4, 0),
             ],
+            true,
             cx,
         );
     });
@@ -482,7 +484,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
     cx.set_global(Settings::test(cx));
     cx.set_global(DragAndDrop::<Workspace>::default());
     use workspace::item::Item;
-    let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
+    let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(0, None, || &[], cx));
     let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
 
     cx.add_view(&pane, |cx| {
@@ -628,7 +630,7 @@ fn test_cancel(cx: &mut gpui::MutableAppContext) {
 }
 
 #[gpui::test]
-fn test_fold(cx: &mut gpui::MutableAppContext) {
+fn test_fold_action(cx: &mut gpui::MutableAppContext) {
     cx.set_global(Settings::test(cx));
     let buffer = MultiBuffer::build_simple(
         &"
@@ -668,10 +670,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
                         1
                     }
 
-                    fn b() {…
+                    fn b() {⋯
                     }
 
-                    fn c() {…
+                    fn c() {⋯
                     }
                 }
             "
@@ -682,7 +684,7 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
         assert_eq!(
             view.display_text(cx),
             "
-                impl Foo {…
+                impl Foo {⋯
                 }
             "
             .unindent(),
@@ -699,10 +701,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
                         1
                     }
 
-                    fn b() {…
+                    fn b() {⋯
                     }
 
-                    fn c() {…
+                    fn c() {⋯
                     }
                 }
             "
@@ -806,9 +808,10 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
                 Point::new(1, 2)..Point::new(1, 4),
                 Point::new(2, 4)..Point::new(2, 8),
             ],
+            true,
             cx,
         );
-        assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n");
+        assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε\n");
 
         view.move_right(&MoveRight, cx);
         assert_eq!(
@@ -823,13 +826,13 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
         view.move_right(&MoveRight, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ…".len())]
+            &[empty_range(0, "ⓐⓑ⋯".len())]
         );
 
         view.move_down(&MoveDown, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(1, "ab…".len())]
+            &[empty_range(1, "ab⋯".len())]
         );
         view.move_left(&MoveLeft, cx);
         assert_eq!(
@@ -855,28 +858,28 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
         view.move_right(&MoveRight, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(2, "αβ…".len())]
+            &[empty_range(2, "αβ⋯".len())]
         );
         view.move_right(&MoveRight, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(2, "αβ…ε".len())]
+            &[empty_range(2, "αβ⋯ε".len())]
         );
 
         view.move_up(&MoveUp, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(1, "ab…e".len())]
+            &[empty_range(1, "ab⋯e".len())]
         );
         view.move_up(&MoveUp, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ…ⓔ".len())]
+            &[empty_range(0, "ⓐⓑ⋯ⓔ".len())]
         );
         view.move_left(&MoveLeft, cx);
         assert_eq!(
             view.selections.display_ranges(cx),
-            &[empty_range(0, "ⓐⓑ…".len())]
+            &[empty_range(0, "ⓐⓑ⋯".len())]
         );
         view.move_left(&MoveLeft, cx);
         assert_eq!(
@@ -2118,6 +2121,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
                 Point::new(2, 3)..Point::new(4, 1),
                 Point::new(7, 0)..Point::new(8, 4),
             ],
+            true,
             cx,
         );
         view.change_selections(None, cx, |s| {
@@ -2130,13 +2134,13 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
         });
         assert_eq!(
             view.display_text(cx),
-            "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj"
+            "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
         );
 
         view.move_line_up(&MoveLineUp, cx);
         assert_eq!(
             view.display_text(cx),
-            "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff"
+            "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
         );
         assert_eq!(
             view.selections.display_ranges(cx),
@@ -2153,7 +2157,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
         view.move_line_down(&MoveLineDown, cx);
         assert_eq!(
             view.display_text(cx),
-            "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj"
+            "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
         );
         assert_eq!(
             view.selections.display_ranges(cx),
@@ -2170,7 +2174,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
         view.move_line_down(&MoveLineDown, cx);
         assert_eq!(
             view.display_text(cx),
-            "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj"
+            "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
         );
         assert_eq!(
             view.selections.display_ranges(cx),
@@ -2187,7 +2191,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
         view.move_line_up(&MoveLineUp, cx);
         assert_eq!(
             view.display_text(cx),
-            "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff"
+            "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
         );
         assert_eq!(
             view.selections.display_ranges(cx),
@@ -2349,12 +2353,16 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
         e.paste(&Paste, cx);
         e.handle_input(") ", cx);
     });
-    cx.assert_editor_state(indoc! {"
-        ( one✅ 
-        three 
-        five ) ˇtwo one✅ four three six five ( one✅ 
-        three 
-        five ) ˇ"});
+    cx.assert_editor_state(
+        &([
+            "( one✅ ",
+            "three ",
+            "five ) ˇtwo one✅ four three six five ( one✅ ",
+            "three ",
+            "five ) ˇ",
+        ]
+        .join("\n")),
+    );
 
     // Cut with three selections, one of which is full-line.
     cx.set_state(indoc! {"
@@ -2585,6 +2593,7 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
                 Point::new(2, 3)..Point::new(4, 1),
                 Point::new(7, 0)..Point::new(8, 4),
             ],
+            true,
             cx,
         );
         view.change_selections(None, cx, |s| {
@@ -2595,14 +2604,14 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
                 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
             ])
         });
-        assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i");
+        assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
     });
 
     view.update(cx, |view, cx| {
         view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
         assert_eq!(
             view.display_text(cx),
-            "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i"
+            "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
         );
         assert_eq!(
             view.selections.display_ranges(cx),
@@ -2982,6 +2991,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
                 Point::new(0, 21)..Point::new(0, 24),
                 Point::new(3, 20)..Point::new(3, 22),
             ],
+            true,
             cx,
         );
         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
@@ -3002,20 +3012,23 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
     let language = Arc::new(
         Language::new(
             LanguageConfig {
-                brackets: vec![
-                    BracketPair {
-                        start: "{".to_string(),
-                        end: "}".to_string(),
-                        close: false,
-                        newline: true,
-                    },
-                    BracketPair {
-                        start: "(".to_string(),
-                        end: ")".to_string(),
-                        close: false,
-                        newline: true,
-                    },
-                ],
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: false,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "(".to_string(),
+                            end: ")".to_string(),
+                            close: false,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
@@ -3059,38 +3072,41 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
 
     let language = Arc::new(Language::new(
         LanguageConfig {
-            brackets: vec![
-                BracketPair {
-                    start: "{".to_string(),
-                    end: "}".to_string(),
-                    close: true,
-                    newline: true,
-                },
-                BracketPair {
-                    start: "(".to_string(),
-                    end: ")".to_string(),
-                    close: true,
-                    newline: true,
-                },
-                BracketPair {
-                    start: "/*".to_string(),
-                    end: " */".to_string(),
-                    close: true,
-                    newline: true,
-                },
-                BracketPair {
-                    start: "[".to_string(),
-                    end: "]".to_string(),
-                    close: false,
-                    newline: true,
-                },
-                BracketPair {
-                    start: "\"".to_string(),
-                    end: "\"".to_string(),
-                    close: true,
-                    newline: false,
-                },
-            ],
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "(".to_string(),
+                        end: ")".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/*".to_string(),
+                        end: " */".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "[".to_string(),
+                        end: "]".to_string(),
+                        close: false,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "\"".to_string(),
+                        end: "\"".to_string(),
+                        close: true,
+                        newline: false,
+                    },
+                ],
+                ..Default::default()
+            },
             autoclose_before: "})]".to_string(),
             ..Default::default()
         },
@@ -3227,10 +3243,52 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
         Language::new(
             LanguageConfig {
                 name: "HTML".into(),
-                brackets: vec![
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "<".into(),
+                            end: ">".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                        BracketPair {
+                            start: "{".into(),
+                            end: "}".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                        BracketPair {
+                            start: "(".into(),
+                            end: ")".into(),
+                            close: true,
+                            ..Default::default()
+                        },
+                    ],
+                    ..Default::default()
+                },
+                autoclose_before: "})]>".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_html::language()),
+        )
+        .with_injection_query(
+            r#"
+            (script_element
+                (raw_text) @content
+                (#set! "language" "javascript"))
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let javascript_language = Arc::new(Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            brackets: BracketPairConfig {
+                pairs: vec![
                     BracketPair {
-                        start: "<".into(),
-                        end: ">".into(),
+                        start: "/*".into(),
+                        end: " */".into(),
                         close: true,
                         ..Default::default()
                     },
@@ -3247,44 +3305,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
                         ..Default::default()
                     },
                 ],
-                autoclose_before: "})]>".into(),
                 ..Default::default()
             },
-            Some(tree_sitter_html::language()),
-        )
-        .with_injection_query(
-            r#"
-            (script_element
-                (raw_text) @content
-                (#set! "language" "javascript"))
-            "#,
-        )
-        .unwrap(),
-    );
-
-    let javascript_language = Arc::new(Language::new(
-        LanguageConfig {
-            name: "JavaScript".into(),
-            brackets: vec![
-                BracketPair {
-                    start: "/*".into(),
-                    end: " */".into(),
-                    close: true,
-                    ..Default::default()
-                },
-                BracketPair {
-                    start: "{".into(),
-                    end: "}".into(),
-                    close: true,
-                    ..Default::default()
-                },
-                BracketPair {
-                    start: "(".into(),
-                    end: ")".into(),
-                    close: true,
-                    ..Default::default()
-                },
-            ],
             autoclose_before: "})]>".into(),
             ..Default::default()
         },
@@ -3447,25 +3469,125 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+    let mut cx = EditorTestContext::new(cx);
+
+    let rust_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                brackets: serde_json::from_value(json!([
+                    { "start": "{", "end": "}", "close": true, "newline": true },
+                    { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
+                ]))
+                .unwrap(),
+                autoclose_before: "})]>".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_override_query("(string_literal) @string")
+        .unwrap(),
+    );
+
+    let registry = Arc::new(LanguageRegistry::test());
+    registry.add(rust_language.clone());
+
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(registry);
+        buffer.set_language(Some(rust_language), cx);
+    });
+
+    cx.set_state(
+        &r#"
+            let x = ˇ
+        "#
+        .unindent(),
+    );
+
+    // Inserting a quotation mark. A closing quotation mark is automatically inserted.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "ˇ"
+        "#
+        .unindent(),
+    );
+
+    // Inserting another quotation mark. The cursor moves across the existing
+    // automatically-inserted quotation mark.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = ""ˇ
+        "#
+        .unindent(),
+    );
+
+    // Reset
+    cx.set_state(
+        &r#"
+            let x = ˇ
+        "#
+        .unindent(),
+    );
+
+    // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
+    cx.update_editor(|editor, cx| {
+        editor.handle_input("\"", cx);
+        editor.handle_input(" ", cx);
+        editor.move_left(&Default::default(), cx);
+        editor.handle_input("\\", cx);
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "\"ˇ "
+        "#
+        .unindent(),
+    );
+
+    // Inserting a closing quotation mark at the position of an automatically-inserted quotation
+    // mark. Nothing is inserted.
+    cx.update_editor(|editor, cx| {
+        editor.move_right(&Default::default(), cx);
+        editor.handle_input("\"", cx);
+    });
+    cx.assert_editor_state(
+        &r#"
+            let x = "\" "ˇ
+        "#
+        .unindent(),
+    );
+}
+
 #[gpui::test]
 async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
     cx.update(|cx| cx.set_global(Settings::test(cx)));
     let language = Arc::new(Language::new(
         LanguageConfig {
-            brackets: vec![
-                BracketPair {
-                    start: "{".to_string(),
-                    end: "}".to_string(),
-                    close: true,
-                    newline: true,
-                },
-                BracketPair {
-                    start: "/* ".to_string(),
-                    end: "*/".to_string(),
-                    close: true,
-                    ..Default::default()
-                },
-            ],
+            brackets: BracketPairConfig {
+                pairs: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/* ".to_string(),
+                        end: "*/".to_string(),
+                        close: true,
+                        ..Default::default()
+                    },
+                ],
+                ..Default::default()
+            },
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -3603,12 +3725,15 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
     cx.update(|cx| cx.set_global(Settings::test(cx)));
     let language = Arc::new(Language::new(
         LanguageConfig {
-            brackets: vec![BracketPair {
-                start: "{".to_string(),
-                end: "}".to_string(),
-                close: true,
-                newline: true,
-            }],
+            brackets: BracketPairConfig {
+                pairs: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                ..Default::default()
+            },
             autoclose_before: "}".to_string(),
             ..Default::default()
         },
@@ -4077,7 +4202,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
 
-    let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx));
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+    });
     fake_server
         .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
             assert_eq!(
@@ -4109,7 +4236,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
         futures::future::pending::<()>().await;
         unreachable!()
     });
-    let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx));
+    let format = editor.update(cx, |editor, cx| {
+        editor.perform_format(project, FormatTrigger::Manual, cx)
+    });
     cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
     cx.foreground().start_waiting();
     format.await.unwrap();
@@ -4176,6 +4305,121 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
     "});
 }
 
+#[gpui::test]
+async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
+    cx.foreground().forbid_parking();
+
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            document_formatting_provider: Some(lsp::OneOf::Left(true)),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    // Set up a buffer white some trailing whitespace and no trailing newline.
+    cx.set_state(
+        &[
+            "one ",   //
+            "twoˇ",  //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+
+    // Submit a format request.
+    let format = cx
+        .update_editor(|editor, cx| editor.format(&Format, cx))
+        .unwrap();
+
+    // Record which buffer changes have been sent to the language server
+    let buffer_changes = Arc::new(Mutex::new(Vec::new()));
+    cx.lsp
+        .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
+            let buffer_changes = buffer_changes.clone();
+            move |params, _| {
+                buffer_changes.lock().extend(
+                    params
+                        .content_changes
+                        .into_iter()
+                        .map(|e| (e.range.unwrap(), e.text)),
+                );
+            }
+        });
+
+    // Handle formatting requests to the language server.
+    cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
+        let buffer_changes = buffer_changes.clone();
+        move |_, _| {
+            // When formatting is requested, trailing whitespace has already been stripped,
+            // and the trailing newline has already been added.
+            assert_eq!(
+                &buffer_changes.lock()[1..],
+                &[
+                    (
+                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
+                        "".into()
+                    ),
+                    (
+                        lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
+                        "".into()
+                    ),
+                    (
+                        lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
+                        "\n".into()
+                    ),
+                ]
+            );
+
+            // Insert blank lines between each line of the buffer.
+            async move {
+                Ok(Some(vec![
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
+                        new_text: "\n".into(),
+                    },
+                    lsp::TextEdit {
+                        range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
+                        new_text: "\n".into(),
+                    },
+                ]))
+            }
+        }
+    });
+
+    // After formatting the buffer, the trailing whitespace is stripped,
+    // a newline is appended, and the edits provided by the language server
+    // have been applied.
+    format.await.unwrap();
+    cx.assert_editor_state(
+        &[
+            "one",   //
+            "",      //
+            "twoˇ", //
+            "",      //
+            "three", //
+            "four",  //
+            "",      //
+        ]
+        .join("\n"),
+    );
+
+    // Undoing the formatting undoes the trailing whitespace removal, the
+    // trailing newline, and the LSP edits.
+    cx.update_buffer(|buffer, cx| buffer.undo(cx));
+    cx.assert_editor_state(
+        &[
+            "one ",   //
+            "twoˇ",  //
+            "three ", //
+            "four",   //
+        ]
+        .join("\n"),
+    );
+}
+
 #[gpui::test]
 async fn test_completion(cx: &mut gpui::TestAppContext) {
     let mut cx = EditorLspTestContext::new_rust(
@@ -5030,20 +5274,23 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
     let language = Arc::new(
         Language::new(
             LanguageConfig {
-                brackets: vec![
-                    BracketPair {
-                        start: "{".to_string(),
-                        end: "}".to_string(),
-                        close: true,
-                        newline: true,
-                    },
-                    BracketPair {
-                        start: "/* ".to_string(),
-                        end: " */".to_string(),
-                        close: true,
-                        newline: true,
-                    },
-                ],
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".to_string(),
+                            end: "}".to_string(),
+                            close: true,
+                            newline: true,
+                        },
+                        BracketPair {
+                            start: "/* ".to_string(),
+                            end: " */".to_string(),
+                            close: true,
+                            newline: true,
+                        },
+                    ],
+                    ..Default::default()
+                },
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
@@ -5319,7 +5566,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     Settings::test_async(cx);
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
+    let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
 
     let leader = pane.update(cx, |_, cx| {
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -5588,11 +5835,11 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
     cx.assert_editor_state(
         &r#"
         ˇuse some::modified;
-    
-    
+
+
         fn main() {
             println!("hello there");
-    
+
             println!("around the");
             println!("world");
         }

crates/editor/src/element.rs 🔗

@@ -4,7 +4,7 @@ use super::{
     ToPoint, MAX_LINE_LEN,
 };
 use crate::{
-    display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
+    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
         HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
@@ -14,7 +14,7 @@ use crate::{
     },
     mouse_context_menu::DeployMouseContextMenu,
     scroll::actions::Scroll,
-    EditorStyle,
+    EditorStyle, GutterHover, UnfoldAt,
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
@@ -48,6 +48,9 @@ use std::{
     ops::{DerefMut, Range},
     sync::Arc,
 };
+use workspace::item::Item;
+
+enum FoldMarkers {}
 
 struct SelectionLayout {
     head: DisplayPoint,
@@ -212,6 +215,17 @@ impl EditorElement {
                     }
                 }),
         );
+
+        enum GutterHandlers {}
+        cx.scene.push_mouse_region(
+            MouseRegion::new::<GutterHandlers>(view.id(), view.id() + 1, gutter_bounds).on_hover(
+                |hover, cx| {
+                    cx.dispatch_action(GutterHover {
+                        hovered: hover.started,
+                    })
+                },
+            ),
+        )
     }
 
     fn mouse_down(
@@ -400,16 +414,7 @@ impl EditorElement {
     ) -> bool {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
         // Don't trigger hover popover if mouse is hovering over context menu
-        let point = if text_bounds.contains_point(position) {
-            let (point, target_point) = position_map.point_for_position(text_bounds, position);
-            if point == target_point {
-                Some(point)
-            } else {
-                None
-            }
-        } else {
-            None
-        };
+        let point = position_to_display_point(position, text_bounds, position_map);
 
         cx.dispatch_action(UpdateGoToDefinitionLink {
             point,
@@ -418,6 +423,7 @@ impl EditorElement {
         });
 
         cx.dispatch_action(HoverAt { point });
+
         true
     }
 
@@ -568,8 +574,25 @@ impl EditorElement {
             }
         }
 
+        for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
+            if let Some(indicator) = fold_indicator.as_mut() {
+                let position = vec2f(
+                    bounds.width() - layout.gutter_padding,
+                    ix as f32 * line_height - (scroll_top % line_height),
+                );
+                let centering_offset = vec2f(
+                    (layout.gutter_padding + layout.gutter_margin - indicator.size().x()) / 2.,
+                    (line_height - indicator.size().y()) / 2.,
+                );
+
+                let indicator_origin = bounds.origin() + position + centering_offset;
+
+                indicator.paint(indicator_origin, visible_bounds, cx);
+            }
+        }
+
         if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
-            let mut x = bounds.width() - layout.gutter_padding;
+            let mut x = 0.;
             let mut y = *row as f32 * line_height - scroll_top;
             x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
             y += (line_height - indicator.size().y()) / 2.;
@@ -676,6 +699,7 @@ impl EditorElement {
         let max_glyph_width = layout.position_map.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
         let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
+        let line_end_overshoot = 0.15 * layout.position_map.line_height;
 
         cx.scene.push_layer(Some(bounds));
 
@@ -688,12 +712,59 @@ impl EditorElement {
             },
         });
 
+        let fold_corner_radius =
+            self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
+        for (id, range, color) in layout.fold_ranges.iter() {
+            self.paint_highlighted_range(
+                range.clone(),
+                *color,
+                fold_corner_radius,
+                fold_corner_radius * 2.,
+                layout,
+                content_origin,
+                scroll_top,
+                scroll_left,
+                bounds,
+                cx,
+            );
+
+            for bound in range_to_bounds(
+                &range,
+                content_origin,
+                scroll_left,
+                scroll_top,
+                &layout.visible_display_row_range,
+                line_end_overshoot,
+                &layout.position_map,
+            ) {
+                cx.scene.push_cursor_region(CursorRegion {
+                    bounds: bound,
+                    style: CursorStyle::PointingHand,
+                });
+
+                let display_row = range.start.row();
+
+                let buffer_row = DisplayPoint::new(display_row, 0)
+                    .to_point(&layout.position_map.snapshot.display_snapshot)
+                    .row;
+
+                cx.scene.push_mouse_region(
+                    MouseRegion::new::<FoldMarkers>(self.view.id(), *id as usize, bound)
+                        .on_click(MouseButton::Left, move |_, cx| {
+                            cx.dispatch_action(UnfoldAt { buffer_row })
+                        })
+                        .with_notify_on_hover(true)
+                        .with_notify_on_click(true),
+                )
+            }
+        }
+
         for (range, color) in &layout.highlighted_ranges {
             self.paint_highlighted_range(
                 range.clone(),
                 *color,
                 0.,
-                0.15 * layout.position_map.line_height,
+                line_end_overshoot,
                 layout,
                 content_origin,
                 scroll_top,
@@ -704,9 +775,10 @@ impl EditorElement {
         }
 
         let mut cursors = SmallVec::<[Cursor; 32]>::new();
+        let corner_radius = 0.15 * layout.position_map.line_height;
+
         for (replica_id, selections) in &layout.selections {
             let selection_style = style.replica_selection_style(*replica_id);
-            let corner_radius = 0.15 * layout.position_map.line_height;
 
             for selection in selections {
                 self.paint_highlighted_range(
@@ -1145,12 +1217,17 @@ impl EditorElement {
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
+        is_singleton: bool,
         snapshot: &EditorSnapshot,
         cx: &LayoutContext,
-    ) -> Vec<Option<text_layout::Line>> {
+    ) -> (
+        Vec<Option<text_layout::Line>>,
+        Vec<Option<(FoldStatus, BufferRow, bool)>>,
+    ) {
         let style = &self.style;
         let include_line_numbers = snapshot.mode == EditorMode::Full;
         let mut line_number_layouts = Vec::with_capacity(rows.len());
+        let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
         for (ix, row) in snapshot
             .buffer_rows(rows.start)
@@ -1158,10 +1235,10 @@ impl EditorElement {
             .enumerate()
         {
             let display_row = rows.start + ix as u32;
-            let color = if active_rows.contains_key(&display_row) {
-                style.line_number_active
+            let (active, color) = if active_rows.contains_key(&display_row) {
+                (true, style.line_number_active)
             } else {
-                style.line_number
+                (false, style.line_number)
             };
             if let Some(buffer_row) = row {
                 if include_line_numbers {
@@ -1179,13 +1256,23 @@ impl EditorElement {
                             },
                         )],
                     )));
+                    fold_statuses.push(
+                        is_singleton
+                            .then(|| {
+                                snapshot
+                                    .fold_for_line(buffer_row)
+                                    .map(|fold_status| (fold_status, buffer_row, active))
+                            })
+                            .flatten(),
+                    )
                 }
             } else {
+                fold_statuses.push(None);
                 line_number_layouts.push(None);
             }
         }
 
-        line_number_layouts
+        (line_number_layouts, fold_statuses)
     }
 
     fn layout_lines(
@@ -1231,45 +1318,47 @@ impl EditorElement {
                 .collect()
         } else {
             let style = &self.style;
-            let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| {
-                let mut highlight_style = chunk
-                    .syntax_highlight_id
-                    .and_then(|id| id.style(&style.syntax));
-
-                if let Some(chunk_highlight) = chunk.highlight_style {
-                    if let Some(highlight_style) = highlight_style.as_mut() {
-                        highlight_style.highlight(chunk_highlight);
-                    } else {
-                        highlight_style = Some(chunk_highlight);
+            let chunks = snapshot
+                .chunks(rows.clone(), true, Some(style.theme.suggestion))
+                .map(|chunk| {
+                    let mut highlight_style = chunk
+                        .syntax_highlight_id
+                        .and_then(|id| id.style(&style.syntax));
+
+                    if let Some(chunk_highlight) = chunk.highlight_style {
+                        if let Some(highlight_style) = highlight_style.as_mut() {
+                            highlight_style.highlight(chunk_highlight);
+                        } else {
+                            highlight_style = Some(chunk_highlight);
+                        }
                     }
-                }
 
-                let mut diagnostic_highlight = HighlightStyle::default();
+                    let mut diagnostic_highlight = HighlightStyle::default();
 
-                if chunk.is_unnecessary {
-                    diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
-                }
+                    if chunk.is_unnecessary {
+                        diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
+                    }
 
-                if let Some(severity) = chunk.diagnostic_severity {
-                    // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
-                    if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
-                        let diagnostic_style = super::diagnostic_style(severity, true, style);
-                        diagnostic_highlight.underline = Some(Underline {
-                            color: Some(diagnostic_style.message.text.color),
-                            thickness: 1.0.into(),
-                            squiggly: true,
-                        });
+                    if let Some(severity) = chunk.diagnostic_severity {
+                        // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+                        if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+                            let diagnostic_style = super::diagnostic_style(severity, true, style);
+                            diagnostic_highlight.underline = Some(Underline {
+                                color: Some(diagnostic_style.message.text.color),
+                                thickness: 1.0.into(),
+                                squiggly: true,
+                            });
+                        }
                     }
-                }
 
-                if let Some(highlight_style) = highlight_style.as_mut() {
-                    highlight_style.highlight(diagnostic_highlight);
-                } else {
-                    highlight_style = Some(diagnostic_highlight);
-                }
+                    if let Some(highlight_style) = highlight_style.as_mut() {
+                        highlight_style.highlight(diagnostic_highlight);
+                    } else {
+                        highlight_style = Some(diagnostic_highlight);
+                    }
 
-                (chunk.text, highlight_style)
-            });
+                    (chunk.text, highlight_style)
+                });
             layout_highlighted_chunks(
                 chunks,
                 &style.text,
@@ -1438,7 +1527,7 @@ impl EditorElement {
                     } else {
                         let text_style = self.style.text.clone();
                         Flex::row()
-                            .with_child(Label::new("…".to_string(), text_style).boxed())
+                            .with_child(Label::new("⋯", text_style).boxed())
                             .with_children(jump_icon)
                             .contained()
                             .with_padding_left(gutter_padding)
@@ -1606,9 +1695,13 @@ impl Element for EditorElement {
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
         let mut highlighted_ranges = Vec::new();
+        let mut fold_ranges = Vec::new();
         let mut show_scrollbars = false;
         let mut include_root = false;
+        let mut is_singleton = false;
         self.update_view(cx.app, |view, cx| {
+            is_singleton = view.is_singleton(cx);
+
             let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
 
             highlighted_rows = view.highlighted_rows();
@@ -1616,6 +1709,19 @@ impl Element for EditorElement {
             highlighted_ranges =
                 view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
 
+            fold_ranges.extend(
+                snapshot
+                    .folds_in_range(start_anchor..end_anchor)
+                    .map(|anchor| {
+                        let start = anchor.start.to_point(&snapshot.buffer_snapshot);
+                        (
+                            start.row,
+                            start.to_display_point(&snapshot.display_snapshot)
+                                ..anchor.end.to_display_point(&snapshot),
+                        )
+                    }),
+            );
+
             let mut remote_selections = HashMap::default();
             for (replica_id, line_mode, cursor_shape, selection) in display_map
                 .buffer_snapshot
@@ -1684,8 +1790,28 @@ impl Element for EditorElement {
                 .unwrap_or_default()
         });
 
-        let line_number_layouts =
-            self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
+        let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
+            .into_iter()
+            .map(|(id, fold)| {
+                let color = self
+                    .style
+                    .folds
+                    .ellipses
+                    .background
+                    .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
+                    .color;
+
+                (id, fold, color)
+            })
+            .collect();
+
+        let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+            start_row..end_row,
+            &active_rows,
+            is_singleton,
+            &snapshot,
+            cx,
+        );
 
         let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
 
@@ -1755,7 +1881,7 @@ impl Element for EditorElement {
         let mut code_actions_indicator = None;
         let mut hover = None;
         let mut mode = EditorMode::Full;
-        cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
+        let mut fold_indicators = cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
             let newest_selection_head = view
                 .selections
                 .newest::<usize>(cx)
@@ -1769,14 +1895,25 @@ impl Element for EditorElement {
                         view.render_context_menu(newest_selection_head, style.clone(), cx);
                 }
 
+                let active = matches!(view.context_menu, Some(crate::ContextMenu::CodeActions(_)));
+
                 code_actions_indicator = view
-                    .render_code_actions_indicator(&style, cx)
+                    .render_code_actions_indicator(&style, active, cx)
                     .map(|indicator| (newest_selection_head.row(), indicator));
             }
 
             let visible_rows = start_row..start_row + line_layouts.len() as u32;
             hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
             mode = view.mode;
+
+            view.render_fold_indicators(
+                fold_statuses,
+                &style,
+                view.gutter_hovered,
+                line_height,
+                gutter_margin,
+                cx,
+            )
         });
 
         if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1802,6 +1939,18 @@ impl Element for EditorElement {
             );
         }
 
+        for fold_indicator in fold_indicators.iter_mut() {
+            if let Some(indicator) = fold_indicator.as_mut() {
+                indicator.layout(
+                    SizeConstraint::strict_along(
+                        Axis::Vertical,
+                        line_height * style.code_actions.vertical_scale,
+                    ),
+                    cx,
+                );
+            }
+        }
+
         if let Some((_, hover_popovers)) = hover.as_mut() {
             for hover_popover in hover_popovers.iter_mut() {
                 hover_popover.layout(
@@ -1845,12 +1994,14 @@ impl Element for EditorElement {
                 active_rows,
                 highlighted_rows,
                 highlighted_ranges,
+                fold_ranges,
                 line_number_layouts,
                 display_hunks,
                 blocks,
                 selections,
                 context_menu,
                 code_actions_indicator,
+                fold_indicators,
                 hover_popovers: hover,
             },
         )
@@ -1958,6 +2109,8 @@ impl Element for EditorElement {
     }
 }
 
+type BufferRow = u32;
+
 pub struct LayoutState {
     position_map: Arc<PositionMap>,
     gutter_size: Vector2F,
@@ -1972,6 +2125,7 @@ pub struct LayoutState {
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
+    fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
@@ -1979,6 +2133,7 @@ pub struct LayoutState {
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
     hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
+    fold_indicators: Vec<Option<ElementBox>>,
 }
 
 pub struct PositionMap {
@@ -2277,6 +2432,75 @@ impl HighlightedRange {
     }
 }
 
+pub fn position_to_display_point(
+    position: Vector2F,
+    text_bounds: RectF,
+    position_map: &PositionMap,
+) -> Option<DisplayPoint> {
+    if text_bounds.contains_point(position) {
+        let (point, target_point) = position_map.point_for_position(text_bounds, position);
+        if point == target_point {
+            Some(point)
+        } else {
+            None
+        }
+    } else {
+        None
+    }
+}
+
+pub fn range_to_bounds(
+    range: &Range<DisplayPoint>,
+    content_origin: Vector2F,
+    scroll_left: f32,
+    scroll_top: f32,
+    visible_row_range: &Range<u32>,
+    line_end_overshoot: f32,
+    position_map: &PositionMap,
+) -> impl Iterator<Item = RectF> {
+    let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
+
+    if range.start == range.end {
+        return bounds.into_iter();
+    }
+
+    let start_row = visible_row_range.start;
+    let end_row = visible_row_range.end;
+
+    let row_range = if range.end.column() == 0 {
+        cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+    } else {
+        cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+    };
+
+    let first_y =
+        content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
+
+    for (idx, row) in row_range.enumerate() {
+        let line_layout = &position_map.line_layouts[(row - start_row) as usize];
+
+        let start_x = if row == range.start.row() {
+            content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
+                - scroll_left
+        } else {
+            content_origin.x() - scroll_left
+        };
+
+        let end_x = if row == range.end.row() {
+            content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
+        } else {
+            content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
+        };
+
+        bounds.push(RectF::from_points(
+            vec2f(start_x, first_y + position_map.line_height * idx as f32),
+            vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
+        ))
+    }
+
+    bounds.into_iter()
+}
+
 pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
     delta.powf(1.5) / 100.0
 }
@@ -2310,7 +2534,9 @@ mod tests {
             let snapshot = editor.snapshot(cx);
             let mut presenter = cx.build_presenter(window_id, 30., Default::default());
             let layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
-            element.layout_line_numbers(0..6, &Default::default(), &snapshot, &layout_cx)
+            element
+                .layout_line_numbers(0..6, &Default::default(), false, &snapshot, &layout_cx)
+                .0
         });
         assert_eq!(layouts.len(), 6);
     }

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -32,11 +32,10 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
 
 #[cfg(test)]
 mod tests {
-    use crate::test::editor_lsp_test_context::EditorLspTestContext;
-
     use super::*;
+    use crate::test::editor_lsp_test_context::EditorLspTestContext;
     use indoc::indoc;
-    use language::{BracketPair, Language, LanguageConfig};
+    use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
 
     #[gpui::test]
     async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
@@ -45,20 +44,23 @@ mod tests {
                 LanguageConfig {
                     name: "Rust".into(),
                     path_suffixes: vec!["rs".to_string()],
-                    brackets: vec![
-                        BracketPair {
-                            start: "{".to_string(),
-                            end: "}".to_string(),
-                            close: false,
-                            newline: true,
-                        },
-                        BracketPair {
-                            start: "(".to_string(),
-                            end: ")".to_string(),
-                            close: false,
-                            newline: true,
-                        },
-                    ],
+                    brackets: BracketPairConfig {
+                        pairs: vec![
+                            BracketPair {
+                                start: "{".to_string(),
+                                end: "}".to_string(),
+                                close: false,
+                                newline: true,
+                            },
+                            BracketPair {
+                                start: "(".to_string(),
+                                end: ")".to_string(),
+                                close: false,
+                                newline: true,
+                            },
+                        ],
+                        ..Default::default()
+                    },
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),

crates/editor/src/hover_popover.rs 🔗

@@ -1,3 +1,4 @@
+use futures::FutureExt;
 use gpui::{
     actions,
     elements::{Flex, MouseEventHandler, Padding, Text},
@@ -327,12 +328,10 @@ impl InfoPopover {
         MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
             let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
             flex.extend(self.contents.iter().map(|content| {
-                let project = self.project.read(cx);
-                if let Some(language) = content
-                    .language
-                    .clone()
-                    .and_then(|language| project.languages().language_for_name(&language))
-                {
+                let languages = self.project.read(cx).languages();
+                if let Some(language) = content.language.clone().and_then(|language| {
+                    languages.language_for_name(&language).now_or_never()?.ok()
+                }) {
                     let runs = language
                         .highlight_text(&content.text.as_str().into(), 0..content.text.len());
 

crates/editor/src/items.rs 🔗

@@ -14,7 +14,7 @@ use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
     SelectionGoal,
 };
-use project::{Item as _, Project, ProjectPath};
+use project::{FormatTrigger, Item as _, Project, ProjectPath};
 use rpc::proto::{self, update_view};
 use settings::Settings;
 use smallvec::SmallVec;
@@ -529,7 +529,7 @@ impl Item for Editor {
     ) -> ElementBox {
         Flex::row()
             .with_child(
-                Label::new(self.title(cx).into(), style.label.clone())
+                Label::new(self.title(cx).to_string(), style.label.clone())
                     .aligned()
                     .boxed(),
             )
@@ -538,11 +538,7 @@ impl Item for Editor {
                 let description = path.to_string_lossy();
                 Some(
                     Label::new(
-                        if description.len() > MAX_TAB_TITLE_LEN {
-                            description[..MAX_TAB_TITLE_LEN].to_string() + "…"
-                        } else {
-                            description.into()
-                        },
+                        util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN),
                         style.description.text.clone(),
                     )
                     .contained()
@@ -608,13 +604,38 @@ impl Item for Editor {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         self.report_event("save editor", cx);
-        let format = self.perform_format(project.clone(), cx);
+        let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
         let buffers = self.buffer().clone().read(cx).all_buffers();
         cx.as_mut().spawn(|mut cx| async move {
             format.await?;
-            project
-                .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
-                .await?;
+
+            if buffers.len() == 1 {
+                project
+                    .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
+                    .await?;
+            } else {
+                // For multi-buffers, only save those ones that contain changes. For clean buffers
+                // we simulate saving by calling `Buffer::did_save`, so that language servers or
+                // other downstream listeners of save events get notified.
+                let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
+                    buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
+                });
+
+                project
+                    .update(&mut cx, |project, cx| {
+                        project.save_buffers(dirty_buffers, cx)
+                    })
+                    .await?;
+                for buffer in clean_buffers {
+                    buffer.update(&mut cx, |buffer, cx| {
+                        let version = buffer.saved_version().clone();
+                        let fingerprint = buffer.saved_version_fingerprint();
+                        let mtime = buffer.saved_mtime();
+                        buffer.did_save(version, fingerprint, mtime, cx);
+                    });
+                }
+            }
+
             Ok(())
         })
     }
@@ -726,11 +747,15 @@ impl Item for Editor {
             .map(|path| path.to_string_lossy().to_string())
             .unwrap_or_else(|| "untitled".to_string());
 
-        let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
+        let filename_label = Label::new(filename, theme.workspace.breadcrumbs.default.text.clone());
+        let mut breadcrumbs = vec![filename_label.boxed()];
         breadcrumbs.extend(symbols.into_iter().map(|symbol| {
-            Text::new(symbol.text, theme.breadcrumbs.text.clone())
-                .with_highlights(symbol.highlight_ranges)
-                .boxed()
+            Text::new(
+                symbol.text,
+                theme.workspace.breadcrumbs.default.text.clone(),
+            )
+            .with_highlights(symbol.highlight_ranges)
+            .boxed()
         }));
         Some(breadcrumbs)
     }
@@ -810,7 +835,7 @@ impl Item for Editor {
                         .context("Project item at stored path was not a buffer")?;
 
                     Ok(cx.update(|cx| {
-                        cx.add_view(pane, |cx| {
+                        cx.add_view(&pane, |cx| {
                             let mut editor = Editor::for_buffer(buffer, Some(project), cx);
                             editor.read_scroll_position_from_db(item_id, workspace_id, cx);
                             editor
@@ -886,7 +911,7 @@ impl SearchableItem for Editor {
         matches: Vec<Range<Anchor>>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.unfold_ranges([matches[index].clone()], false, cx);
+        self.unfold_ranges([matches[index].clone()], false, true, cx);
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.select_ranges([matches[index].clone()])
         });

crates/editor/src/mouse_context_menu.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
 
 use crate::{
     DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
-    Rename, SelectMode, ToggleCodeActions,
+    Rename, RevealInFinder, SelectMode, ToggleCodeActions,
 };
 
 #[derive(Clone, PartialEq)]
@@ -61,6 +61,8 @@ pub fn deploy_context_menu(
                         deployed_from_indicator: false,
                     },
                 ),
+                ContextMenuItem::Separator,
+                ContextMenuItem::item("Reveal in Finder", RevealInFinder),
             ],
             cx,
         );

crates/editor/src/movement.rs 🔗

@@ -69,16 +69,11 @@ pub fn up_by_rows(
         goal_column = 0;
     }
 
-    let clip_bias = if point.column() == map.line_len(point.row()) {
-        Bias::Left
-    } else {
-        Bias::Right
-    };
-
-    (
-        map.clip_point(point, clip_bias),
-        SelectionGoal::Column(goal_column),
-    )
+    let mut clipped_point = map.clip_point(point, Bias::Left);
+    if clipped_point.row() < point.row() {
+        clipped_point = map.clip_point(point, Bias::Right);
+    }
+    (clipped_point, SelectionGoal::Column(goal_column))
 }
 
 pub fn down_by_rows(
@@ -105,16 +100,11 @@ pub fn down_by_rows(
         goal_column = map.column_to_chars(point.row(), point.column())
     }
 
-    let clip_bias = if point.column() == map.line_len(point.row()) {
-        Bias::Left
-    } else {
-        Bias::Right
-    };
-
-    (
-        map.clip_point(point, clip_bias),
-        SelectionGoal::Column(goal_column),
-    )
+    let mut clipped_point = map.clip_point(point, Bias::Right);
+    if clipped_point.row() > point.row() {
+        clipped_point = map.clip_point(point, Bias::Left);
+    }
+    (clipped_point, SelectionGoal::Column(goal_column))
 }
 
 pub fn line_beginning(
@@ -587,7 +577,10 @@ mod tests {
     #[gpui::test]
     fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
-        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let family_id = cx
+            .font_cache()
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())

crates/editor/src/multi_buffer.rs 🔗

@@ -1082,18 +1082,21 @@ impl MultiBuffer {
 
         let mut cursor = snapshot.excerpts.cursor::<usize>();
         cursor.seek(&position, Bias::Right, &());
-        cursor.item().map(|excerpt| {
-            (
-                excerpt.id.clone(),
-                self.buffers
-                    .borrow()
-                    .get(&excerpt.buffer_id)
-                    .unwrap()
-                    .buffer
-                    .clone(),
-                excerpt.range.context.clone(),
-            )
-        })
+        cursor
+            .item()
+            .or_else(|| snapshot.excerpts.last())
+            .map(|excerpt| {
+                (
+                    excerpt.id.clone(),
+                    self.buffers
+                        .borrow()
+                        .get(&excerpt.buffer_id)
+                        .unwrap()
+                        .buffer
+                        .clone(),
+                    excerpt.range.context.clone(),
+                )
+            })
     }
 
     // If point is at the end of the buffer, the last excerpt is returned
@@ -2191,7 +2194,11 @@ impl MultiBufferSnapshot {
 
     pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
         let mut cursor = self.excerpts.cursor::<Point>();
-        cursor.seek(&Point::new(row, 0), Bias::Right, &());
+        let point = Point::new(row, 0);
+        cursor.seek(&point, Bias::Right, &());
+        if cursor.item().is_none() && *cursor.start() == point {
+            cursor.prev(&());
+        }
         if let Some(excerpt) = cursor.item() {
             let overshoot = row - cursor.start().row;
             let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
@@ -2926,6 +2933,10 @@ impl MultiBufferSnapshot {
         Some(self.excerpt(excerpt_id)?.buffer_id)
     }
 
+    pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> {
+        Some(&self.excerpt(excerpt_id)?.buffer)
+    }
+
     fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
         let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
         let locator = self.excerpt_locator_for_id(excerpt_id);

crates/editor/src/test.rs 🔗

@@ -25,7 +25,10 @@ pub fn marked_display_snapshot(
 ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
     let (unmarked_text, markers) = marked_text_offsets(text);
 
-    let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+    let family_id = cx
+        .font_cache()
+        .load_family(&["Helvetica"], &Default::default())
+        .unwrap();
     let font_id = cx
         .font_cache()
         .select_font(family_id, &Default::default())

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

@@ -39,7 +39,7 @@ impl<'a> EditorLspTestContext<'a> {
             pane::init(cx);
         });
 
-        let params = cx.update(AppState::test);
+        let app_state = cx.update(AppState::test);
 
         let file_name = format!(
             "file.{}",
@@ -56,24 +56,16 @@ impl<'a> EditorLspTestContext<'a> {
             }))
             .await;
 
-        let project = Project::test(params.fs.clone(), [], cx).await;
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 
-        params
+        app_state
             .fs
             .as_fake()
             .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
@@ -134,7 +126,7 @@ impl<'a> EditorLspTestContext<'a> {
                     (let_chain)
                     (await_expression)
                 ] @indent
-                
+
                 (_ "[" "]" @end) @indent
                 (_ "<" ">" @end) @indent
                 (_ "{" "}" @end) @indent

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

@@ -185,6 +185,7 @@ impl<'a> EditorTestContext<'a> {
     /// of its selections using a string containing embedded range markers.
     ///
     /// See the `util::test::marked_text_ranges` function for more information.
+    #[track_caller]
     pub fn assert_editor_state(&mut self, marked_text: &str) {
         let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
         let buffer_text = self.buffer_text();

crates/feedback/Cargo.toml 🔗

@@ -21,14 +21,15 @@ gpui = { path = "../gpui" }
 human_bytes = "0.4.1"
 isahc = "1.7"
 lazy_static = "1.4.0"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 project = { path = "../project" }
 search = { path = "../search" }
-serde = { version = "1.0", features = ["derive", "rc"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 settings = { path = "../settings" }
 sysinfo = "0.27.1"
 theme = { path = "../theme" }
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
 util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { path = "../workspace" }

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -1,38 +1,66 @@
-use gpui::{
-    elements::{MouseEventHandler, ParentElement, Stack, Text},
-    CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
-};
+use gpui::{elements::*, CursorStyle, Entity, MouseButton, RenderContext, View, ViewContext};
 use settings::Settings;
 use workspace::{item::ItemHandle, StatusItemView};
 
-use crate::feedback_editor::GiveFeedback;
+use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
 
-pub struct DeployFeedbackButton;
+pub struct DeployFeedbackButton {
+    active: bool,
+}
 
 impl Entity for DeployFeedbackButton {
     type Event = ();
 }
 
+impl DeployFeedbackButton {
+    pub fn new() -> Self {
+        DeployFeedbackButton { active: false }
+    }
+}
+
 impl View for DeployFeedbackButton {
     fn ui_name() -> &'static str {
         "DeployFeedbackButton"
     }
 
     fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
+        let active = self.active;
+        let theme = cx.global::<Settings>().theme.clone();
         Stack::new()
             .with_child(
-                MouseEventHandler::<Self>::new(0, cx, |state, cx| {
-                    let theme = &cx.global::<Settings>().theme;
-                    let theme = &theme.workspace.status_bar.feedback;
-
-                    Text::new(
-                        "Give Feedback".to_string(),
-                        theme.style_for(state, true).clone(),
-                    )
-                    .boxed()
+                MouseEventHandler::<Self>::new(0, cx, |state, _| {
+                    let style = &theme
+                        .workspace
+                        .status_bar
+                        .sidebar_buttons
+                        .item
+                        .style_for(state, active);
+
+                    Svg::new("icons/feedback_16.svg")
+                        .with_color(style.icon_color)
+                        .constrained()
+                        .with_width(style.icon_size)
+                        .aligned()
+                        .constrained()
+                        .with_width(style.icon_size)
+                        .with_height(style.icon_size)
+                        .contained()
+                        .with_style(style.container)
+                        .boxed()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
+                .on_click(MouseButton::Left, move |_, cx| {
+                    if !active {
+                        cx.dispatch_action(GiveFeedback)
+                    }
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Send Feedback".into(),
+                    Some(Box::new(GiveFeedback)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
                 .boxed(),
             )
             .boxed()
@@ -40,5 +68,15 @@ impl View for DeployFeedbackButton {
 }
 
 impl StatusItemView for DeployFeedbackButton {
-    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
+    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        if let Some(item) = item {
+            if let Some(_) = item.downcast::<FeedbackEditor>() {
+                self.active = true;
+                cx.notify();
+                return;
+            }
+        }
+        self.active = false;
+        cx.notify();
+    }
 }

crates/feedback/src/feedback.rs 🔗

@@ -20,7 +20,12 @@ impl_actions!(zed, [OpenBrowser]);
 
 actions!(
     zed,
-    [CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
+    [
+        CopySystemSpecsIntoClipboard,
+        FileBugReport,
+        RequestFeature,
+        OpenZedCommunityRepo
+    ]
 );
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
@@ -66,4 +71,11 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
             });
         },
     );
+
+    cx.add_action(
+        |_: &mut Workspace, _: &OpenZedCommunityRepo, cx: &mut ViewContext<Workspace>| {
+            let url = "https://github.com/zed-industries/community";
+            cx.dispatch_action(OpenBrowser { url: url.into() });
+        },
+    );
 }

crates/feedback/src/feedback_editor.rs 🔗

@@ -10,10 +10,9 @@ use editor::{Anchor, Editor};
 use futures::AsyncReadExt;
 use gpui::{
     actions,
-    elements::{ChildView, Flex, Label, ParentElement},
+    elements::{ChildView, Flex, Label, ParentElement, Svg},
     serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
     MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle,
 };
 use isahc::Request;
 use language::Buffer;
@@ -21,10 +20,10 @@ use postage::prelude::Stream;
 
 use project::Project;
 use serde::Serialize;
+use util::ResultExt;
 use workspace::{
     item::{Item, ItemHandle},
     searchable::{SearchableItem, SearchableItemHandle},
-    smallvec::SmallVec,
     AppState, Workspace,
 };
 
@@ -202,24 +201,28 @@ impl FeedbackEditor {
 impl FeedbackEditor {
     pub fn deploy(
         system_specs: SystemSpecs,
-        workspace: &mut Workspace,
+        _: &mut Workspace,
         app_state: Arc<AppState>,
         cx: &mut ViewContext<Workspace>,
     ) {
-        workspace
-            .with_local_workspace(&app_state, cx, |workspace, cx| {
-                let project = workspace.project().clone();
-                let markdown_language = project.read(cx).languages().language_for_name("Markdown");
-                let buffer = project
-                    .update(cx, |project, cx| {
-                        project.create_buffer("", markdown_language, cx)
+        let markdown = app_state.languages.language_for_name("Markdown");
+        cx.spawn(|workspace, mut cx| async move {
+            let markdown = markdown.await.log_err();
+            workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
+                        let project = workspace.project().clone();
+                        let buffer = project
+                            .update(cx, |project, cx| project.create_buffer("", markdown, cx))
+                            .expect("creating buffers on a local workspace always succeeds");
+                        let feedback_editor = cx
+                            .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
+                        workspace.add_item(Box::new(feedback_editor), cx);
                     })
-                    .expect("creating buffers on a local workspace always succeeds");
-                let feedback_editor =
-                    cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
-                workspace.add_item(Box::new(feedback_editor), cx);
-            })
-            .detach();
+                })
+                .await;
+        })
+        .detach();
     }
 }
 
@@ -247,7 +250,17 @@ impl Item for FeedbackEditor {
     fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
         Flex::row()
             .with_child(
-                Label::new("Feedback".to_string(), style.label.clone())
+                Svg::new("icons/feedback_16.svg")
+                    .with_color(style.label.text.color)
+                    .constrained()
+                    .with_width(style.type_icon_width)
+                    .aligned()
+                    .contained()
+                    .with_margin_right(style.spacing)
+                    .boxed(),
+            )
+            .with_child(
+                Label::new("Send Feedback", style.label.clone())
                     .aligned()
                     .contained()
                     .boxed(),
@@ -259,16 +272,10 @@ impl Item for FeedbackEditor {
         self.editor.for_each_project_item(cx, f)
     }
 
-    fn to_item_events(_: &Self::Event) -> SmallVec<[workspace::item::ItemEvent; 2]> {
-        SmallVec::new()
-    }
-
     fn is_singleton(&self, _: &AppContext) -> bool {
         true
     }
 
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
     fn can_save(&self, _: &AppContext) -> bool {
         true
     }
@@ -295,7 +302,7 @@ impl Item for FeedbackEditor {
         _: ModelHandle<Project>,
         _: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        unreachable!("reload should not have been called")
+        Task::Ready(Some(Ok(())))
     }
 
     fn clone_on_split(
@@ -322,34 +329,20 @@ impl Item for FeedbackEditor {
         ))
     }
 
-    fn serialized_item_kind() -> Option<&'static str> {
-        None
-    }
-
-    fn deserialize(
-        _: ModelHandle<Project>,
-        _: WeakViewHandle<Workspace>,
-        _: workspace::WorkspaceId,
-        _: workspace::ItemId,
-        _: &mut ViewContext<workspace::Pane>,
-    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
-        unreachable!()
-    }
-
     fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
         Some(Box::new(handle.clone()))
     }
 
-    fn act_as_type(
-        &self,
+    fn act_as_type<'a>(
+        &'a self,
         type_id: TypeId,
-        self_handle: &ViewHandle<Self>,
-        _: &AppContext,
-    ) -> Option<AnyViewHandle> {
+        self_handle: &'a ViewHandle<Self>,
+        _: &'a AppContext,
+    ) -> Option<&'a AnyViewHandle> {
         if type_id == TypeId::of::<Self>() {
-            Some(self_handle.into())
+            Some(self_handle)
         } else if type_id == TypeId::of::<Editor>() {
-            Some((&self.editor).into())
+            Some(&self.editor)
         } else {
             None
         }

crates/feedback/src/feedback_info_text.rs 🔗

@@ -1,10 +1,12 @@
 use gpui::{
-    elements::Label, Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle,
+    elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
+    CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
+    ViewHandle,
 };
 use settings::Settings;
 use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
-use crate::feedback_editor::FeedbackEditor;
+use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
 
 pub struct FeedbackInfoText {
     active_item: Option<ViewHandle<FeedbackEditor>>,
@@ -29,9 +31,44 @@ impl View for FeedbackInfoText {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = cx.global::<Settings>().theme.clone();
-        let text = "We read whatever you submit here. For issues and discussions, visit the community repo on GitHub.";
-        Label::new(text.to_string(), theme.feedback.info_text.text.clone())
-            .contained()
+
+        Flex::row()
+            .with_child(
+                Text::new(
+                    "We read whatever you submit here. For issues and discussions, visit the ",
+                    theme.feedback.info_text_default.text.clone(),
+                )
+                .with_soft_wrap(false)
+                .aligned()
+                .boxed(),
+            )
+            .with_child(
+                MouseEventHandler::<OpenZedCommunityRepo>::new(0, cx, |state, _| {
+                    let contained_text = if state.hovered() {
+                        &theme.feedback.link_text_hover
+                    } else {
+                        &theme.feedback.link_text_default
+                    };
+
+                    Label::new("community repo", contained_text.text.clone())
+                        .contained()
+                        .aligned()
+                        .left()
+                        .clipped()
+                        .boxed()
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, |_, cx| {
+                    cx.dispatch_action(OpenZedCommunityRepo)
+                })
+                .boxed(),
+            )
+            .with_child(
+                Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
+                    .with_soft_wrap(false)
+                    .aligned()
+                    .boxed(),
+            )
             .aligned()
             .left()
             .clipped()

crates/feedback/src/submit_feedback_button.rs 🔗

@@ -34,7 +34,7 @@ impl View for SubmitFeedbackButton {
         enum SubmitFeedbackButton {}
         MouseEventHandler::<SubmitFeedbackButton>::new(0, cx, |state, _| {
             let style = theme.feedback.submit_button.style_for(state, false);
-            Label::new("Submit as Markdown".into(), style.text.clone())
+            Label::new("Submit as Markdown", style.text.clone())
                 .contained()
                 .with_style(style.container)
                 .boxed()

crates/file_finder/Cargo.toml 🔗

@@ -19,11 +19,11 @@ settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.9"

crates/file_finder/src/file_finder.rs 🔗

@@ -23,6 +23,7 @@ pub struct FileFinder {
     latest_search_id: usize,
     latest_search_did_cancel: bool,
     latest_search_query: String,
+    relative_to: Option<Arc<Path>>,
     matches: Vec<PathMatch>,
     selected: Option<(usize, Arc<Path>)>,
     cancel_flag: Arc<AtomicBool>,
@@ -50,7 +51,7 @@ impl View for FileFinder {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -90,7 +91,11 @@ impl FileFinder {
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
         workspace.toggle_modal(cx, |workspace, cx| {
             let project = workspace.project().clone();
-            let finder = cx.add_view(|cx| Self::new(project, cx));
+            let relative_to = workspace
+                .active_item(cx)
+                .and_then(|item| item.project_path(cx))
+                .map(|project_path| project_path.path.clone());
+            let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
             cx.subscribe(&finder, Self::on_event).detach();
             finder
         });
@@ -115,7 +120,11 @@ impl FileFinder {
         }
     }
 
-    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        project: ModelHandle<Project>,
+        relative_to: Option<Arc<Path>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let handle = cx.weak_handle();
         cx.observe(&project, Self::project_updated).detach();
         Self {
@@ -125,6 +134,7 @@ impl FileFinder {
             latest_search_id: 0,
             latest_search_did_cancel: false,
             latest_search_query: String::new(),
+            relative_to,
             matches: Vec::new(),
             selected: None,
             cancel_flag: Arc::new(AtomicBool::new(false)),
@@ -137,6 +147,7 @@ impl FileFinder {
     }
 
     fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        let relative_to = self.relative_to.clone();
         let worktrees = self
             .project
             .read(cx)
@@ -165,6 +176,7 @@ impl FileFinder {
             let matches = fuzzy::match_path_sets(
                 candidate_sets.as_slice(),
                 &query,
+                relative_to,
                 false,
                 100,
                 &cancel_flag,
@@ -317,9 +329,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -342,8 +352,8 @@ mod tests {
             let active_item = active_pane.read(cx).active_item().unwrap();
             assert_eq!(
                 active_item
-                    .to_any()
-                    .downcast::<Editor>()
+                    .as_any()
+                    .downcast_ref::<Editor>()
                     .unwrap()
                     .read(cx)
                     .title(cx),
@@ -373,11 +383,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
         let query = "hi".to_string();
         finder
@@ -449,11 +457,9 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
         finder
             .update(cx, |f, cx| f.spawn_search("hi".into(), cx))
             .await;
@@ -475,11 +481,9 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -529,11 +533,10 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
         // Run a search that matches two files with the same relative path.
         finder
@@ -551,6 +554,46 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "dir1": { "a.txt": "" },
+                    "dir2": {
+                        "a.txt": "",
+                        "b.txt": ""
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+
+        // When workspace has an active item, sort items which are closer to that item
+        // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
+        // so that one should be sorted earlier
+        let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
+        let (_, finder) =
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
+
+        finder
+            .update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
+            .await;
+
+        finder.read_with(cx, |f, _| {
+            assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
+            assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
+        });
+    }
+
     #[gpui::test]
     async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
         let app_state = cx.update(AppState::test);
@@ -569,11 +612,9 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
-            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
+            cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
         finder
             .update(cx, |f, cx| f.spawn_search("dir".into(), cx))
             .await;

crates/fs/Cargo.toml 🔗

@@ -24,6 +24,7 @@ smol = "1.2.5"
 regex = "1.5"
 git2 = { version = "0.15", default-features = false }
 serde = { workspace = true }
+serde_derive = { workspace = true }
 serde_json = { workspace = true }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 libc = "0.2"

crates/fs/src/fs.rs 🔗

@@ -380,6 +380,8 @@ struct FakeFsState {
     next_inode: u64,
     next_mtime: SystemTime,
     event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
+    events_paused: bool,
+    buffered_events: Vec<fsevent::Event>,
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -483,15 +485,21 @@ impl FakeFsState {
         I: IntoIterator<Item = T>,
         T: Into<PathBuf>,
     {
-        let events = paths
-            .into_iter()
-            .map(|path| fsevent::Event {
+        self.buffered_events
+            .extend(paths.into_iter().map(|path| fsevent::Event {
                 event_id: 0,
                 flags: fsevent::StreamFlags::empty(),
                 path: path.into(),
-            })
-            .collect::<Vec<_>>();
+            }));
+
+        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()
@@ -514,6 +522,8 @@ impl FakeFs {
                 next_mtime: SystemTime::UNIX_EPOCH,
                 next_inode: 1,
                 event_txs: Default::default(),
+                buffered_events: Vec::new(),
+                events_paused: false,
             }),
         })
     }
@@ -567,6 +577,18 @@ impl FakeFs {
         state.emit_event(&[path]);
     }
 
+    pub async fn pause_events(&self) {
+        self.state.lock().await.events_paused = true;
+    }
+
+    pub async fn buffered_event_count(&self) -> usize {
+        self.state.lock().await.buffered_events.len()
+    }
+
+    pub async fn flush_events(&self, count: usize) {
+        self.state.lock().await.flush_events(count);
+    }
+
     #[must_use]
     pub fn insert_tree<'a>(
         &'a self,
@@ -868,7 +890,7 @@ impl Fs for FakeFs {
             .ok_or_else(|| anyhow!("cannot remove the root"))?;
         let base_name = path.file_name().unwrap();
 
-        let state = self.state.lock().await;
+        let mut state = self.state.lock().await;
         let parent_entry = state.read_path(parent_path).await?;
         let mut parent_entry = parent_entry.lock().await;
         let entry = parent_entry
@@ -892,7 +914,7 @@ impl Fs for FakeFs {
                 e.remove();
             }
         }
-
+        state.emit_event(&[path]);
         Ok(())
     }
 

crates/fs/src/repository.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::Result;
 use collections::HashMap;
 use parking_lot::Mutex;
 use std::{
-    path::{Path, PathBuf},
+    path::{Component, Path, PathBuf},
     sync::Arc,
 };
 
@@ -27,7 +27,11 @@ impl GitRepository for LibGitRepository {
         fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
             const STAGE_NORMAL: i32 = 0;
             let index = repo.index()?;
-            let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
+
+            // 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),
             };
@@ -69,3 +73,32 @@ impl GitRepository for FakeGitRepository {
         state.index_contents.get(path).cloned()
     }
 }
+
+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(()),
+    }
+}

crates/fuzzy/src/matcher.rs 🔗

@@ -443,6 +443,7 @@ mod tests {
                 positions: Vec::new(),
                 path: candidate.path.clone(),
                 path_prefix: "".into(),
+                distance_to_relative_ancestor: usize::MAX,
             },
         );
 

crates/fuzzy/src/paths.rs 🔗

@@ -25,6 +25,9 @@ pub struct PathMatch {
     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 {
@@ -78,6 +81,11 @@ impl Ord for PathMatch {
             .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))
     }
 }
@@ -85,6 +93,7 @@ impl Ord for PathMatch {
 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,
@@ -111,6 +120,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     background
         .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;
@@ -149,6 +159,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
                                     positions: Vec::new(),
                                     path: candidate.path.clone(),
                                     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(),
+                                            )
+                                        },
+                                    ),
                                 },
                             );
                         }
@@ -172,3 +191,30 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'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/go_to_line/Cargo.toml 🔗

@@ -15,4 +15,4 @@ menu = { path = "../menu" }
 settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }

crates/gpui/Cargo.toml 🔗

@@ -36,12 +36,14 @@ parking = "2.0.0"
 parking_lot = "0.11.1"
 pathfinder_color = "0.5"
 pathfinder_geometry = "0.5"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = "0.8.3"
 resvg = "0.14"
+schemars = "0.8"
 seahash = "4.1"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = "1.0"
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 time = { version = "0.3", features = ["serde", "serde-well-known"] }

crates/gpui/examples/text.rs 🔗

@@ -56,7 +56,10 @@ impl gpui::Element for TextElement {
         cx: &mut gpui::PaintContext,
     ) -> Self::PaintState {
         let font_size = 12.;
-        let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap();
+        let family = cx
+            .font_cache
+            .load_family(&["SF Pro Display"], &Default::default())
+            .unwrap();
         let normal = RunStyle {
             font_id: cx
                 .font_cache

crates/gpui/src/app.rs 🔗

@@ -31,7 +31,7 @@ use uuid::Uuid;
 
 pub use action::*;
 use callback_collection::CallbackCollection;
-use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
+use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
 pub use menu::*;
 use platform::Event;
 #[cfg(any(test, feature = "test-support"))]
@@ -86,7 +86,7 @@ pub trait View: Entity + Sized {
     }
     fn default_keymap_context() -> keymap_matcher::KeymapContext {
         let mut cx = keymap_matcher::KeymapContext::default();
-        cx.set.insert(Self::ui_name().into());
+        cx.add_identifier(Self::ui_name());
         cx
     }
     fn debug_json(&self, _: &AppContext) -> serde_json::Value {
@@ -254,6 +254,19 @@ impl App {
         self
     }
 
+    /// Handle the application being re-activated when no windows are open.
+    pub fn on_reopen<F>(&mut self, mut callback: F) -> &mut Self
+    where
+        F: 'static + FnMut(&mut MutableAppContext),
+    {
+        let cx = self.0.clone();
+        self.0
+            .borrow_mut()
+            .foreground_platform
+            .on_reopen(Box::new(move || callback(&mut *cx.borrow_mut())));
+        self
+    }
+
     pub fn on_event<F>(&mut self, mut callback: F) -> &mut Self
     where
         F: 'static + FnMut(Event, &mut MutableAppContext) -> bool,
@@ -276,9 +289,7 @@ impl App {
         self.0
             .borrow_mut()
             .foreground_platform
-            .on_open_urls(Box::new(move |paths| {
-                callback(paths, &mut *cx.borrow_mut())
-            }));
+            .on_open_urls(Box::new(move |urls| callback(urls, &mut *cx.borrow_mut())));
         self
     }
 
@@ -474,6 +485,7 @@ type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut MutableAppCon
 type KeystrokeCallback = Box<
     dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
 >;
+type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 
@@ -484,7 +496,9 @@ pub struct MutableAppContext {
     cx: AppContext,
     action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
     capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
+    // Entity Types -> { Action Types -> Action Handlers }
     actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
+    // Action Types -> Action Handlers
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
     keystroke_matcher: KeymapMatcher,
     next_entity_id: usize,
@@ -503,6 +517,7 @@ pub struct MutableAppContext {
     window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
     window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
     keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
+    active_labeled_task_observations: CallbackCollection<(), ActiveLabeledTasksCallback>,
 
     #[allow(clippy::type_complexity)]
     presenters_and_platform_windows:
@@ -514,6 +529,8 @@ pub struct MutableAppContext {
     pending_flushes: usize,
     flushing_effects: bool,
     halt_action_dispatch: bool,
+    next_labeled_task_id: usize,
+    active_labeled_tasks: BTreeMap<usize, &'static str>,
 }
 
 impl MutableAppContext {
@@ -562,6 +579,7 @@ impl MutableAppContext {
             window_bounds_observations: Default::default(),
             keystroke_observations: Default::default(),
             action_dispatch_observations: Default::default(),
+            active_labeled_task_observations: Default::default(),
             presenters_and_platform_windows: Default::default(),
             foreground,
             pending_effects: VecDeque::new(),
@@ -570,6 +588,8 @@ impl MutableAppContext {
             pending_flushes: 0,
             flushing_effects: false,
             halt_action_dispatch: false,
+            next_labeled_task_id: 0,
+            active_labeled_tasks: Default::default(),
         }
     }
 
@@ -579,17 +599,20 @@ impl MutableAppContext {
 
     pub fn quit(&mut self) {
         let mut futures = Vec::new();
-        for model_id in self.cx.models.keys().copied().collect::<Vec<_>>() {
-            let mut model = self.cx.models.remove(&model_id).unwrap();
-            futures.extend(model.app_will_quit(self));
-            self.cx.models.insert(model_id, model);
-        }
 
-        for view_id in self.cx.views.keys().copied().collect::<Vec<_>>() {
-            let mut view = self.cx.views.remove(&view_id).unwrap();
-            futures.extend(view.app_will_quit(self));
-            self.cx.views.insert(view_id, view);
-        }
+        self.update(|cx| {
+            for model_id in cx.models.keys().copied().collect::<Vec<_>>() {
+                let mut model = cx.cx.models.remove(&model_id).unwrap();
+                futures.extend(model.app_will_quit(cx));
+                cx.cx.models.insert(model_id, model);
+            }
+
+            for view_id in cx.views.keys().copied().collect::<Vec<_>>() {
+                let mut view = cx.cx.views.remove(&view_id).unwrap();
+                futures.extend(view.app_will_quit(cx));
+                cx.cx.views.insert(view_id, view);
+            }
+        });
 
         self.remove_all_windows();
 
@@ -753,6 +776,12 @@ impl MutableAppContext {
             })
     }
 
+    pub fn has_window(&self, window_id: usize) -> bool {
+        self.window_ids()
+            .find(|window| window == &window_id)
+            .is_some()
+    }
+
     pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
         self.cx.windows.keys().copied()
     }
@@ -794,6 +823,12 @@ impl MutableAppContext {
         window.screen().display_uuid()
     }
 
+    pub fn active_labeled_tasks<'a>(
+        &'a self,
+    ) -> impl DoubleEndedIterator<Item = &'static str> + 'a {
+        self.active_labeled_tasks.values().cloned()
+    }
+
     pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
         let window_id = params.window_id;
         let view_id = params.view_id;
@@ -1160,6 +1195,19 @@ impl MutableAppContext {
         )
     }
 
+    pub fn observe_active_labeled_tasks<F>(&mut self, callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut MutableAppContext) -> bool,
+    {
+        let subscription_id = post_inc(&mut self.next_subscription_id);
+        self.active_labeled_task_observations
+            .add_callback((), subscription_id, Box::new(callback));
+        Subscription::ActiveLabeledTasksObservation(
+            self.active_labeled_task_observations
+                .subscribe((), subscription_id),
+        )
+    }
+
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
         self.pending_effects.push_back(Effect::Deferred {
             callback: Box::new(callback),
@@ -1213,20 +1261,34 @@ impl MutableAppContext {
         action: &dyn Action,
     ) -> Option<SmallVec<[Keystroke; 2]>> {
         let mut contexts = Vec::new();
-        for view_id in self.ancestors(window_id, view_id) {
+        let mut handler_depth = None;
+        for (i, view_id) in self.ancestors(window_id, view_id).enumerate() {
             if let Some(view) = self.views.get(&(window_id, view_id)) {
+                if let Some(actions) = self.actions.get(&view.as_any().type_id()) {
+                    if actions.contains_key(&action.as_any().type_id()) {
+                        handler_depth = Some(i);
+                    }
+                }
                 contexts.push(view.keymap_context(self));
             }
         }
 
+        if self.global_actions.contains_key(&action.as_any().type_id()) {
+            handler_depth = Some(contexts.len())
+        }
+
         self.keystroke_matcher
             .bindings_for_action_type(action.as_any().type_id())
             .find_map(|b| {
-                if b.match_context(&contexts) {
-                    Some(b.keystrokes().into())
-                } else {
-                    None
-                }
+                handler_depth
+                    .map(|highest_handler| {
+                        if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) {
+                            Some(b.keystrokes().into())
+                        } else {
+                            None
+                        }
+                    })
+                    .flatten()
             })
     }
 
@@ -1235,29 +1297,42 @@ impl MutableAppContext {
         window_id: usize,
         view_id: usize,
     ) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
-        let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect();
-
         let mut contexts = Vec::new();
-        for view_id in self.ancestors(window_id, view_id) {
+        let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
+        for (depth, view_id) in self.ancestors(window_id, view_id).enumerate() {
             if let Some(view) = self.views.get(&(window_id, view_id)) {
                 contexts.push(view.keymap_context(self));
                 let view_type = view.as_any().type_id();
                 if let Some(actions) = self.actions.get(&view_type) {
-                    action_types.extend(actions.keys().copied());
+                    handler_depths_by_action_type.extend(
+                        actions
+                            .keys()
+                            .copied()
+                            .map(|action_type| (action_type, depth)),
+                    );
                 }
             }
         }
 
+        handler_depths_by_action_type.extend(
+            self.global_actions
+                .keys()
+                .copied()
+                .map(|action_type| (action_type, contexts.len())),
+        );
+
         self.action_deserializers
             .iter()
             .filter_map(move |(name, (type_id, deserialize))| {
-                if action_types.contains(type_id) {
+                if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
                     Some((
                         *name,
                         deserialize("{}").ok()?,
                         self.keystroke_matcher
                             .bindings_for_action_type(*type_id)
-                            .filter(|b| b.match_context(&contexts))
+                            .filter(|b| {
+                                (0..=action_depth).any(|depth| b.match_context(&contexts[depth..]))
+                            })
                             .collect(),
                     ))
                 } else {
@@ -1268,7 +1343,7 @@ impl MutableAppContext {
 
     pub fn is_action_available(&self, action: &dyn Action) -> bool {
         let action_type = action.as_any().type_id();
-        if let Some(window_id) = self.cx.platform.key_window_id() {
+        if let Some(window_id) = self.cx.platform.main_window_id() {
             if let Some(focused_view_id) = self.focused_view_id(window_id) {
                 for view_id in self.ancestors(window_id, focused_view_id) {
                     if let Some(view) = self.views.get(&(window_id, view_id)) {
@@ -1421,15 +1496,11 @@ impl MutableAppContext {
         if let Some(focused_view_id) = self.focused_view_id(window_id) {
             let dispatch_path = self
                 .ancestors(window_id, focused_view_id)
-                .map(|view_id| {
-                    (
-                        view_id,
-                        self.cx
-                            .views
-                            .get(&(window_id, view_id))
-                            .unwrap()
-                            .keymap_context(self.as_ref()),
-                    )
+                .filter_map(|view_id| {
+                    self.cx
+                        .views
+                        .get(&(window_id, view_id))
+                        .map(|view| (view_id, view.keymap_context(self.as_ref())))
                 })
                 .collect();
 
@@ -1562,14 +1633,14 @@ impl MutableAppContext {
             this.cx.windows.insert(
                 window_id,
                 Window {
-                    root_view: root_view.clone().into(),
+                    root_view: root_view.clone().into_any(),
                     focused_view_id: Some(root_view.id()),
                     is_active: false,
                     invalidation: None,
                     is_fullscreen: false,
                 },
             );
-            root_view.update(this, |view, cx| view.focus_in(cx.handle().into(), cx));
+            root_view.update(this, |view, cx| view.focus_in(cx.handle().into_any(), cx));
 
             let window =
                 this.cx
@@ -1591,17 +1662,18 @@ impl MutableAppContext {
             let root_view = this
                 .build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
                 .unwrap();
+            let focused_view_id = root_view.id();
             this.cx.windows.insert(
                 window_id,
                 Window {
-                    root_view: root_view.clone().into(),
-                    focused_view_id: Some(root_view.id()),
+                    root_view: root_view.clone().into_any(),
+                    focused_view_id: Some(focused_view_id),
                     is_active: false,
                     invalidation: None,
                     is_fullscreen: false,
                 },
             );
-            root_view.update(this, |view, cx| view.focus_in(cx.handle().into(), cx));
+            root_view.update(this, |view, cx| view.focus_in(cx.handle().into_any(), cx));
 
             let status_item = this.cx.platform.add_status_item();
             this.register_platform_window(window_id, status_item);
@@ -1712,7 +1784,7 @@ impl MutableAppContext {
                 .build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
                 .unwrap();
             let window = this.cx.windows.get_mut(&window_id).unwrap();
-            window.root_view = root_view.clone().into();
+            window.root_view = root_view.clone().into_any();
             window.focused_view_id = Some(root_view.id());
             root_view
         })
@@ -1741,16 +1813,11 @@ impl MutableAppContext {
         )
     }
 
-    pub fn add_view<T, F>(
-        &mut self,
-        parent_handle: impl Into<AnyViewHandle>,
-        build_view: F,
-    ) -> ViewHandle<T>
+    pub fn add_view<T, F>(&mut self, parent_handle: &AnyViewHandle, build_view: F) -> ViewHandle<T>
     where
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> T,
     {
-        let parent_handle = parent_handle.into();
         self.build_and_insert_view(
             parent_handle.window_id,
             ParentId::View(parent_handle.view_id),
@@ -2042,6 +2109,17 @@ impl MutableAppContext {
                             handled_by,
                             result,
                         } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
+                        Effect::ActiveLabeledTasksChanged => {
+                            self.handle_active_labeled_tasks_changed_effect()
+                        }
+                        Effect::ActiveLabeledTasksObservation {
+                            subscription_id,
+                            callback,
+                        } => self.active_labeled_task_observations.add_callback(
+                            (),
+                            subscription_id,
+                            callback,
+                        ),
                     }
                     self.pending_notifications.clear();
                     self.remove_dropped_entities();
@@ -2449,26 +2527,68 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_active_labeled_tasks_changed_effect(&mut self) {
+        self.active_labeled_task_observations
+            .clone()
+            .emit((), self, move |callback, this| {
+                callback(this);
+                true
+            });
+    }
+
     pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
         self.pending_effects
             .push_back(Effect::Focus { window_id, view_id });
     }
 
-    pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
+    fn spawn_internal<F, Fut, T>(&mut self, task_name: Option<&'static str>, f: F) -> Task<T>
     where
         F: FnOnce(AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = T>,
         T: 'static,
     {
+        let label_id = task_name.map(|task_name| {
+            let id = post_inc(&mut self.next_labeled_task_id);
+            self.active_labeled_tasks.insert(id, task_name);
+            self.pending_effects
+                .push_back(Effect::ActiveLabeledTasksChanged);
+            id
+        });
+
         let future = f(self.to_async());
         let cx = self.to_async();
         self.foreground.spawn(async move {
             let result = future.await;
-            cx.0.borrow_mut().flush_effects();
+            let mut cx = cx.0.borrow_mut();
+
+            if let Some(completed_label_id) = label_id {
+                cx.active_labeled_tasks.remove(&completed_label_id);
+                cx.pending_effects
+                    .push_back(Effect::ActiveLabeledTasksChanged);
+            }
+            cx.flush_effects();
             result
         })
     }
 
+    pub fn spawn_labeled<F, Fut, T>(&mut self, task_name: &'static str, f: F) -> Task<T>
+    where
+        F: FnOnce(AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = T>,
+        T: 'static,
+    {
+        self.spawn_internal(Some(task_name), f)
+    }
+
+    pub fn spawn<F, Fut, T>(&mut self, f: F) -> Task<T>
+    where
+        F: FnOnce(AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = T>,
+        T: 'static,
+    {
+        self.spawn_internal(None, f)
+    }
+
     pub fn to_async(&self) -> AsyncAppContext {
         AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap())
     }
@@ -2649,6 +2769,12 @@ impl AppContext {
         Some(self.views.get(&(window_id, view_id))?.ui_name())
     }
 
+    pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option<TypeId> {
+        self.views
+            .get(&(window_id, view_id))
+            .map(|view| view.as_any().type_id())
+    }
+
     pub fn background(&self) -> &Arc<executor::Background> {
         &self.background
     }
@@ -2698,8 +2824,7 @@ impl AppContext {
         }
     }
 
-    pub fn is_child_focused(&self, view: impl Into<AnyViewHandle>) -> bool {
-        let view = view.into();
+    pub fn is_child_focused(&self, view: &AnyViewHandle) -> bool {
         if let Some(focused_view_id) = self.focused_view_id(view.window_id) {
             self.ancestors(view.window_id, focused_view_id)
                 .skip(1) // Skip self id
@@ -2907,6 +3032,11 @@ pub enum Effect {
         window_id: usize,
         callback: WindowShouldCloseSubscriptionCallback,
     },
+    ActiveLabeledTasksChanged,
+    ActiveLabeledTasksObservation {
+        subscription_id: usize,
+        callback: ActiveLabeledTasksCallback,
+    },
 }
 
 impl Debug for Effect {
@@ -3066,6 +3196,16 @@ impl Debug for Effect {
                 )
                 .field("result", result)
                 .finish(),
+            Effect::ActiveLabeledTasksChanged => {
+                f.debug_struct("Effect::ActiveLabeledTasksChanged").finish()
+            }
+            Effect::ActiveLabeledTasksObservation {
+                subscription_id,
+                callback: _,
+            } => f
+                .debug_struct("Effect::ActiveLabeledTasksObservation")
+                .field("subscription_id", subscription_id)
+                .finish(),
         }
     }
 }
@@ -3223,7 +3363,7 @@ where
     ) {
         let mut cx = ViewContext::new(cx, window_id, view_id);
         let focused_view_handle: AnyViewHandle = if view_id == focused_id {
-            cx.handle().into()
+            cx.handle().into_any()
         } else {
             let focused_type = cx
                 .views
@@ -3245,7 +3385,7 @@ where
     ) {
         let mut cx = ViewContext::new(cx, window_id, view_id);
         let blurred_view_handle: AnyViewHandle = if view_id == blurred_id {
-            cx.handle().into()
+            cx.handle().into_any()
         } else {
             let blurred_type = cx
                 .views
@@ -3480,7 +3620,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
         WeakModelHandle::new(self.model_id)
     }
 
-    pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(ModelHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -3490,7 +3630,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
         self.app.spawn(|cx| f(handle, cx))
     }
 
-    pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn_weak<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(WeakModelHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -3649,11 +3789,7 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.debug_elements(self.window_id).unwrap()
     }
 
-    pub fn focus<S>(&mut self, handle: S)
-    where
-        S: Into<AnyViewHandle>,
-    {
-        let handle = handle.into();
+    pub fn focus(&mut self, handle: &AnyViewHandle) {
         self.app.focus(handle.window_id, Some(handle.view_id));
     }
 
@@ -3745,8 +3881,7 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.cx.parent(self.window_id, self.view_id)
     }
 
-    pub fn reparent(&mut self, view_handle: impl Into<AnyViewHandle>) {
-        let view_handle = view_handle.into();
+    pub fn reparent(&mut self, view_handle: &AnyViewHandle) {
         if self.window_id != view_handle.window_id {
             panic!("Can't reparent view to a view from a different window");
         }
@@ -3771,7 +3906,7 @@ impl<'a, T: View> ViewContext<'a, T> {
                 .build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
                 .unwrap();
             let window = this.cx.windows.get_mut(&window_id).unwrap();
-            window.root_view = root_view.clone().into();
+            window.root_view = root_view.clone().into_any();
             window.focused_view_id = Some(root_view.id());
             root_view
         })
@@ -3947,6 +4082,23 @@ impl<'a, T: View> ViewContext<'a, T> {
             })
     }
 
+    pub fn observe_active_labeled_tasks<F>(&mut self, mut callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut T, &mut ViewContext<T>),
+    {
+        let observer = self.weak_handle();
+        self.app.observe_active_labeled_tasks(move |cx| {
+            if let Some(observer) = observer.upgrade(cx) {
+                observer.update(cx, |observer, cx| {
+                    callback(observer, cx);
+                });
+                true
+            } else {
+                false
+            }
+        })
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.view_id,
@@ -3993,7 +4145,17 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.halt_action_dispatch = false;
     }
 
-    pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn_labeled<F, Fut, S>(&mut self, task_label: &'static str, f: F) -> Task<S>
+    where
+        F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = S>,
+        S: 'static,
+    {
+        let handle = self.handle();
+        self.app.spawn_labeled(task_label, |cx| f(handle, cx))
+    }
+
+    pub fn spawn<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -4003,7 +4165,7 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.spawn(|cx| f(handle, cx))
     }
 
-    pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
+    pub fn spawn_weak<F, Fut, S>(&mut self, f: F) -> Task<S>
     where
         F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
         Fut: 'static + Future<Output = S>,
@@ -4038,10 +4200,10 @@ pub struct RenderContext<'a, T: View> {
 
 #[derive(Debug, Clone, Default)]
 pub struct MouseState {
-    hovered: bool,
-    clicked: Option<MouseButton>,
-    accessed_hovered: bool,
-    accessed_clicked: bool,
+    pub(crate) hovered: bool,
+    pub(crate) clicked: Option<MouseButton>,
+    pub(crate) accessed_hovered: bool,
+    pub(crate) accessed_clicked: bool,
 }
 
 impl MouseState {
@@ -4293,32 +4455,23 @@ pub enum EntityLocation {
 }
 
 pub struct ModelHandle<T: Entity> {
-    model_id: usize,
+    any_handle: AnyModelHandle,
     model_type: PhantomData<T>,
-    ref_counts: Arc<Mutex<RefCounts>>,
+}
 
-    #[cfg(any(test, feature = "test-support"))]
-    handle_id: usize,
+impl<T: Entity> Deref for ModelHandle<T> {
+    type Target = AnyModelHandle;
+
+    fn deref(&self) -> &Self::Target {
+        &self.any_handle
+    }
 }
 
 impl<T: Entity> ModelHandle<T> {
     fn new(model_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
-        ref_counts.lock().inc_model(model_id);
-
-        #[cfg(any(test, feature = "test-support"))]
-        let handle_id = ref_counts
-            .lock()
-            .leak_detector
-            .lock()
-            .handle_created(Some(type_name::<T>()), model_id);
-
         Self {
-            model_id,
+            any_handle: AnyModelHandle::new(model_id, TypeId::of::<T>(), ref_counts.clone()),
             model_type: PhantomData,
-            ref_counts: ref_counts.clone(),
-
-            #[cfg(any(test, feature = "test-support"))]
-            handle_id,
         }
     }
 
@@ -4402,19 +4555,6 @@ impl<T: Entity> Debug for ModelHandle<T> {
 unsafe impl<T: Entity> Send for ModelHandle<T> {}
 unsafe impl<T: Entity> Sync for ModelHandle<T> {}
 
-impl<T: Entity> Drop for ModelHandle<T> {
-    fn drop(&mut self) {
-        let mut ref_counts = self.ref_counts.lock();
-        ref_counts.dec_model(self.model_id);
-
-        #[cfg(any(test, feature = "test-support"))]
-        ref_counts
-            .leak_detector
-            .lock()
-            .handle_dropped(self.model_id, self.handle_id);
-    }
-}
-
 impl<T: Entity> Handle<T> for ModelHandle<T> {
     type Weak = WeakModelHandle<T>;
 
@@ -4439,10 +4579,24 @@ impl<T: Entity> Handle<T> for ModelHandle<T> {
 }
 
 pub struct WeakModelHandle<T> {
-    model_id: usize,
+    any_handle: AnyWeakModelHandle,
     model_type: PhantomData<T>,
 }
 
+impl<T> WeakModelHandle<T> {
+    pub fn into_any(self) -> AnyWeakModelHandle {
+        self.any_handle
+    }
+}
+
+impl<T> Deref for WeakModelHandle<T> {
+    type Target = AnyWeakModelHandle;
+
+    fn deref(&self) -> &Self::Target {
+        &self.any_handle
+    }
+}
+
 impl<T> WeakHandle for WeakModelHandle<T> {
     fn id(&self) -> usize {
         self.model_id
@@ -4455,7 +4609,10 @@ unsafe impl<T> Sync for WeakModelHandle<T> {}
 impl<T: Entity> WeakModelHandle<T> {
     fn new(model_id: usize) -> Self {
         Self {
-            model_id,
+            any_handle: AnyWeakModelHandle {
+                model_id,
+                model_type: TypeId::of::<T>(),
+            },
             model_type: PhantomData,
         }
     }
@@ -4496,7 +4653,7 @@ impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
 impl<T> Clone for WeakModelHandle<T> {
     fn clone(&self) -> Self {
         Self {
-            model_id: self.model_id,
+            any_handle: self.any_handle.clone(),
             model_type: PhantomData,
         }
     }
@@ -4504,33 +4661,30 @@ impl<T> Clone for WeakModelHandle<T> {
 
 impl<T> Copy for WeakModelHandle<T> {}
 
+#[repr(transparent)]
 pub struct ViewHandle<T> {
-    window_id: usize,
-    view_id: usize,
+    any_handle: AnyViewHandle,
     view_type: PhantomData<T>,
-    ref_counts: Arc<Mutex<RefCounts>>,
-    #[cfg(any(test, feature = "test-support"))]
-    handle_id: usize,
+}
+
+impl<T> Deref for ViewHandle<T> {
+    type Target = AnyViewHandle;
+
+    fn deref(&self) -> &Self::Target {
+        &self.any_handle
+    }
 }
 
 impl<T: View> ViewHandle<T> {
     fn new(window_id: usize, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
-        ref_counts.lock().inc_view(window_id, view_id);
-        #[cfg(any(test, feature = "test-support"))]
-        let handle_id = ref_counts
-            .lock()
-            .leak_detector
-            .lock()
-            .handle_created(Some(type_name::<T>()), view_id);
-
         Self {
-            window_id,
-            view_id,
+            any_handle: AnyViewHandle::new(
+                window_id,
+                view_id,
+                TypeId::of::<T>(),
+                ref_counts.clone(),
+            ),
             view_type: PhantomData,
-            ref_counts: ref_counts.clone(),
-
-            #[cfg(any(test, feature = "test-support"))]
-            handle_id,
         }
     }
 
@@ -4538,6 +4692,10 @@ impl<T: View> ViewHandle<T> {
         WeakViewHandle::new(self.window_id, self.view_id)
     }
 
+    pub fn into_any(self) -> AnyViewHandle {
+        self.any_handle
+    }
+
     pub fn window_id(&self) -> usize {
         self.window_id
     }
@@ -4633,20 +4791,6 @@ impl<T> Debug for ViewHandle<T> {
     }
 }
 
-impl<T> Drop for ViewHandle<T> {
-    fn drop(&mut self) {
-        self.ref_counts
-            .lock()
-            .dec_view(self.window_id, self.view_id);
-        #[cfg(any(test, feature = "test-support"))]
-        self.ref_counts
-            .lock()
-            .leak_detector
-            .lock()
-            .handle_dropped(self.view_id, self.handle_id);
-    }
-}
-
 impl<T: View> Handle<T> for ViewHandle<T> {
     type Weak = WeakViewHandle<T>;
 
@@ -4725,19 +4869,18 @@ impl AnyViewHandle {
 
     pub fn downcast<T: View>(self) -> Option<ViewHandle<T>> {
         if self.is::<T>() {
-            let result = Some(ViewHandle {
-                window_id: self.window_id,
-                view_id: self.view_id,
-                ref_counts: self.ref_counts.clone(),
+            Some(ViewHandle {
+                any_handle: self,
                 view_type: PhantomData,
-                #[cfg(any(test, feature = "test-support"))]
-                handle_id: self.handle_id,
-            });
-            unsafe {
-                Arc::decrement_strong_count(Arc::as_ptr(&self.ref_counts));
-            }
-            std::mem::forget(self);
-            result
+            })
+        } else {
+            None
+        }
+    }
+
+    pub fn downcast_ref<T: View>(&self) -> Option<&ViewHandle<T>> {
+        if self.is::<T>() {
+            Some(unsafe { mem::transmute(self) })
         } else {
             None
         }
@@ -4773,42 +4916,6 @@ impl Clone for AnyViewHandle {
     }
 }
 
-impl From<&AnyViewHandle> for AnyViewHandle {
-    fn from(handle: &AnyViewHandle) -> Self {
-        handle.clone()
-    }
-}
-
-impl<T: View> From<&ViewHandle<T>> for AnyViewHandle {
-    fn from(handle: &ViewHandle<T>) -> Self {
-        Self::new(
-            handle.window_id,
-            handle.view_id,
-            TypeId::of::<T>(),
-            handle.ref_counts.clone(),
-        )
-    }
-}
-
-impl<T: View> From<ViewHandle<T>> for AnyViewHandle {
-    fn from(handle: ViewHandle<T>) -> Self {
-        let any_handle = AnyViewHandle {
-            window_id: handle.window_id,
-            view_id: handle.view_id,
-            view_type: TypeId::of::<T>(),
-            ref_counts: handle.ref_counts.clone(),
-            #[cfg(any(test, feature = "test-support"))]
-            handle_id: handle.handle_id,
-        };
-
-        unsafe {
-            Arc::decrement_strong_count(Arc::as_ptr(&handle.ref_counts));
-        }
-        std::mem::forget(handle);
-        any_handle
-    }
-}
-
 impl<T> PartialEq<ViewHandle<T>> for AnyViewHandle {
     fn eq(&self, other: &ViewHandle<T>) -> bool {
         self.window_id == other.window_id && self.view_id == other.view_id
@@ -4861,19 +4968,10 @@ impl AnyModelHandle {
 
     pub fn downcast<T: Entity>(self) -> Option<ModelHandle<T>> {
         if self.is::<T>() {
-            let result = Some(ModelHandle {
-                model_id: self.model_id,
+            Some(ModelHandle {
+                any_handle: self,
                 model_type: PhantomData,
-                ref_counts: self.ref_counts.clone(),
-
-                #[cfg(any(test, feature = "test-support"))]
-                handle_id: self.handle_id,
-            });
-            unsafe {
-                Arc::decrement_strong_count(Arc::as_ptr(&self.ref_counts));
-            }
-            std::mem::forget(self);
-            result
+            })
         } else {
             None
         }
@@ -4895,16 +4993,6 @@ impl AnyModelHandle {
     }
 }
 
-impl<T: Entity> From<ModelHandle<T>> for AnyModelHandle {
-    fn from(handle: ModelHandle<T>) -> Self {
-        Self::new(
-            handle.model_id,
-            TypeId::of::<T>(),
-            handle.ref_counts.clone(),
-        )
-    }
-}
-
 impl Clone for AnyModelHandle {
     fn clone(&self) -> Self {
         Self::new(self.model_id, self.model_type, self.ref_counts.clone())
@@ -4924,7 +5012,7 @@ impl Drop for AnyModelHandle {
     }
 }
 
-#[derive(Hash, PartialEq, Eq, Debug)]
+#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)]
 pub struct AnyWeakModelHandle {
     model_id: usize,
     model_type: TypeId,
@@ -4942,10 +5030,10 @@ impl AnyWeakModelHandle {
         TypeId::of::<T>() == self.model_type
     }
 
-    pub fn downcast<T: Entity>(&self) -> Option<WeakModelHandle<T>> {
+    pub fn downcast<T: Entity>(self) -> Option<WeakModelHandle<T>> {
         if self.is::<T>() {
             let result = Some(WeakModelHandle {
-                model_id: self.model_id,
+                any_handle: self,
                 model_type: PhantomData,
             });
 
@@ -4956,19 +5044,9 @@ impl AnyWeakModelHandle {
     }
 }
 
-impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
-    fn from(handle: WeakModelHandle<T>) -> Self {
-        AnyWeakModelHandle {
-            model_id: handle.model_id,
-            model_type: TypeId::of::<T>(),
-        }
-    }
-}
-
-#[derive(Debug)]
+#[derive(Debug, Copy)]
 pub struct WeakViewHandle<T> {
-    window_id: usize,
-    view_id: usize,
+    any_handle: AnyWeakViewHandle,
     view_type: PhantomData<T>,
 }
 
@@ -4981,8 +5059,11 @@ impl<T> WeakHandle for WeakViewHandle<T> {
 impl<T: View> WeakViewHandle<T> {
     fn new(window_id: usize, view_id: usize) -> Self {
         Self {
-            window_id,
-            view_id,
+            any_handle: AnyWeakViewHandle {
+                window_id,
+                view_id,
+                view_type: TypeId::of::<T>(),
+            },
             view_type: PhantomData,
         }
     }

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

@@ -16,6 +16,14 @@ pub trait Action: 'static {
         Self: Sized;
 }
 
+impl std::fmt::Debug for dyn Action {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn Action")
+            .field("namespace", &self.namespace())
+            .field("name", &self.name())
+            .finish()
+    }
+}
 /// Define a set of unit struct types that all implement the `Action` trait.
 ///
 /// The first argument is a namespace that will be associated with each of

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

@@ -11,9 +11,46 @@ pub enum MenuItem<'a> {
     Action {
         name: &'a str,
         action: Box<dyn Action>,
+        os_action: Option<OsAction>,
     },
 }
 
+impl<'a> MenuItem<'a> {
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+
+    pub fn submenu(menu: Menu<'a>) -> Self {
+        Self::Submenu(menu)
+    }
+
+    pub fn action(name: &'a str, action: impl Action) -> Self {
+        Self::Action {
+            name,
+            action: Box::new(action),
+            os_action: None,
+        }
+    }
+
+    pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
+        Self::Action {
+            name,
+            action: Box::new(action),
+            os_action: Some(os_action),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+pub enum OsAction {
+    Cut,
+    Copy,
+    Paste,
+    SelectAll,
+    Undo,
+    Redo,
+}
+
 impl MutableAppContext {
     pub fn set_menus(&mut self, menus: Vec<Menu>) {
         self.foreground_platform
@@ -40,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
         let cx = app.0.clone();
         move |action| {
             let mut cx = cx.borrow_mut();
-            if let Some(key_window_id) = cx.cx.platform.key_window_id() {
-                if let Some(view_id) = cx.focused_view_id(key_window_id) {
-                    cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action);
+            if let Some(main_window_id) = cx.cx.platform.main_window_id() {
+                if let Some(view_id) = cx.focused_view_id(main_window_id) {
+                    cx.handle_dispatch_action_from_effect(main_window_id, Some(view_id), action);
                     return;
                 }
             }

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

@@ -18,9 +18,10 @@ use smol::stream::StreamExt;
 
 use crate::{
     executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
-    AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
-    ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
-    RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
+    AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler,
+    KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
+    ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
+    WeakHandle,
 };
 use collections::BTreeMap;
 
@@ -137,11 +138,7 @@ impl TestAppContext {
         (window_id, view)
     }
 
-    pub fn add_view<T, F>(
-        &mut self,
-        parent_handle: impl Into<AnyViewHandle>,
-        build_view: F,
-    ) -> ViewHandle<T>
+    pub fn add_view<T, F>(&mut self, parent_handle: &AnyViewHandle, build_view: F) -> ViewHandle<T>
     where
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> T,
@@ -330,6 +327,14 @@ impl TestAppContext {
             .assert_dropped(handle.id())
     }
 
+    /// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
+    /// where the stray handles were created.
+    pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
+        let weak = handle.downgrade();
+        self.update(|_| drop(handle));
+        self.assert_dropped(weak);
+    }
+
     fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
         std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
             let (_, window) = state

crates/gpui/src/assets.rs 🔗

@@ -1,5 +1,8 @@
 use anyhow::{anyhow, Result};
-use std::{borrow::Cow, cell::RefCell, collections::HashMap};
+use image::ImageFormat;
+use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
+
+use crate::ImageData;
 
 pub trait AssetSource: 'static + Send + Sync {
     fn load(&self, path: &str) -> Result<Cow<[u8]>>;
@@ -22,6 +25,7 @@ impl AssetSource for () {
 pub struct AssetCache {
     source: Box<dyn AssetSource>,
     svgs: RefCell<HashMap<String, usvg::Tree>>,
+    pngs: RefCell<HashMap<String, Arc<ImageData>>>,
 }
 
 impl AssetCache {
@@ -29,6 +33,7 @@ impl AssetCache {
         Self {
             source: Box::new(source),
             svgs: RefCell::new(HashMap::new()),
+            pngs: RefCell::new(HashMap::new()),
         }
     }
 
@@ -43,4 +48,18 @@ impl AssetCache {
             Ok(svg)
         }
     }
+
+    pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
+        let mut pngs = self.pngs.borrow_mut();
+        if let Some(png) = pngs.get(path) {
+            Ok(png.clone())
+        } else {
+            let bytes = self.source.load(path)?;
+            let image = ImageData::new(
+                image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
+            );
+            pngs.insert(path.to_string(), image.clone());
+            Ok(image)
+        }
+    }
 }

crates/gpui/src/elements.rs 🔗

@@ -296,7 +296,10 @@ impl<T: Element> AnyElement for Lifecycle<T> {
                     paint,
                 }
             }
-            _ => panic!("invalid element lifecycle state"),
+            Lifecycle::Empty => panic!("invalid element lifecycle state"),
+            Lifecycle::Init { .. } => {
+                panic!("invalid element lifecycle state, paint called before layout")
+            }
         }
     }
 
@@ -363,6 +366,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
                     value
                 }
             }
+
             _ => panic!("invalid element lifecycle state"),
         }
     }
@@ -385,6 +389,12 @@ impl ElementBox {
     }
 }
 
+impl Clone for ElementBox {
+    fn clone(&self) -> Self {
+        ElementBox(self.0.clone())
+    }
+}
+
 impl From<ElementBox> for ElementRc {
     fn from(val: ElementBox) -> Self {
         val.0

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

@@ -153,7 +153,9 @@ impl Element for ConstrainedBox {
         _: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        self.child.paint(bounds.origin(), visible_bounds, cx);
+        cx.paint_layer(Some(visible_bounds), |cx| {
+            self.child.paint(bounds.origin(), visible_bounds, cx);
+        })
     }
 
     fn rect_for_text_range(

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

@@ -22,6 +22,7 @@ pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
     scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
+    child_alignment: f32,
 }
 
 impl Flex {
@@ -30,6 +31,7 @@ impl Flex {
             axis,
             children: Default::default(),
             scroll_state: None,
+            child_alignment: -1.,
         }
     }
 
@@ -41,6 +43,15 @@ impl Flex {
         Self::new(Axis::Vertical)
     }
 
+    /// Render children centered relative to the cross-axis of the parent flex.
+    ///
+    /// If this is a flex row, children will be centered vertically. If this is a
+    /// flex column, children will be centered horizontally.
+    pub fn align_children_center(mut self) -> Self {
+        self.child_alignment = 0.;
+        self
+    }
+
     pub fn scrollable<Tag, V>(
         mut self,
         element_id: usize,
@@ -308,7 +319,32 @@ impl Element for Flex {
                     }
                 }
             }
-            child.paint(child_origin, visible_bounds, cx);
+
+            // We use the child_alignment f32 to determine a point along the cross axis of the
+            // overall flex element and each child. We then align these points. So 0 would center
+            // each child relative to the overall height/width of the flex. -1 puts children at
+            // the start. 1 puts children at the end.
+            let aligned_child_origin = {
+                let cross_axis = self.axis.invert();
+                let my_center = bounds.size().along(cross_axis) / 2.;
+                let my_target = my_center + my_center * self.child_alignment;
+
+                let child_center = child.size().along(cross_axis) / 2.;
+                let child_target = child_center + child_center * self.child_alignment;
+
+                let mut aligned_child_origin = child_origin;
+                match self.axis {
+                    Axis::Horizontal => aligned_child_origin
+                        .set_y(aligned_child_origin.y() - (child_target - my_target)),
+                    Axis::Vertical => aligned_child_origin
+                        .set_x(aligned_child_origin.x() - (child_target - my_target)),
+                }
+
+                aligned_child_origin
+            };
+
+            child.paint(aligned_child_origin, visible_bounds, cx);
+
             match self.axis {
                 Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
                 Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),

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

@@ -11,8 +11,13 @@ use crate::{
 use serde::Deserialize;
 use std::{ops::Range, sync::Arc};
 
+enum ImageSource {
+    Path(&'static str),
+    Data(Arc<ImageData>),
+}
+
 pub struct Image {
-    data: Arc<ImageData>,
+    source: ImageSource,
     style: ImageStyle,
 }
 
@@ -31,9 +36,16 @@ pub struct ImageStyle {
 }
 
 impl Image {
-    pub fn new(data: Arc<ImageData>) -> Self {
+    pub fn new(asset_path: &'static str) -> Self {
+        Self {
+            source: ImageSource::Path(asset_path),
+            style: Default::default(),
+        }
+    }
+
+    pub fn from_data(data: Arc<ImageData>) -> Self {
         Self {
-            data,
+            source: ImageSource::Data(data),
             style: Default::default(),
         }
     }
@@ -45,39 +57,53 @@ impl Image {
 }
 
 impl Element for Image {
-    type LayoutState = ();
+    type LayoutState = Option<Arc<ImageData>>;
     type PaintState = ();
 
     fn layout(
         &mut self,
         constraint: SizeConstraint,
-        _: &mut LayoutContext,
+        cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
+        let data = match &self.source {
+            ImageSource::Path(path) => match cx.asset_cache.png(path) {
+                Ok(data) => data,
+                Err(error) => {
+                    log::error!("could not load image: {}", error);
+                    return (Vector2F::zero(), None);
+                }
+            },
+            ImageSource::Data(data) => data.clone(),
+        };
+
         let desired_size = vec2f(
             self.style.width.unwrap_or_else(|| constraint.max.x()),
             self.style.height.unwrap_or_else(|| constraint.max.y()),
         );
         let size = constrain_size_preserving_aspect_ratio(
             constraint.constrain(desired_size),
-            self.data.size().to_f32(),
+            data.size().to_f32(),
         );
-        (size, ())
+
+        (size, Some(data))
     }
 
     fn paint(
         &mut self,
         bounds: RectF,
         _: RectF,
-        _: &mut Self::LayoutState,
+        layout: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        cx.scene.push_image(scene::Image {
-            bounds,
-            border: self.style.border,
-            corner_radius: self.style.corner_radius,
-            grayscale: self.style.grayscale,
-            data: self.data.clone(),
-        });
+        if let Some(data) = layout {
+            cx.scene.push_image(scene::Image {
+                bounds,
+                border: self.style.border,
+                corner_radius: self.style.corner_radius,
+                grayscale: self.style.grayscale,
+                data: data.clone(),
+            });
+        }
     }
 
     fn rect_for_text_range(

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

@@ -1,4 +1,4 @@
-use std::ops::Range;
+use std::{borrow::Cow, ops::Range};
 
 use crate::{
     fonts::TextStyle,
@@ -16,7 +16,7 @@ use serde_json::json;
 use smallvec::{smallvec, SmallVec};
 
 pub struct Label {
-    text: String,
+    text: Cow<'static, str>,
     style: LabelStyle,
     highlight_indices: Vec<usize>,
 }
@@ -44,9 +44,9 @@ impl LabelStyle {
 }
 
 impl Label {
-    pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
+    pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
         Self {
-            text,
+            text: text.into(),
             highlight_indices: Default::default(),
             style: style.into(),
         }
@@ -138,11 +138,9 @@ impl Element for Label {
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
         let runs = self.compute_runs();
-        let line = cx.text_layout_cache.layout_str(
-            self.text.as_str(),
-            self.style.text.font_size,
-            runs.as_slice(),
-        );
+        let line =
+            cx.text_layout_cache
+                .layout_str(&self.text, self.style.text.font_size, runs.as_slice());
 
         let size = vec2f(
             line.width()
@@ -218,6 +216,7 @@ mod tests {
             12.,
             Default::default(),
             Default::default(),
+            Default::default(),
             Color::black(),
             cx.font_cache(),
         )
@@ -227,6 +226,7 @@ mod tests {
             12.,
             *FontProperties::new().weight(Weight::BOLD),
             Default::default(),
+            Default::default(),
             Color::new(255, 0, 0, 255),
             cx.font_cache(),
         )

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

@@ -15,7 +15,7 @@ use serde_json::json;
 use std::{borrow::Cow, ops::Range, sync::Arc};
 
 pub struct Text {
-    text: String,
+    text: Cow<'static, str>,
     style: TextStyle,
     soft_wrap: bool,
     highlights: Vec<(Range<usize>, HighlightStyle)>,
@@ -28,9 +28,9 @@ pub struct LayoutState {
 }
 
 impl Text {
-    pub fn new(text: String, style: TextStyle) -> Self {
+    pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
         Self {
-            text,
+            text: text.into(),
             style,
             soft_wrap: true,
             highlights: Vec::new(),
@@ -280,7 +280,7 @@ mod tests {
         let (window_id, _) = cx.add_window(Default::default(), |_| TestView);
         let mut presenter = cx.build_presenter(window_id, Default::default(), Default::default());
         fonts::with_font_cache(cx.font_cache().clone(), || {
-            let mut text = Text::new("Hello\r\n".into(), Default::default()).with_soft_wrap(true);
+            let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
             let (_, state) = text.layout(
                 SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
                 &mut presenter.build_layout_context(Default::default(), false, cx),

crates/gpui/src/font_cache.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    fonts::{FontId, Metrics, Properties},
+    fonts::{Features, FontId, Metrics, Properties},
     geometry::vector::{vec2f, Vector2F},
     platform,
     text_layout::LineWrapper,
@@ -18,6 +18,7 @@ pub struct FamilyId(usize);
 
 struct Family {
     name: Arc<str>,
+    font_features: Features,
     font_ids: Vec<FontId>,
 }
 
@@ -58,17 +59,21 @@ impl FontCache {
             .map(|family| family.name.clone())
     }
 
-    pub fn load_family(&self, names: &[&str]) -> Result<FamilyId> {
+    pub fn load_family(&self, names: &[&str], features: &Features) -> Result<FamilyId> {
         for name in names {
             let state = self.0.upgradable_read();
 
-            if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
+            if let Some(ix) = state
+                .families
+                .iter()
+                .position(|f| f.name.as_ref() == *name && f.font_features == *features)
+            {
                 return Ok(FamilyId(ix));
             }
 
             let mut state = RwLockUpgradableReadGuard::upgrade(state);
 
-            if let Ok(font_ids) = state.fonts.load_family(name) {
+            if let Ok(font_ids) = state.fonts.load_family(name, features) {
                 if font_ids.is_empty() {
                     continue;
                 }
@@ -82,6 +87,7 @@ impl FontCache {
 
                 state.families.push(Family {
                     name: Arc::from(*name),
+                    font_features: features.clone(),
                     font_ids,
                 });
                 return Ok(family_id);
@@ -254,7 +260,15 @@ mod tests {
     fn test_select_font() {
         let platform = test::platform();
         let fonts = FontCache::new(platform.fonts());
-        let arial = fonts.load_family(&["Arial"]).unwrap();
+        let arial = fonts
+            .load_family(
+                &["Arial"],
+                &Features {
+                    calt: Some(false),
+                    ..Default::default()
+                },
+            )
+            .unwrap();
         let arial_regular = fonts.select_font(arial, &Properties::new()).unwrap();
         let arial_italic = fonts
             .select_font(arial, Properties::new().style(Style::Italic))
@@ -265,5 +279,16 @@ mod tests {
         assert_ne!(arial_regular, arial_italic);
         assert_ne!(arial_regular, arial_bold);
         assert_ne!(arial_italic, arial_bold);
+
+        let arial_with_calt = fonts
+            .load_family(
+                &["Arial"],
+                &Features {
+                    calt: Some(true),
+                    ..Default::default()
+                },
+            )
+            .unwrap();
+        assert_ne!(arial_with_calt, arial);
     }
 }

crates/gpui/src/fonts.rs 🔗

@@ -11,7 +11,8 @@ pub use font_kit::{
     properties::{Properties, Stretch, Style, Weight},
 };
 use ordered_float::OrderedFloat;
-use serde::{de, Deserialize};
+use schemars::JsonSchema;
+use serde::{de, Deserialize, Serialize};
 use serde_json::Value;
 use std::{cell::RefCell, sync::Arc};
 
@@ -20,6 +21,44 @@ pub struct FontId(pub usize);
 
 pub type GlyphId = u32;
 
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct Features {
+    pub calt: Option<bool>,
+    pub case: Option<bool>,
+    pub cpsp: Option<bool>,
+    pub frac: Option<bool>,
+    pub liga: Option<bool>,
+    pub onum: Option<bool>,
+    pub ordn: Option<bool>,
+    pub pnum: Option<bool>,
+    pub ss01: Option<bool>,
+    pub ss02: Option<bool>,
+    pub ss03: Option<bool>,
+    pub ss04: Option<bool>,
+    pub ss05: Option<bool>,
+    pub ss06: Option<bool>,
+    pub ss07: Option<bool>,
+    pub ss08: Option<bool>,
+    pub ss09: Option<bool>,
+    pub ss10: Option<bool>,
+    pub ss11: Option<bool>,
+    pub ss12: Option<bool>,
+    pub ss13: Option<bool>,
+    pub ss14: Option<bool>,
+    pub ss15: Option<bool>,
+    pub ss16: Option<bool>,
+    pub ss17: Option<bool>,
+    pub ss18: Option<bool>,
+    pub ss19: Option<bool>,
+    pub ss20: Option<bool>,
+    pub subs: Option<bool>,
+    pub sups: Option<bool>,
+    pub swsh: Option<bool>,
+    pub titl: Option<bool>,
+    pub tnum: Option<bool>,
+    pub zero: Option<bool>,
+}
+
 #[derive(Clone, Debug)]
 pub struct TextStyle {
     pub color: Color,
@@ -71,6 +110,8 @@ thread_local! {
 struct TextStyleJson {
     color: Color,
     family: String,
+    #[serde(default)]
+    features: Features,
     weight: Option<WeightJson>,
     size: f32,
     #[serde(default)]
@@ -107,12 +148,13 @@ impl TextStyle {
         font_family_name: impl Into<Arc<str>>,
         font_size: f32,
         font_properties: Properties,
+        font_features: Features,
         underline: Underline,
         color: Color,
         font_cache: &FontCache,
     ) -> Result<Self> {
         let font_family_name = font_family_name.into();
-        let font_family_id = font_cache.load_family(&[&font_family_name])?;
+        let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
         let font_id = font_cache.select_font(font_family_id, &font_properties)?;
         Ok(Self {
             color,
@@ -175,6 +217,7 @@ impl TextStyle {
                     json.family,
                     json.size,
                     font_properties,
+                    json.features,
                     underline_from_json(json.underline),
                     json.color,
                     font_cache,
@@ -253,7 +296,9 @@ impl Default for TextStyle {
                 .expect("TextStyle::default can only be called within a call to with_font_cache");
 
             let font_family_name = Arc::from("Courier");
-            let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
+            let font_family_id = font_cache
+                .load_family(&[&font_family_name], &Default::default())
+                .unwrap();
             let font_id = font_cache
                 .select_font(font_family_id, &Default::default())
                 .unwrap();

crates/gpui/src/keymap_matcher.rs 🔗

@@ -5,7 +5,7 @@ mod keystroke;
 
 use std::{any::TypeId, fmt::Debug};
 
-use collections::{BTreeMap, HashMap};
+use collections::HashMap;
 use smallvec::SmallVec;
 
 use crate::Action;
@@ -68,8 +68,8 @@ impl KeymapMatcher {
     ///         There exist bindings which are still waiting for more keys.
     ///     MatchResult::Complete(matches) =>
     ///         1 or more bindings have recieved the necessary key presses.
-    ///         The order of the matched actions is by order in the keymap file first and
-    ///         position of the matching view second.
+    ///         The order of the matched actions is by position of the matching first,
+    //          and order in the keymap second.
     pub fn push_keystroke(
         &mut self,
         keystroke: Keystroke,
@@ -80,8 +80,7 @@ impl KeymapMatcher {
         // and then the order the binding matched in the view tree second.
         // The key is the reverse position of the binding in the bindings list so that later bindings
         // match before earlier ones in the user's config
-        let mut matched_bindings: BTreeMap<usize, Vec<(usize, Box<dyn Action>)>> =
-            Default::default();
+        let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
 
         let first_keystroke = self.pending_keystrokes.is_empty();
         self.pending_keystrokes.push(keystroke.clone());
@@ -105,14 +104,11 @@ impl KeymapMatcher {
                 }
             }
 
-            for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
+            for binding in self.keymap.bindings().iter().rev() {
                 match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
                 {
                     BindingMatchResult::Complete(action) => {
-                        matched_bindings
-                            .entry(order)
-                            .or_default()
-                            .push((*view_id, action));
+                        matched_bindings.push((*view_id, action));
                     }
                     BindingMatchResult::Partial => {
                         self.pending_views
@@ -131,7 +127,7 @@ impl KeymapMatcher {
         if !matched_bindings.is_empty() {
             // Collect the sorted matched bindings into the final vec for ease of use
             // Matched bindings are in order by precedence
-            MatchResult::Matches(matched_bindings.into_values().flatten().collect())
+            MatchResult::Matches(matched_bindings)
         } else if any_pending {
             MatchResult::Pending
         } else {
@@ -225,15 +221,47 @@ mod tests {
 
     use super::*;
 
+    #[test]
+    fn test_keymap_and_view_ordering() -> Result<()> {
+        actions!(test, [EditorAction, ProjectPanelAction]);
+
+        let mut editor = KeymapContext::default();
+        editor.add_identifier("Editor");
+
+        let mut project_panel = KeymapContext::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.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
+            MatchResult::Matches(vec![
+                (2, Box::new(EditorAction)),
+                (1, Box::new(ProjectPanelAction)),
+            ]),
+        );
+
+        Ok(())
+    }
+
     #[test]
     fn test_push_keystroke() -> Result<()> {
-        actions!(test, [B, AB, C, D, DA]);
+        actions!(test, [B, AB, C, D, DA, E, EF]);
 
         let mut context1 = KeymapContext::default();
-        context1.set.insert("1".into());
+        context1.add_identifier("1");
 
         let mut context2 = KeymapContext::default();
-        context2.set.insert("2".into());
+        context2.add_identifier("2");
 
         let dispatch_path = vec![(2, context2), (1, context1)];
 
@@ -286,6 +314,7 @@ mod tests {
             matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
             MatchResult::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!(
@@ -366,22 +395,22 @@ mod tests {
         let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
+        context.add_identifier("a");
         assert!(!predicate.eval(&[context]));
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
-        context.set.insert("b".into());
+        context.add_identifier("a");
+        context.add_identifier("b");
         assert!(predicate.eval(&[context]));
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
-        context.map.insert("c".into(), "x".into());
+        context.add_identifier("a");
+        context.add_key("c", "x");
         assert!(!predicate.eval(&[context]));
 
         let mut context = KeymapContext::default();
-        context.set.insert("a".into());
-        context.map.insert("c".into(), "d".into());
+        context.add_identifier("a");
+        context.add_key("c", "d");
         assert!(predicate.eval(&[context]));
 
         let predicate = KeymapContextPredicate::parse("!a").unwrap();
@@ -421,10 +450,11 @@ mod tests {
         assert!(!predicate.eval(&contexts[6..]));
 
         fn context_set(names: &[&str]) -> KeymapContext {
-            KeymapContext {
-                set: names.iter().copied().map(str::to_string).collect(),
-                ..Default::default()
-            }
+            let mut keymap = KeymapContext::new();
+            names
+                .iter()
+                .for_each(|name| keymap.add_identifier(name.to_string()));
+            keymap
         }
     }
 
@@ -447,10 +477,10 @@ mod tests {
         ]);
 
         let mut context_a = KeymapContext::default();
-        context_a.set.insert("a".into());
+        context_a.add_identifier("a");
 
         let mut context_b = KeymapContext::default();
-        context_b.set.insert("b".into());
+        context_b.add_identifier("b");
 
         let mut matcher = KeymapMatcher::new(keymap);
 
@@ -495,7 +525,7 @@ mod tests {
         matcher.clear_pending();
 
         let mut context_c = KeymapContext::default();
-        context_c.set.insert("c".into());
+        context_c.add_identifier("c");
 
         // Pending keystrokes are maintained per-view
         assert_eq!(

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

@@ -1,13 +1,22 @@
+use std::borrow::Cow;
+
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet};
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct KeymapContext {
-    pub set: HashSet<String>,
-    pub map: HashMap<String, String>,
+    set: HashSet<Cow<'static, str>>,
+    map: HashMap<Cow<'static, str>, Cow<'static, str>>,
 }
 
 impl KeymapContext {
+    pub fn new() -> Self {
+        KeymapContext {
+            set: HashSet::default(),
+            map: HashMap::default(),
+        }
+    }
+
     pub fn extend(&mut self, other: &Self) {
         for v in &other.set {
             self.set.insert(v.clone());
@@ -16,6 +25,18 @@ impl KeymapContext {
             self.map.insert(k.clone(), v.clone());
         }
     }
+
+    pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
+        self.set.insert(identifier.into());
+    }
+
+    pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
+        &mut self,
+        key: S1,
+        value: S2,
+    ) {
+        self.map.insert(key.into(), value.into());
+    }
 }
 
 #[derive(Debug, Eq, PartialEq)]
@@ -46,12 +67,12 @@ impl KeymapContextPredicate {
             Self::Identifier(name) => (&context.set).contains(name.as_str()),
             Self::Equal(left, right) => context
                 .map
-                .get(left)
+                .get(left.as_str())
                 .map(|value| value == right)
                 .unwrap_or(false),
             Self::NotEqual(left, right) => context
                 .map
-                .get(left)
+                .get(left.as_str())
                 .map(|value| value != right)
                 .unwrap_or(true),
             Self::Not(pred) => !pred.eval(contexts),

crates/gpui/src/platform.rs 🔗

@@ -9,7 +9,10 @@ pub mod current {
 
 use crate::{
     executor,
-    fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
+    fonts::{
+        Features as FontFeatures, FontId, GlyphId, Metrics as FontMetrics,
+        Properties as FontProperties,
+    },
     geometry::{
         rect::{RectF, RectI},
         vector::Vector2F,
@@ -58,7 +61,7 @@ pub trait Platform: Send + Sync {
         options: WindowOptions,
         executor: Rc<executor::Foreground>,
     ) -> Box<dyn Window>;
-    fn key_window_id(&self) -> Option<usize>;
+    fn main_window_id(&self) -> Option<usize>;
 
     fn add_status_item(&self) -> Box<dyn Window>;
 
@@ -87,6 +90,10 @@ pub(crate) trait ForegroundPlatform {
     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()>);
+
+    /// Handle the application being re-activated with no windows open.
+    fn on_reopen(&self, callback: Box<dyn FnMut()>);
+
     fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
     fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
     fn run(&self, on_finish_launching: Box<dyn FnOnce()>);
@@ -335,7 +342,7 @@ pub enum RasterizationOptions {
 
 pub trait FontSystem: Send + Sync {
     fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
-    fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
+    fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
     fn select_font(
         &self,
         font_ids: &[FontId],

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

@@ -1,5 +1,7 @@
+mod open_type;
+
 use crate::{
-    fonts::{FontId, GlyphId, Metrics, Properties},
+    fonts::{Features, FontId, GlyphId, Metrics, Properties},
     geometry::{
         rect::{RectF, RectI},
         transform2d::Transform2F,
@@ -64,8 +66,8 @@ impl platform::FontSystem for FontSystem {
         self.0.write().add_fonts(fonts)
     }
 
-    fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
-        self.0.write().load_family(name)
+    fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
+        self.0.write().load_family(name, features)
     }
 
     fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
@@ -126,7 +128,7 @@ impl FontSystemState {
         Ok(())
     }
 
-    fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
+    fn load_family(&mut self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
         let mut font_ids = Vec::new();
 
         let family = self
@@ -134,7 +136,8 @@ impl FontSystemState {
             .select_family_by_name(name)
             .or_else(|_| self.system_source.select_family_by_name(name))?;
         for font in family.fonts() {
-            let font = font.load()?;
+            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();
@@ -503,7 +506,7 @@ mod tests {
     fn test_layout_str(_: &mut MutableAppContext) {
         // 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").unwrap();
+        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(),
@@ -544,13 +547,13 @@ mod tests {
     #[test]
     fn test_glyph_offsets() -> anyhow::Result<()> {
         let fonts = FontSystem::new();
-        let zapfino = fonts.load_family("Zapfino")?;
+        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")?;
+        let menlo = fonts.load_family("Menlo", &Default::default())?;
         let menlo_regular = RunStyle {
             font_id: fonts.select_font(&menlo, &Properties::new())?,
             color: Default::default(),
@@ -584,7 +587,7 @@ mod tests {
         use std::{fs::File, io::BufWriter, path::Path};
 
         let fonts = FontSystem::new();
-        let font_ids = fonts.load_family("Fira Code").unwrap();
+        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();
 
@@ -618,7 +621,7 @@ mod tests {
     #[test]
     fn test_wrap_line() {
         let fonts = FontSystem::new();
-        let font_ids = fonts.load_family("Helvetica").unwrap();
+        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";
@@ -636,7 +639,7 @@ mod tests {
     #[test]
     fn test_layout_line_bom_char() {
         let fonts = FontSystem::new();
-        let font_ids = fonts.load_family("Helvetica").unwrap();
+        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(),

crates/gpui/src/platform/mac/fonts/open_type.rs 🔗

@@ -0,0 +1,395 @@
+#![allow(unused, non_upper_case_globals)]
+
+use std::ptr;
+
+use crate::fonts::Features;
+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;
+
+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: &Features) {
+    // 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/gpui/src/platform/mac/geometry.rs 🔗

@@ -1,7 +1,6 @@
 use cocoa::{
-    appkit::NSWindow,
     base::id,
-    foundation::{NSPoint, NSRect, NSSize},
+    foundation::{NSPoint, NSRect},
 };
 use objc::{msg_send, sel, sel_impl};
 use pathfinder_geometry::{
@@ -25,61 +24,15 @@ impl Vector2FExt for Vector2F {
     }
 }
 
-pub trait RectFExt {
-    /// Converts self to an NSRect with y axis pointing up.
-    /// The resulting NSRect will have an origin at the bottom left of the rectangle.
-    /// Also takes care of converting from window scaled coordinates to screen coordinates
-    fn to_screen_ns_rect(&self, native_window: id) -> NSRect;
-
-    /// Converts self to an NSRect with y axis point up.
-    /// The resulting NSRect will have an origin at the bottom left of the rectangle.
-    /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
-    fn to_ns_rect(&self) -> NSRect;
-}
-impl RectFExt for RectF {
-    fn to_screen_ns_rect(&self, native_window: id) -> NSRect {
-        unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) }
-    }
-
-    fn to_ns_rect(&self) -> NSRect {
-        NSRect::new(
-            NSPoint::new(
-                self.origin_x() as f64,
-                -(self.origin_y() + self.height()) as f64,
-            ),
-            NSSize::new(self.width() as f64, self.height() as f64),
-        )
-    }
-}
-
 pub trait NSRectExt {
-    /// Converts self to a RectF with y axis pointing down.
-    /// The resulting RectF will have an origin at the top left of the rectangle.
-    /// Also takes care of converting from screen scale coordinates to window coordinates
-    fn to_window_rectf(&self, native_window: id) -> RectF;
-
-    /// Converts self to a RectF with y axis pointing down.
-    /// The resulting RectF will have an origin at the top left of the rectangle.
-    /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
     fn to_rectf(&self) -> RectF;
-
     fn intersects(&self, other: Self) -> bool;
 }
-impl NSRectExt for NSRect {
-    fn to_window_rectf(&self, native_window: id) -> RectF {
-        unsafe {
-            self.origin.x;
-            let rect: NSRect = native_window.convertRectFromScreen_(*self);
-            rect.to_rectf()
-        }
-    }
 
+impl NSRectExt for NSRect {
     fn to_rectf(&self) -> RectF {
         RectF::new(
-            vec2f(
-                self.origin.x as f32,
-                -(self.origin.y + self.size.height) as f32,
-            ),
+            vec2f(self.origin.x as f32, self.origin.y as f32),
             vec2f(self.size.width as f32, self.size.height as f32),
         )
     }

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

@@ -82,6 +82,10 @@ unsafe fn build_classes() {
             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),
@@ -98,6 +102,31 @@ unsafe fn build_classes() {
             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,
@@ -119,6 +148,7 @@ pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
 pub struct MacForegroundPlatformState {
     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(crate::Event) -> bool>>,
     menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
@@ -133,15 +163,16 @@ pub struct MacForegroundPlatformState {
 impl MacForegroundPlatform {
     pub fn new(foreground: Rc<executor::Foreground>) -> Self {
         Self(RefCell::new(MacForegroundPlatformState {
-            become_active: Default::default(),
-            resign_active: Default::default(),
-            quit: Default::default(),
-            event: Default::default(),
-            menu_command: Default::default(),
-            validate_menu_command: Default::default(),
-            will_open_menu: Default::default(),
-            open_urls: Default::default(),
-            finish_launching: Default::default(),
+            become_active: None,
+            resign_active: None,
+            reopen: None,
+            quit: None,
+            event: None,
+            menu_command: None,
+            validate_menu_command: None,
+            will_open_menu: None,
+            open_urls: None,
+            finish_launching: None,
             menu_actions: Default::default(),
             foreground,
         }))
@@ -193,11 +224,25 @@ impl MacForegroundPlatform {
     ) -> id {
         match item {
             MenuItem::Separator => NSMenuItem::separatorItem(nil),
-            MenuItem::Action { name, action } => {
+            MenuItem::Action {
+                name,
+                action,
+                os_action,
+            } => {
+                // TODO
                 let keystrokes = keystroke_matcher
                     .bindings_for_action_type(action.as_any().type_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 {
@@ -218,7 +263,7 @@ impl MacForegroundPlatform {
                         item = NSMenuItem::alloc(nil)
                             .initWithTitle_action_keyEquivalent_(
                                 ns_string(name),
-                                selector("handleGPUIMenuItem:"),
+                                selector,
                                 ns_string(key_to_native(&keystroke.key).as_ref()),
                             )
                             .autorelease();
@@ -240,7 +285,7 @@ impl MacForegroundPlatform {
                         item = NSMenuItem::alloc(nil)
                             .initWithTitle_action_keyEquivalent_(
                                 ns_string(&name),
-                                selector("handleGPUIMenuItem:"),
+                                selector,
                                 ns_string(""),
                             )
                             .autorelease();
@@ -249,7 +294,7 @@ impl MacForegroundPlatform {
                     item = NSMenuItem::alloc(nil)
                         .initWithTitle_action_keyEquivalent_(
                             ns_string(name),
-                            selector("handleGPUIMenuItem:"),
+                            selector,
                             ns_string(""),
                         )
                         .autorelease();
@@ -293,6 +338,10 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().quit = Some(callback);
     }
 
+    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().reopen = Some(callback);
+    }
+
     fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
         self.0.borrow_mut().event = Some(callback);
     }
@@ -548,8 +597,8 @@ impl platform::Platform for MacPlatform {
         Box::new(Window::open(id, options, executor, self.fonts()))
     }
 
-    fn key_window_id(&self) -> Option<usize> {
-        Window::key_window_id()
+    fn main_window_id(&self) -> Option<usize> {
+        Window::main_window_id()
     }
 
     fn add_status_item(&self) -> Box<dyn platform::Window> {
@@ -828,17 +877,37 @@ impl platform::Platform for MacPlatform {
     }
 
     fn restart(&self) {
-        #[cfg(debug_assertions)]
-        let path = std::env::current_exe();
-
-        #[cfg(not(debug_assertions))]
-        let path = self.app_path().or_else(|_| std::env::current_exe());
-
-        let command = path.and_then(|path| Command::new("/usr/bin/open").arg(path).spawn());
-
-        match command {
-            Err(err) => log::error!("Unable to restart application {}", err),
-            Ok(_child) => self.quit(),
+        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),
         }
     }
 }
@@ -884,6 +953,15 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
     }
 }
 
+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.borrow_mut().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.borrow_mut().become_active.as_mut() {

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

@@ -85,16 +85,12 @@ impl SpriteCache {
     ) -> Option<GlyphSprite> {
         const SUBPIXEL_VARIANTS: u8 = 4;
 
-        let scale_factor = self.scale_factor;
-        let target_position = target_position * scale_factor;
-        let fonts = &self.fonts;
-        let atlases = &mut self.atlases;
+        let target_position = target_position * self.scale_factor;
         let subpixel_variant = (
-            (target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
-                % SUBPIXEL_VARIANTS,
-            (target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
-                % SUBPIXEL_VARIANTS,
+            (target_position.x().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
+            (target_position.y().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
         );
+
         self.glyphs
             .entry(GlyphDescriptor {
                 font_id,
@@ -107,16 +103,17 @@ impl SpriteCache {
                     subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
                     subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
                 );
-                let (glyph_bounds, mask) = fonts.rasterize_glyph(
+                let (glyph_bounds, mask) = self.fonts.rasterize_glyph(
                     font_id,
                     font_size,
                     glyph_id,
                     subpixel_shift,
-                    scale_factor,
+                    self.scale_factor,
                     RasterizationOptions::Alpha,
                 )?;
 
-                let (alloc_id, atlas_bounds) = atlases
+                let (alloc_id, atlas_bounds) = self
+                    .atlases
                     .upload(glyph_bounds.size(), &mask)
                     .expect("could not upload glyph");
                 Some(GlyphSprite {

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

@@ -8,7 +8,7 @@ use crate::{
     mac::platform::NSViewLayerContentsRedrawDuringViewResize,
     platform::{
         self,
-        mac::{geometry::RectFExt, renderer::Renderer, screen::Screen},
+        mac::{renderer::Renderer, screen::Screen},
         Event, WindowBounds,
     },
     InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
@@ -372,7 +372,8 @@ impl WindowState {
             }
 
             let window_frame = self.frame();
-            if window_frame == self.native_window.screen().visibleFrame().to_rectf() {
+            let screen_frame = self.native_window.screen().visibleFrame().to_rectf();
+            if window_frame.size() == screen_frame.size() {
                 WindowBounds::Maximized
             } else {
                 WindowBounds::Fixed(window_frame)
@@ -383,8 +384,19 @@ impl WindowState {
     // Returns the window bounds in window coordinates
     fn frame(&self) -> RectF {
         unsafe {
-            let ns_frame = NSWindow::frame(self.native_window);
-            ns_frame.to_rectf()
+            let screen_frame = self.native_window.screen().visibleFrame();
+            let window_frame = NSWindow::frame(self.native_window);
+            RectF::new(
+                vec2f(
+                    window_frame.origin.x as f32,
+                    (screen_frame.size.height - window_frame.origin.y - window_frame.size.height)
+                        as f32,
+                ),
+                vec2f(
+                    window_frame.size.width as f32,
+                    window_frame.size.height as f32,
+                ),
+            )
         }
     }
 
@@ -472,7 +484,16 @@ impl Window {
                 }
                 WindowBounds::Fixed(rect) => {
                     let screen_frame = screen.visibleFrame();
-                    let ns_rect = rect.to_ns_rect();
+                    let ns_rect = NSRect::new(
+                        NSPoint::new(
+                            rect.origin_x() as f64,
+                            screen_frame.size.height
+                                - rect.origin_y() as f64
+                                - rect.height() as f64,
+                        ),
+                        NSSize::new(rect.width() as f64, rect.height() as f64),
+                    );
+
                     if ns_rect.intersects(screen_frame) {
                         native_window.setFrame_display_(ns_rect, YES);
                     } else {
@@ -604,12 +625,12 @@ impl Window {
         }
     }
 
-    pub fn key_window_id() -> Option<usize> {
+    pub fn main_window_id() -> Option<usize> {
         unsafe {
             let app = NSApplication::sharedApplication(nil);
-            let key_window: id = msg_send![app, keyWindow];
-            if msg_send![key_window, isKindOfClass: WINDOW_CLASS] {
-                let id = get_window_state(&*key_window).borrow().id;
+            let main_window: id = msg_send![app, mainWindow];
+            if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
+                let id = get_window_state(&*main_window).borrow().id;
                 Some(id)
             } else {
                 None
@@ -737,6 +758,7 @@ impl platform::Window for Window {
             let title = ns_string(title);
             let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
             let _: () = msg_send![window, setTitle: title];
+            self.0.borrow().move_traffic_light();
         }
     }
 

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

@@ -61,13 +61,10 @@ impl ForegroundPlatform {
 
 impl super::ForegroundPlatform for ForegroundPlatform {
     fn on_become_active(&self, _: Box<dyn FnMut()>) {}
-
     fn on_resign_active(&self, _: Box<dyn FnMut()>) {}
-
     fn on_quit(&self, _: Box<dyn FnMut()>) {}
-
+    fn on_reopen(&self, _: Box<dyn FnMut()>) {}
     fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
-
     fn on_open_urls(&self, _: Box<dyn FnMut(Vec<String>)>) {}
 
     fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
@@ -157,7 +154,7 @@ impl super::Platform for Platform {
         }))
     }
 
-    fn key_window_id(&self) -> Option<usize> {
+    fn main_window_id(&self) -> Option<usize> {
         None
     }
 

crates/gpui/src/presenter.rs 🔗

@@ -12,9 +12,9 @@ use crate::{
     text_layout::TextLayoutCache,
     Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance,
     AssetCache, ElementBox, Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent,
-    MouseRegion, MouseRegionId, ParentId, ReadModel, ReadView, RenderContext, RenderParams,
-    SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
-    WeakViewHandle,
+    MouseRegion, MouseRegionId, MouseState, ParentId, ReadModel, ReadView, RenderContext,
+    RenderParams, SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
+    WeakModelHandle, WeakViewHandle,
 };
 use anyhow::bail;
 use collections::{HashMap, HashSet};
@@ -507,15 +507,18 @@ impl Presenter {
                 }
                 // Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
                 // not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
-                // This behavior can be overridden by adding a Down handler that calls cx.propogate_event
+                // This behavior can be overridden by adding a Down handler
                 if let MouseEvent::Down(e) = &mouse_event {
-                    if valid_region
+                    let has_click = valid_region
                         .handlers
-                        .contains(MouseEvent::click_disc(), Some(e.button))
-                        || valid_region
-                            .handlers
-                            .contains(MouseEvent::drag_disc(), Some(e.button))
-                    {
+                        .contains(MouseEvent::click_disc(), Some(e.button));
+                    let has_drag = valid_region
+                        .handlers
+                        .contains(MouseEvent::drag_disc(), Some(e.button));
+                    let has_down = valid_region
+                        .handlers
+                        .contains(MouseEvent::down_disc(), Some(e.button));
+                    if !has_down && (has_click || has_drag) {
                         event_cx.handled = true;
                     }
                 }
@@ -523,14 +526,13 @@ impl Presenter {
                 // `event_consumed` should only be true if there are any handlers for this event.
                 let mut event_consumed = event_cx.handled;
                 if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) {
-                    event_consumed = true;
                     for callback in callbacks {
                         event_cx.handled = true;
                         event_cx.with_current_view(valid_region.id().view_id(), {
                             let region_event = mouse_event.clone();
                             |cx| callback(region_event, cx)
                         });
-                        event_consumed &= event_cx.handled;
+                        event_consumed |= event_cx.handled;
                         any_event_handled |= event_cx.handled;
                     }
                 }
@@ -603,6 +605,24 @@ pub struct LayoutContext<'a> {
 }
 
 impl<'a> LayoutContext<'a> {
+    pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
+        let view_id = self.view_stack.last().unwrap();
+
+        let region_id = MouseRegionId::new::<Tag>(*view_id, region_id);
+        MouseState {
+            hovered: self.hovered_region_ids.contains(&region_id),
+            clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
+                if ids.contains(&region_id) {
+                    Some(*button)
+                } else {
+                    None
+                }
+            }),
+            accessed_hovered: false,
+            accessed_clicked: false,
+        }
+    }
+
     fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
         let print_error = |view_id| {
             format!(
@@ -618,7 +638,7 @@ impl<'a> LayoutContext<'a> {
             (Some(layout_parent), Some(ParentId::View(app_parent))) => {
                 if layout_parent != app_parent {
                     panic!(
-                        "View {} was laid out with parent {} when it was constructed with parent {}", 
+                        "View {} was laid out with parent {} when it was constructed with parent {}",
                         print_error(view_id),
                         print_error(*layout_parent),
                         print_error(*app_parent))
@@ -1039,8 +1059,7 @@ pub struct ChildView {
 }
 
 impl ChildView {
-    pub fn new(view: impl Into<AnyViewHandle>, cx: &AppContext) -> Self {
-        let view = view.into();
+    pub fn new(view: &AnyViewHandle, cx: &AppContext) -> Self {
         let view_name = cx.view_ui_name(view.window_id(), view.id()).unwrap();
         Self {
             view: view.downgrade(),

crates/gpui/src/text_layout.rs 🔗

@@ -663,7 +663,9 @@ mod tests {
     fn test_wrap_line(cx: &mut crate::MutableAppContext) {
         let font_cache = cx.font_cache().clone();
         let font_system = cx.platform().fonts();
-        let family = font_cache.load_family(&["Courier"]).unwrap();
+        let family = font_cache
+            .load_family(&["Courier"], &Default::default())
+            .unwrap();
         let font_id = font_cache.select_font(family, &Default::default()).unwrap();
 
         let mut wrapper = LineWrapper::new(font_id, 16., font_system);
@@ -725,7 +727,9 @@ mod tests {
         let font_system = cx.platform().fonts();
         let text_layout_cache = TextLayoutCache::new(font_system.clone());
 
-        let family = font_cache.load_family(&["Helvetica"]).unwrap();
+        let family = font_cache
+            .load_family(&["Helvetica"], &Default::default())
+            .unwrap();
         let font_id = font_cache.select_font(family, &Default::default()).unwrap();
         let normal = RunStyle {
             font_id,

crates/install_cli/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "install_cli"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/install_cli.rs"
+
+[features]
+test-support = []
+
+[dependencies]
+smol = "1.2.5"
+anyhow = "1.0.38"
+log = "0.4"
+gpui = { path = "../gpui" }
+util = { path = "../util" }

crates/install_cli/src/install_cli.rs 🔗

@@ -0,0 +1,55 @@
+use std::path::Path;
+
+use anyhow::{anyhow, Result};
+use gpui::{actions, AsyncAppContext};
+use util::ResultExt;
+
+actions!(cli, [Install]);
+
+pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
+    let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
+    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("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/journal/Cargo.toml 🔗

@@ -13,6 +13,7 @@ editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
+anyhow = "1.0"
 chrono = "0.4"
 dirs = "4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/journal/src/journal.rs 🔗

@@ -48,7 +48,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         async move {
             let (journal_dir, entry_path) = create_entry.await?;
             let (workspace, _) = cx
-                .update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
+                .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))
                 .await;
 
             let opened = workspace
@@ -73,7 +73,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                 }
             }
 
-            Ok(())
+            anyhow::Ok(())
         }
         .log_err()
     })

crates/language/Cargo.toml 🔗

@@ -43,11 +43,12 @@ futures = "0.3"
 lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 regex = "1.5"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1", features = ["preserve_order"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 similar = "1.3"
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"

crates/language/src/buffer.rs 🔗

@@ -305,7 +305,7 @@ pub struct Chunk<'a> {
 }
 
 pub struct Diff {
-    base_version: clock::Global,
+    pub(crate) base_version: clock::Global,
     line_ending: LineEnding,
     edits: Vec<(Range<usize>, Arc<str>)>,
 }
@@ -569,18 +569,21 @@ impl Buffer {
                     .read_with(&cx, |this, cx| this.diff(new_text, cx))
                     .await;
                 this.update(&mut cx, |this, cx| {
-                    if let Some(transaction) = this.apply_diff(diff, cx).cloned() {
-                        this.did_reload(
-                            this.version(),
-                            this.as_rope().fingerprint(),
-                            this.line_ending(),
-                            new_mtime,
-                            cx,
-                        );
-                        Ok(Some(transaction))
-                    } else {
-                        Ok(None)
+                    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 Ok(Some(transaction));
+                        }
                     }
+                    Ok(None)
                 })
             } else {
                 Ok(None)
@@ -1154,20 +1157,84 @@ impl Buffer {
         })
     }
 
-    pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
-        if self.version == diff.base_version {
-            self.finalize_last_transaction();
-            self.start_transaction();
-            self.text.set_line_ending(diff.line_ending);
-            self.edit(diff.edits, None, cx);
-            if self.end_transaction(cx).is_some() {
-                self.finalize_last_transaction()
-            } else {
-                None
+    /// 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.background().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;
             }
-        } else {
-            None
         }
+        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 {
@@ -1286,13 +1353,20 @@ impl Buffer {
     }
 
     pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
-        self.set_active_selections(Arc::from([]), false, Default::default(), cx);
+        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::Local>
     where
         T: Into<Arc<str>>,
     {
+        self.autoindent_requests.clear();
         self.edit([(0..self.len(), text)], None, cx)
     }
 
@@ -2840,3 +2914,42 @@ pub fn char_kind(c: char) -> CharKind {
         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/language/src/buffer_tests.rs 🔗

@@ -6,6 +6,7 @@ use gpui::{ModelHandle, MutableAppContext};
 use indoc::indoc;
 use proto::deserialize_operation;
 use rand::prelude::*;
+use regex::RegexBuilder;
 use settings::Settings;
 use std::{
     cell::RefCell,
@@ -18,6 +19,13 @@ use text::network::Network;
 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() {
@@ -72,31 +80,49 @@ fn test_select_language() {
 
     // matching file extension
     assert_eq!(
-        registry.language_for_path("zed/lib.rs").map(|l| l.name()),
+        registry
+            .language_for_path("zed/lib.rs")
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
         Some("Rust".into())
     );
     assert_eq!(
-        registry.language_for_path("zed/lib.mk").map(|l| l.name()),
+        registry
+            .language_for_path("zed/lib.mk")
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
         Some("Make".into())
     );
 
     // matching filename
     assert_eq!(
-        registry.language_for_path("zed/Makefile").map(|l| l.name()),
+        registry
+            .language_for_path("zed/Makefile")
+            .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_path("zed/cars").map(|l| l.name()),
+        registry
+            .language_for_path("zed/cars")
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
         None
     );
     assert_eq!(
-        registry.language_for_path("zed/a.cars").map(|l| l.name()),
+        registry
+            .language_for_path("zed/a.cars")
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
         None
     );
     assert_eq!(
-        registry.language_for_path("zed/sumk").map(|l| l.name()),
+        registry
+            .language_for_path("zed/sumk")
+            .now_or_never()
+            .and_then(|l| Some(l.ok()?.name())),
         None
     );
 }
@@ -211,6 +237,79 @@ async fn test_apply_diff(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
+    let text = [
+        "zero",     //
+        "one  ",    // 2 trailing spaces
+        "two",      //
+        "three   ", // 3 trailing spaces
+        "four",     //
+        "five    ", // 4 trailing spaces
+    ]
+    .join("\n");
+
+    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
+
+    // Spawn a task to format the buffer's whitespace.
+    // Pause so that the foratting task starts running.
+    let format = buffer.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx));
+    smol::future::yield_now().await;
+
+    // Edit the buffer while the normalization task is running.
+    let version_before_edit = buffer.read_with(cx, |buffer, _| buffer.version());
+    buffer.update(cx, |buffer, cx| {
+        buffer.edit(
+            [
+                (Point::new(0, 1)..Point::new(0, 1), "EE"),
+                (Point::new(3, 5)..Point::new(3, 5), "EEE"),
+            ],
+            None,
+            cx,
+        );
+    });
+
+    let format_diff = format.await;
+    buffer.update(cx, |buffer, cx| {
+        let version_before_format = format_diff.base_version.clone();
+        buffer.apply_diff(format_diff, cx);
+
+        // The outcome depends on the order of concurrent taks.
+        //
+        // If the edit occurred while searching for trailing whitespace ranges,
+        // then the trailing whitespace region touched by the edit is left intact.
+        if version_before_format == version_before_edit {
+            assert_eq!(
+                buffer.text(),
+                [
+                    "zEEero",      //
+                    "one",         //
+                    "two",         //
+                    "threeEEE   ", //
+                    "four",        //
+                    "five",        //
+                ]
+                .join("\n")
+            );
+        }
+        // Otherwise, all trailing whitespace is removed.
+        else {
+            assert_eq!(
+                buffer.text(),
+                [
+                    "zEEero",   //
+                    "one",      //
+                    "two",      //
+                    "threeEEE", //
+                    "four",     //
+                    "five",     //
+                ]
+                .join("\n")
+            );
+        }
+    });
+}
+
 #[gpui::test]
 async fn test_reparse(cx: &mut gpui::TestAppContext) {
     let text = "fn a() {}";
@@ -585,14 +684,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         indoc! {"
             mod x {
                 moˇd y {
-                
+
                 }
             }
             let foo = 1;"},
         vec![indoc! {"
             mod x «{»
                 mod y {
-                
+
                 }
             «}»
             let foo = 1;"}],
@@ -602,7 +701,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         indoc! {"
             mod x {
                 mod y ˇ{
-                
+
                 }
             }
             let foo = 1;"},
@@ -610,14 +709,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
             indoc! {"
                 mod x «{»
                     mod y {
-                    
+
                     }
                 «}»
                 let foo = 1;"},
             indoc! {"
                 mod x {
                     mod y «{»
-                    
+
                     «}»
                 }
                 let foo = 1;"},
@@ -628,7 +727,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         indoc! {"
             mod x {
                 mod y {
-                
+
                 }ˇ
             }
             let foo = 1;"},
@@ -636,14 +735,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
             indoc! {"
                 mod x «{»
                     mod y {
-                    
+
                     }
                 «}»
                 let foo = 1;"},
             indoc! {"
                 mod x {
                     mod y «{»
-                    
+
                     «}»
                 }
                 let foo = 1;"},
@@ -654,14 +753,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         indoc! {"
             mod x {
                 mod y {
-                
+
                 }
             ˇ}
             let foo = 1;"},
         vec![indoc! {"
             mod x «{»
                 mod y {
-                
+
                 }
             «}»
             let foo = 1;"}],
@@ -671,7 +770,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         indoc! {"
             mod x {
                 mod y {
-                
+
                 }
             }
             let fˇoo = 1;"},
@@ -683,7 +782,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
         indoc! {"
             mod x {
                 mod y {
-                
+
                 }
             }
             let foo = 1;ˇ"},
@@ -710,7 +809,6 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
         }"}],
     );
 
-    eprintln!("-----------------------");
     // 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.
@@ -1527,42 +1625,34 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
             LanguageConfig {
                 name: "JavaScript".into(),
                 line_comment: Some("// ".into()),
-                brackets: vec![
-                    BracketPair {
-                        start: "{".into(),
-                        end: "}".into(),
-                        close: true,
-                        newline: false,
-                    },
-                    BracketPair {
-                        start: "'".into(),
-                        end: "'".into(),
-                        close: true,
-                        newline: false,
-                    },
-                ],
-                overrides: [
-                    (
-                        "element".into(),
-                        LanguageConfigOverride {
-                            line_comment: Override::Remove { remove: true },
-                            block_comment: Override::Set(("{/*".into(), "*/}".into())),
-                            ..Default::default()
+                brackets: BracketPairConfig {
+                    pairs: vec![
+                        BracketPair {
+                            start: "{".into(),
+                            end: "}".into(),
+                            close: true,
+                            newline: false,
                         },
-                    ),
-                    (
-                        "string".into(),
-                        LanguageConfigOverride {
-                            brackets: Override::Set(vec![BracketPair {
-                                start: "{".into(),
-                                end: "}".into(),
-                                close: true,
-                                newline: false,
-                            }]),
-                            ..Default::default()
+                        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()
@@ -1584,11 +1674,19 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
 
         let config = snapshot.language_scope_at(0).unwrap();
         assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
-        assert_eq!(config.brackets().len(), 2);
+        // 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!(config.line_comment_prefix().unwrap().as_ref(), "// ");
-        assert_eq!(string_config.brackets().len(), 1);
+        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);
@@ -1596,7 +1694,11 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
             element_config.block_comment_delimiters(),
             Some((&"{/*".into(), &"*/}".into()))
         );
-        assert_eq!(element_config.brackets().len(), 2);
+        // Both bracket pairs are enabled
+        assert_eq!(
+            element_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
+            &[true, true]
+        );
 
         buffer
     });
@@ -1719,25 +1821,31 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
             }
             30..=39 if mutation_count != 0 => {
                 buffer.update(cx, |buffer, cx| {
-                    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,
-                        });
+                    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);
                     }
-                    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;
             }
@@ -1939,6 +2047,45 @@ fn test_contiguous_ranges() {
     );
 }
 
+#[gpui::test(iterations = 500)]
+fn test_trailing_whitespace_ranges(mut rng: StdRng) {
+    // Generate a random multi-line string containing
+    // some lines with trailing whitespace.
+    let mut text = String::new();
+    for _ in 0..rng.gen_range(0..16) {
+        for _ in 0..rng.gen_range(0..36) {
+            text.push(match rng.gen_range(0..10) {
+                0..=1 => ' ',
+                3 => '\t',
+                _ => rng.gen_range('a'..'z'),
+            });
+        }
+        text.push('\n');
+    }
+
+    match rng.gen_range(0..10) {
+        // sometimes remove the last newline
+        0..=1 => drop(text.pop()), //
+
+        // sometimes add extra newlines
+        2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
+        _ => {}
+    }
+
+    let rope = Rope::from(text.as_str());
+    let actual_ranges = trailing_whitespace_ranges(&rope);
+    let expected_ranges = TRAILING_WHITESPACE_REGEX
+        .find_iter(&text)
+        .map(|m| m.range())
+        .collect::<Vec<_>>();
+    assert_eq!(
+        actual_ranges,
+        expected_ranges,
+        "wrong ranges for text lines:\n{:?}",
+        text.split("\n").collect::<Vec<_>>()
+    );
+}
+
 fn ruby_lang() -> Language {
     Language::new(
         LanguageConfig {

crates/language/src/highlight_map.rs 🔗

@@ -59,7 +59,6 @@ impl HighlightId {
         theme.highlights.get(self.0 as usize).map(|entry| entry.1)
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn name<'a>(&self, theme: &'a SyntaxTheme) -> Option<&'a str> {
         theme.highlights.get(self.0 as usize).map(|e| e.0.as_str())
     }

crates/language/src/language.rs 🔗

@@ -10,15 +10,16 @@ mod buffer_tests;
 
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use collections::HashMap;
 use futures::{
+    channel::oneshot,
     future::{BoxFuture, Shared},
-    FutureExt, TryFutureExt,
+    FutureExt, TryFutureExt as _,
 };
 use gpui::{executor::Background, MutableAppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
+use lsp::CodeActionKind;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use regex::Regex;
@@ -28,6 +29,7 @@ use std::{
     any::Any,
     borrow::Cow,
     cell::RefCell,
+    ffi::OsString,
     fmt::Debug,
     hash::Hash,
     mem,
@@ -43,7 +45,8 @@ use syntax_map::SyntaxSnapshot;
 use theme::{SyntaxTheme, Theme};
 use tree_sitter::{self, Query};
 use unicase::UniCase;
-use util::ResultExt;
+use util::http::HttpClient;
+use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
 
 #[cfg(any(test, feature = "test-support"))]
 use futures::channel::mpsc;
@@ -76,23 +79,27 @@ pub trait ToLspPosition {
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct LanguageServerName(pub Arc<str>);
 
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageServerBinary {
+    pub path: PathBuf,
+    pub arguments: Vec<OsString>,
+}
+
 /// 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 server_args: Vec<String>,
     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: Box<dyn LspAdapter>,
+    pub adapter: Arc<dyn LspAdapter>,
 }
 
 impl CachedLspAdapter {
-    pub async fn new(adapter: Box<dyn LspAdapter>) -> Arc<Self> {
+    pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
         let name = adapter.name().await;
-        let server_args = adapter.server_args().await;
         let initialization_options = adapter.initialization_options().await;
         let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
         let disk_based_diagnostics_progress_token =
@@ -101,7 +108,6 @@ impl CachedLspAdapter {
 
         Arc::new(CachedLspAdapter {
             name,
-            server_args,
             initialization_options,
             disk_based_diagnostic_sources,
             disk_based_diagnostics_progress_token,
@@ -122,16 +128,30 @@ impl CachedLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         self.adapter
             .fetch_server_binary(version, http, container_dir)
             .await
     }
 
-    pub async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    pub async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
         self.adapter.cached_server_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 MutableAppContext,
+    ) -> Option<BoxFuture<'static, Value>> {
+        self.adapter.workspace_configuration(cx)
+    }
+
     pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
         self.adapter.process_diagnostics(params).await
     }
@@ -174,9 +194,9 @@ pub trait LspAdapter: 'static + Send + Sync {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf>;
+    ) -> Result<LanguageServerBinary>;
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf>;
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary>;
 
     async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
@@ -199,14 +219,27 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        Vec::new()
+    async fn initialization_options(&self) -> Option<Value> {
+        None
     }
 
-    async fn initialization_options(&self) -> Option<Value> {
+    fn workspace_configuration(
+        &self,
+        _: &mut MutableAppContext,
+    ) -> Option<BoxFuture<'static, Value>> {
         None
     }
 
+    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()
     }
@@ -227,11 +260,11 @@ pub struct CodeLabel {
     pub filter_range: Range<usize>,
 }
 
-#[derive(Deserialize)]
+#[derive(Clone, Deserialize)]
 pub struct LanguageConfig {
     pub name: Arc<str>,
     pub path_suffixes: Vec<String>,
-    pub brackets: Vec<BracketPair>,
+    pub brackets: BracketPairConfig,
     #[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")]
@@ -258,23 +291,23 @@ pub struct LanguageQueries {
     pub overrides: Option<Cow<'static, str>>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct LanguageScope {
     language: Arc<Language>,
     override_id: Option<u32>,
 }
 
-#[derive(Deserialize, Default, Debug)]
+#[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(default)]
-    pub brackets: Override<Vec<BracketPair>>,
+    #[serde(skip_deserializing)]
+    pub disabled_bracket_ixs: Vec<u16>,
 }
 
-#[derive(Deserialize, Debug)]
+#[derive(Clone, Deserialize, Debug)]
 #[serde(untagged)]
 pub enum Override<T> {
     Remove { remove: bool },
@@ -336,7 +369,41 @@ pub struct FakeLspAdapter {
     pub disk_based_diagnostics_sources: Vec<String>,
 }
 
-#[derive(Clone, Debug, Default, Deserialize)]
+#[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,
@@ -393,7 +460,7 @@ struct InjectionConfig {
 
 struct OverrideConfig {
     query: Query,
-    values: HashMap<u32, LanguageConfigOverride>,
+    values: HashMap<u32, (String, LanguageConfigOverride)>,
 }
 
 #[derive(Default, Clone)]
@@ -417,17 +484,20 @@ pub enum LanguageServerBinaryStatus {
     Failed { error: String },
 }
 
+type AvailableLanguageId = usize;
+
+#[derive(Clone)]
 struct AvailableLanguage {
+    id: AvailableLanguageId,
     path: &'static str,
     config: LanguageConfig,
     grammar: tree_sitter::Language,
-    lsp_adapter: Option<Box<dyn LspAdapter>>,
+    lsp_adapter: Option<Arc<dyn LspAdapter>>,
     get_queries: fn(&str) -> LanguageQueries,
 }
 
 pub struct LanguageRegistry {
-    languages: RwLock<Vec<Arc<Language>>>,
-    available_languages: RwLock<Vec<AvailableLanguage>>,
+    state: RwLock<LanguageRegistryState>,
     language_server_download_dir: Option<Arc<Path>>,
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
     lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
@@ -436,29 +506,40 @@ pub struct LanguageRegistry {
     lsp_binary_paths: Mutex<
         HashMap<
             LanguageServerName,
-            Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
+            Shared<BoxFuture<'static, Result<LanguageServerBinary, Arc<anyhow::Error>>>>,
         >,
     >,
-    subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>,
-    theme: RwLock<Option<Arc<Theme>>>,
     executor: Option<Arc<Background>>,
-    version: AtomicUsize,
+}
+
+struct LanguageRegistryState {
+    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,
 }
 
 impl LanguageRegistry {
     pub fn new(login_shell_env_loaded: Task<()>) -> Self {
         let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
         Self {
+            state: RwLock::new(LanguageRegistryState {
+                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,
+            }),
             language_server_download_dir: None,
-            languages: Default::default(),
-            available_languages: Default::default(),
             lsp_binary_statuses_tx,
             lsp_binary_statuses_rx,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
             lsp_binary_paths: Default::default(),
-            subscription: RwLock::new(watch::channel()),
-            theme: Default::default(),
-            version: Default::default(),
             executor: None,
         }
     }
@@ -477,10 +558,12 @@ impl LanguageRegistry {
         path: &'static str,
         config: LanguageConfig,
         grammar: tree_sitter::Language,
-        lsp_adapter: Option<Box<dyn LspAdapter>>,
+        lsp_adapter: Option<Arc<dyn LspAdapter>>,
         get_queries: fn(&str) -> LanguageQueries,
     ) {
-        self.available_languages.write().push(AvailableLanguage {
+        let state = &mut *self.state.write();
+        state.available_languages.push(AvailableLanguage {
+            id: post_inc(&mut state.next_available_language_id),
             path,
             config,
             grammar,
@@ -490,42 +573,66 @@ impl LanguageRegistry {
     }
 
     pub fn language_names(&self) -> Vec<String> {
-        let mut result = self
+        let state = self.state.read();
+        let mut result = state
             .available_languages
-            .read()
             .iter()
             .map(|l| l.config.name.to_string())
-            .chain(
-                self.languages
-                    .read()
-                    .iter()
-                    .map(|l| l.config.name.to_string()),
-            )
+            .chain(state.languages.iter().map(|l| l.config.name.to_string()))
             .collect::<Vec<_>>();
-        result.sort_unstable();
+        result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
         result
     }
 
-    pub fn add(&self, language: Arc<Language>) {
-        if let Some(theme) = self.theme.read().clone() {
-            language.set_theme(&theme.editor.syntax);
+    pub fn workspace_configuration(&self, cx: &mut MutableAppContext) -> Task<serde_json::Value> {
+        let lsp_adapters = {
+            let state = self.state.read();
+            state
+                .available_languages
+                .iter()
+                .filter_map(|l| l.lsp_adapter.clone())
+                .chain(
+                    state
+                        .languages
+                        .iter()
+                        .filter_map(|l| l.adapter.as_ref().map(|a| a.adapter.clone())),
+                )
+                .collect::<Vec<_>>()
+        };
+
+        let mut language_configs = Vec::new();
+        for adapter in &lsp_adapters {
+            if let Some(language_config) = adapter.workspace_configuration(cx) {
+                language_configs.push(language_config);
+            }
         }
-        self.languages.write().push(language);
-        self.version.fetch_add(1, SeqCst);
-        *self.subscription.write().0.borrow_mut() = ();
+
+        cx.background().spawn(async move {
+            let mut config = serde_json::json!({});
+            let language_configs = futures::future::join_all(language_configs).await;
+            for language_config in language_configs {
+                merge_json_value_into(language_config, &mut config);
+            }
+            config
+        })
+    }
+
+    pub fn add(&self, language: Arc<Language>) {
+        self.state.write().add(language);
     }
 
     pub fn subscribe(&self) -> watch::Receiver<()> {
-        self.subscription.read().1.clone()
+        self.state.read().subscription.1.clone()
     }
 
     pub fn version(&self) -> usize {
-        self.version.load(SeqCst)
+        self.state.read().version
     }
 
     pub fn set_theme(&self, theme: Arc<Theme>) {
-        *self.theme.write() = Some(theme.clone());
-        for language in self.languages.read().iter() {
+        let mut state = self.state.write();
+        state.theme = Some(theme.clone());
+        for language in &state.languages {
             language.set_theme(&theme.editor.syntax);
         }
     }
@@ -534,12 +641,18 @@ impl LanguageRegistry {
         self.language_server_download_dir = Some(path.into());
     }
 
-    pub fn language_for_name(self: &Arc<Self>, name: &str) -> Option<Arc<Language>> {
+    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) -> Option<Arc<Language>> {
+    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
@@ -550,7 +663,10 @@ impl LanguageRegistry {
         })
     }
 
-    pub fn language_for_path(self: &Arc<Self>, path: impl AsRef<Path>) -> Option<Arc<Language>> {
+    pub fn language_for_path(
+        self: &Arc<Self>,
+        path: impl AsRef<Path>,
+    ) -> 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().and_then(|name| name.to_str());
@@ -566,47 +682,85 @@ impl LanguageRegistry {
     fn get_or_load_language(
         self: &Arc<Self>,
         callback: impl Fn(&LanguageConfig) -> bool,
-    ) -> Option<Arc<Language>> {
-        if let Some(language) = self
+    ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
+        let (tx, rx) = oneshot::channel();
+
+        let mut state = self.state.write();
+        if let Some(language) = state
             .languages
-            .read()
             .iter()
             .find(|language| callback(&language.config))
         {
-            return Some(language.clone());
-        }
-
-        if let Some(executor) = self.executor.clone() {
-            let mut available_languages = self.available_languages.write();
-
-            if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) {
-                let language = available_languages.remove(ix);
-                drop(available_languages);
-                let name = language.config.name.clone();
-                let this = self.clone();
-                executor
-                    .spawn(async move {
-                        let queries = (language.get_queries)(&language.path);
-                        let language = Language::new(language.config, Some(language.grammar))
-                            .with_lsp_adapter(language.lsp_adapter)
-                            .await;
-                        match language.with_queries(queries) {
-                            Ok(language) => this.add(Arc::new(language)),
-                            Err(err) => {
-                                log::error!("failed  to load language {}: {}", name, err);
-                                return;
-                            }
-                        };
-                    })
-                    .detach();
+            let _ = tx.send(Ok(language.clone()));
+        } else if let Some(executor) = self.executor.clone() {
+            if let Some(language) = state
+                .available_languages
+                .iter()
+                .find(|l| 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_adapter(language.lsp_adapter)
+                                        .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
+                                            .available_languages
+                                            .retain(|language| language.id != id);
+                                        if let Some(mut txs) = state.loading_languages.remove(&id) {
+                                            for tx in txs.drain(..) {
+                                                let _ = tx.send(Ok(language.clone()));
+                                            }
+                                        }
+                                    }
+                                    Err(err) => {
+                                        let mut state = this.state.write();
+                                        state
+                                            .available_languages
+                                            .retain(|language| language.id != 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,
+                                                    err
+                                                )));
+                                            }
+                                        }
+                                    }
+                                };
+                            })
+                            .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")));
         }
 
-        None
+        rx.unwrap()
     }
 
     pub fn to_vec(&self) -> Vec<Arc<Language>> {
-        self.languages.read().iter().cloned().collect()
+        self.state.read().languages.iter().cloned().collect()
     }
 
     pub fn start_language_server(
@@ -658,14 +812,15 @@ impl LanguageRegistry {
         let adapter = language.adapter.clone()?;
         let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
+
         Some(cx.spawn(|cx| async move {
             login_shell_env_loaded.await;
-            let server_binary_path = this
-                .lsp_binary_paths
-                .lock()
+
+            let mut lock = this.lsp_binary_paths.lock();
+            let entry = lock
                 .entry(adapter.name.clone())
                 .or_insert_with(|| {
-                    get_server_binary_path(
+                    get_binary(
                         adapter.clone(),
                         language.clone(),
                         http_client,
@@ -676,18 +831,19 @@ impl LanguageRegistry {
                     .boxed()
                     .shared()
                 })
-                .clone()
-                .map_err(|e| anyhow!(e));
+                .clone();
+            drop(lock);
+            let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
 
-            let server_binary_path = server_binary_path.await?;
-            let server_args = &adapter.server_args;
             let server = lsp::LanguageServer::new(
                 server_id,
-                &server_binary_path,
-                server_args,
+                &binary.path,
+                &binary.arguments,
                 &root_path,
+                adapter.code_action_kinds(),
                 cx,
             )?;
+
             Ok(server)
         }))
     }
@@ -699,6 +855,17 @@ impl LanguageRegistry {
     }
 }
 
+impl LanguageRegistryState {
+    fn add(&mut self, language: Arc<Language>) {
+        if let Some(theme) = self.theme.as_ref() {
+            language.set_theme(&theme.editor.syntax);
+        }
+        self.languages.push(language);
+        self.version += 1;
+        *self.subscription.0.borrow_mut() = ();
+    }
+}
+
 #[cfg(any(test, feature = "test-support"))]
 impl Default for LanguageRegistry {
     fn default() -> Self {
@@ -706,13 +873,13 @@ impl Default for LanguageRegistry {
     }
 }
 
-async fn get_server_binary_path(
+async fn get_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
     http_client: Arc<dyn HttpClient>,
     download_dir: Arc<Path>,
     statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
-) -> Result<PathBuf> {
+) -> Result<LanguageServerBinary> {
     let container_dir = download_dir.join(adapter.name.0.as_ref());
     if !container_dir.exists() {
         smol::fs::create_dir_all(&container_dir)
@@ -720,7 +887,7 @@ async fn get_server_binary_path(
             .context("failed to create container directory")?;
     }
 
-    let path = fetch_latest_server_binary_path(
+    let binary = fetch_latest_binary(
         adapter.clone(),
         language.clone(),
         http_client,
@@ -728,12 +895,13 @@ async fn get_server_binary_path(
         statuses.clone(),
     )
     .await;
-    if let Err(error) = path.as_ref() {
-        if let Some(cached_path) = adapter.cached_server_binary(container_dir).await {
+
+    if let Err(error) = binary.as_ref() {
+        if let Some(cached) = adapter.cached_server_binary(container_dir).await {
             statuses
                 .broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
                 .await?;
-            return Ok(cached_path);
+            return Ok(cached);
         } else {
             statuses
                 .broadcast((
@@ -745,16 +913,16 @@ async fn get_server_binary_path(
                 .await?;
         }
     }
-    path
+    binary
 }
 
-async fn fetch_latest_server_binary_path(
+async fn fetch_latest_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
     http_client: Arc<dyn HttpClient>,
     container_dir: &Path,
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
-) -> Result<PathBuf> {
+) -> Result<LanguageServerBinary> {
     let container_dir: Arc<Path> = container_dir.into();
     lsp_binary_statuses_tx
         .broadcast((
@@ -768,13 +936,13 @@ async fn fetch_latest_server_binary_path(
     lsp_binary_statuses_tx
         .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
         .await?;
-    let path = adapter
+    let binary = adapter
         .fetch_server_binary(version_info, http_client, container_dir.to_path_buf())
         .await?;
     lsp_binary_statuses_tx
         .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
         .await?;
-    Ok(path)
+    Ok(binary)
 }
 
 impl Language {
@@ -967,16 +1135,11 @@ impl Language {
     pub fn with_override_query(mut self, source: &str) -> Result<Self> {
         let query = Query::new(self.grammar_mut().ts_language, source)?;
 
-        let mut values = HashMap::default();
+        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).ok_or_else(|| {
-                    anyhow!(
-                        "language {:?} has override in query but not in config: {name:?}",
-                        self.config.name
-                    )
-                })?;
-                values.insert(ix as u32, value);
+                let value = self.config.overrides.remove(name).unwrap_or_default();
+                override_configs_by_id.insert(ix as u32, (name.clone(), value));
             }
         }
 
@@ -988,7 +1151,46 @@ impl Language {
             ))?;
         }
 
-        self.grammar_mut().override_config = Some(OverrideConfig { query, values });
+        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)
     }
 
@@ -996,7 +1198,7 @@ impl Language {
         Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
     }
 
-    pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Box<dyn LspAdapter>>) -> Self {
+    pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Arc<dyn LspAdapter>>) -> Self {
         if let Some(adapter) = lsp_adapter {
             self.adapter = Some(CachedLspAdapter::new(adapter).await);
         }
@@ -1010,7 +1212,7 @@ impl Language {
     ) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
         let (servers_tx, servers_rx) = mpsc::unbounded();
         self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
-        let adapter = CachedLspAdapter::new(Box::new(fake_lsp_adapter)).await;
+        let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
         self.adapter = Some(adapter);
         servers_rx
     }
@@ -1132,12 +1334,26 @@ impl LanguageScope {
         .map(|e| (&e.0, &e.1))
     }
 
-    pub fn brackets(&self) -> &[BracketPair] {
-        Override::as_option(
-            self.config_override().map(|o| &o.brackets),
-            Some(&self.language.config.brackets),
-        )
-        .map_or(&[], Vec::as_slice)
+    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 {
@@ -1148,7 +1364,7 @@ impl LanguageScope {
         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)
+        override_config.values.get(&id).map(|e| &e.1)
     }
 }
 
@@ -1259,11 +1475,11 @@ impl LspAdapter for Arc<FakeLspAdapter> {
         _: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         _: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         unreachable!();
     }
 
-    async fn cached_server_binary(&self, _: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
         unreachable!();
     }
 
@@ -1312,3 +1528,76 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
     }
     start..end
 }
+
+#[cfg(test)]
+mod tests {
+    use gpui::TestAppContext;
+
+    use super::*;
+
+    #[gpui::test(iterations = 10)]
+    async fn test_language_loading(cx: &mut TestAppContext) {
+        let mut languages = LanguageRegistry::test();
+        languages.set_executor(cx.background());
+        let languages = Arc::new(languages);
+        languages.register(
+            "/JSON",
+            LanguageConfig {
+                name: "JSON".into(),
+                path_suffixes: vec!["json".into()],
+                ..Default::default()
+            },
+            tree_sitter_json::language(),
+            None,
+            |_| Default::default(),
+        );
+        languages.register(
+            "/rust",
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".into()],
+                ..Default::default()
+            },
+            tree_sitter_rust::language(),
+            None,
+            |_| 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/language/src/syntax_map.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
 use collections::HashMap;
+use futures::FutureExt;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use std::{
@@ -165,6 +166,7 @@ struct ParseStep {
     mode: ParseMode,
 }
 
+#[derive(Debug)]
 enum ParseStepLanguage {
     Loaded { language: Arc<Language> },
     Pending { name: Arc<str> },
@@ -381,11 +383,11 @@ impl SyntaxSnapshot {
                 cursor.next(text);
                 while let Some(layer) = cursor.item() {
                     let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() };
-                    if {
-                        let language_registry = &registry;
-                        language_registry.language_for_name_or_extension(language_name)
-                    }
-                    .is_some()
+                    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));
                     }
@@ -514,15 +516,32 @@ impl SyntaxSnapshot {
                     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, .. }) =
                         old_layer.map(|layer| &layer.content)
                     {
                         if let ParseMode::Combined {
-                            parent_layer_changed_ranges,
+                            mut parent_layer_changed_ranges,
                             ..
                         } = step.mode
                         {
+                            for range in &mut parent_layer_changed_ranges {
+                                range.start -= step_start_byte;
+                                range.end -= step_start_byte;
+                            }
+
                             included_ranges = splice_included_ranges(
                                 old_tree.included_ranges(),
                                 &parent_layer_changed_ranges,
@@ -534,7 +553,6 @@ impl SyntaxSnapshot {
                             grammar,
                             text.as_rope(),
                             step_start_byte,
-                            step_start_point,
                             included_ranges,
                             Some(old_tree.clone()),
                         );
@@ -551,7 +569,6 @@ impl SyntaxSnapshot {
                             grammar,
                             text.as_rope(),
                             step_start_byte,
-                            step_start_point,
                             included_ranges,
                             None,
                         );
@@ -1060,17 +1077,9 @@ fn parse_text(
     grammar: &Grammar,
     text: &Rope,
     start_byte: usize,
-    start_point: Point,
-    mut ranges: Vec<tree_sitter::Range>,
+    ranges: Vec<tree_sitter::Range>,
     old_tree: Option<Tree>,
 ) -> Tree {
-    for range in &mut ranges {
-        range.start_byte -= start_byte;
-        range.end_byte -= start_byte;
-        range.start_point = (Point::from_ts_point(range.start_point) - start_point).to_ts_point();
-        range.end_point = (Point::from_ts_point(range.end_point) - start_point).to_ts_point();
-    }
-
     PARSER.with(|parser| {
         let mut parser = parser.borrow_mut();
         let mut chunks = text.chunks_in_range(start_byte..text.len());
@@ -1108,7 +1117,10 @@ fn get_injections(
     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)
+            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());
             }
@@ -1154,10 +1166,10 @@ fn get_injections(
             };
 
             if let Some(language_name) = language_name {
-                let language = {
-                    let language_name: &str = &language_name;
-                    language_registry.language_for_name_or_extension(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 {
@@ -2208,6 +2220,37 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    fn test_combined_injections_inside_injections() {
+        let (_buffer, _syntax_map) = test_edit_sequence(
+            "Markdown",
+            &[
+                r#"
+                      here is some ERB code:
+
+                      ```erb
+                      <ul>
+                        <% people.each do |person| %>
+                          <li><%= person.name %></li>
+                        <% end %>
+                      </ul>
+                      ```
+                "#,
+                r#"
+                    here is some ERB code:
+
+                    ```erb
+                    <ul>
+                      <% people«2».each do |person| %>
+                        <li><%= person.name %></li>
+                      <% end %>
+                    </ul>
+                    ```
+                "#,
+            ],
+        );
+    }
+
     #[gpui::test(iterations = 50)]
     fn test_random_syntax_map_edits(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
@@ -2483,7 +2526,11 @@ mod tests {
         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).unwrap();
+        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();

crates/language_selector/Cargo.toml 🔗

@@ -0,0 +1,21 @@
+[package]
+name = "language_selector"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/language_selector.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+language = { path = "../language" }
+gpui = { path = "../gpui" }
+picker = { path = "../picker" }
+project = { path = "../project" }
+theme = { path = "../theme" }
+settings = { path = "../settings" }
+workspace = { path = "../workspace" }
+anyhow = "1.0"

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -0,0 +1,93 @@
+use editor::Editor;
+use gpui::{
+    elements::*, CursorStyle, Entity, MouseButton, RenderContext, Subscription, View, ViewContext,
+    ViewHandle,
+};
+use settings::Settings;
+use std::sync::Arc;
+use workspace::{item::ItemHandle, StatusItemView};
+
+pub struct ActiveBufferLanguage {
+    active_language: Option<Option<Arc<str>>>,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl Default for ActiveBufferLanguage {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl ActiveBufferLanguage {
+    pub fn new() -> Self {
+        Self {
+            active_language: None,
+            _observe_active_editor: None,
+        }
+    }
+
+    fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+        self.active_language = Some(None);
+
+        let editor = editor.read(cx);
+        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
+            if let Some(language) = buffer.read(cx).language() {
+                self.active_language = Some(Some(language.name()));
+            }
+        }
+
+        cx.notify();
+    }
+}
+
+impl Entity for ActiveBufferLanguage {
+    type Event = ();
+}
+
+impl View for ActiveBufferLanguage {
+    fn ui_name() -> &'static str {
+        "ActiveBufferLanguage"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        if let Some(active_language) = self.active_language.as_ref() {
+            let active_language_text = if let Some(active_language_text) = active_language {
+                active_language_text.to_string()
+            } else {
+                "Unknown".to_string()
+            };
+
+            MouseEventHandler::<Self>::new(0, cx, |state, cx| {
+                let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+                let style = theme.active_language.style_for(state, false);
+                Label::new(active_language_text, style.text.clone())
+                    .contained()
+                    .with_style(style.container)
+                    .boxed()
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Toggle))
+            .boxed()
+        } else {
+            Empty::new().boxed()
+        }
+    }
+}
+
+impl StatusItemView for ActiveBufferLanguage {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
+            self.update_language(editor, cx);
+        } else {
+            self.active_language = None;
+            self._observe_active_editor = None;
+        }
+
+        cx.notify();
+    }
+}

crates/language_selector/src/language_selector.rs 🔗

@@ -0,0 +1,230 @@
+mod active_buffer_language;
+
+pub use active_buffer_language::ActiveBufferLanguage;
+use editor::Editor;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState,
+    MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+};
+use language::{Buffer, LanguageRegistry};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use settings::Settings;
+use std::sync::Arc;
+use workspace::{AppState, Workspace};
+
+actions!(language_selector, [Toggle]);
+
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
+    Picker::<LanguageSelector>::init(cx);
+    cx.add_action({
+        let language_registry = app_state.languages.clone();
+        move |workspace, _: &Toggle, cx| {
+            LanguageSelector::toggle(workspace, language_registry.clone(), cx)
+        }
+    });
+}
+
+pub enum Event {
+    Dismissed,
+}
+
+pub struct LanguageSelector {
+    buffer: ModelHandle<Buffer>,
+    project: ModelHandle<Project>,
+    language_registry: Arc<LanguageRegistry>,
+    candidates: Vec<StringMatchCandidate>,
+    matches: Vec<StringMatch>,
+    picker: ViewHandle<Picker<Self>>,
+    selected_index: usize,
+}
+
+impl LanguageSelector {
+    fn new(
+        buffer: ModelHandle<Buffer>,
+        project: ModelHandle<Project>,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let handle = cx.weak_handle();
+        let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx));
+
+        let candidates = language_registry
+            .language_names()
+            .into_iter()
+            .enumerate()
+            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
+            .collect::<Vec<_>>();
+        let mut matches = candidates
+            .iter()
+            .map(|candidate| StringMatch {
+                candidate_id: candidate.id,
+                score: 0.,
+                positions: Default::default(),
+                string: candidate.string.clone(),
+            })
+            .collect::<Vec<_>>();
+        matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
+
+        Self {
+            buffer,
+            project,
+            language_registry,
+            candidates,
+            matches,
+            picker,
+            selected_index: 0,
+        }
+    }
+
+    fn toggle(
+        workspace: &mut Workspace,
+        registry: Arc<LanguageRegistry>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        if let Some((_, buffer, _)) = workspace
+            .active_item(cx)
+            .and_then(|active_item| active_item.act_as::<Editor>(cx))
+            .and_then(|editor| editor.read(cx).active_excerpt(cx))
+        {
+            workspace.toggle_modal(cx, |workspace, cx| {
+                let project = workspace.project().clone();
+                let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx));
+                cx.subscribe(&this, Self::on_event).detach();
+                this
+            });
+        }
+    }
+
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<LanguageSelector>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => {
+                workspace.dismiss_modal(cx);
+            }
+        }
+    }
+}
+
+impl Entity for LanguageSelector {
+    type Event = Event;
+}
+
+impl View for LanguageSelector {
+    fn ui_name() -> &'static str {
+        "LanguageSelector"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(&self.picker, cx).boxed()
+    }
+
+    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(&self.picker);
+        }
+    }
+}
+
+impl PickerDelegate for LanguageSelector {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(mat) = self.matches.get(self.selected_index) {
+            let language_name = &self.candidates[mat.candidate_id].string;
+            let language = self.language_registry.language_for_name(language_name);
+            cx.spawn(|this, mut cx| async move {
+                let language = language.await?;
+                this.update(&mut cx, |this, cx| {
+                    this.project.update(cx, |project, cx| {
+                        project.set_language_for_buffer(&this.buffer, language, cx);
+                    });
+                });
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+
+        cx.emit(Event::Dismissed);
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+        let background = cx.background().clone();
+        let candidates = self.candidates.clone();
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, cx| {
+                this.matches = matches;
+                this.selected_index = this
+                    .selected_index
+                    .min(this.matches.len().saturating_sub(1));
+                cx.notify();
+            });
+        })
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &AppContext,
+    ) -> ElementBox {
+        let settings = cx.global::<Settings>();
+        let theme = &settings.theme;
+        let mat = &self.matches[ix];
+        let style = theme.picker.item.style_for(mouse_state, selected);
+        let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
+        let mut label = mat.string.clone();
+        if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
+            label.push_str(" (current)");
+        }
+
+        Label::new(label, style.label.clone())
+            .with_highlights(mat.positions.clone())
+            .contained()
+            .with_style(style.container)
+            .boxed()
+    }
+}

crates/live_kit_client/Cargo.toml 🔗

@@ -14,7 +14,7 @@ name = "test_app"
 
 [features]
 test-support = [
-    "async-trait", 
+    "async-trait",
     "collections/test-support",
     "gpui/test-support",
     "lazy_static",
@@ -35,7 +35,7 @@ core-graphics = "0.22.3"
 futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 
 async-trait = { version = "0.1", optional = true }
 lazy_static = { version = "1.4", optional = true }
@@ -62,10 +62,12 @@ jwt = "0.16"
 lazy_static = "1.4"
 objc = "0.2"
 parking_lot = "0.11.1"
-serde = { version = "1.0", features = ["derive", "rc"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 sha2 = "0.10"
 simplelog = "0.9"
 
 [build-dependencies]
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }

crates/live_kit_client/src/test.rs 🔗

@@ -104,6 +104,15 @@ impl TestServer {
                 room_name
             ))
         } else {
+            for track in &room.tracks {
+                client_room
+                    .0
+                    .lock()
+                    .video_track_updates
+                    .0
+                    .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
+                    .unwrap();
+            }
             room.client_rooms.insert(identity, client_room);
             Ok(())
         }
@@ -167,11 +176,13 @@ impl TestServer {
             .get_mut(&*room_name)
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
 
-        let update = RemoteVideoTrackUpdate::Subscribed(Arc::new(RemoteVideoTrack {
+        let track = Arc::new(RemoteVideoTrack {
             sid: nanoid::nanoid!(17),
             publisher_id: identity.clone(),
             frames_rx: local_track.frames_rx.clone(),
-        }));
+        });
+
+        room.tracks.push(track.clone());
 
         for (id, client_room) in &room.client_rooms {
             if *id != identity {
@@ -180,18 +191,30 @@ impl TestServer {
                     .lock()
                     .video_track_updates
                     .0
-                    .try_broadcast(update.clone())
+                    .try_broadcast(RemoteVideoTrackUpdate::Subscribed(track.clone()))
                     .unwrap();
             }
         }
 
         Ok(())
     }
+
+    fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
+        let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+        let room_name = claims.video.room.unwrap();
+
+        let mut server_rooms = self.rooms.lock();
+        let room = server_rooms
+            .get_mut(&*room_name)
+            .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
+        Ok(room.tracks.clone())
+    }
 }
 
 #[derive(Default)]
 struct TestServerRoom {
     client_rooms: HashMap<Sid, Arc<Room>>,
+    tracks: Vec<Arc<RemoteVideoTrack>>,
 }
 
 impl TestServerRoom {}
@@ -307,8 +330,17 @@ impl Room {
 
     pub fn unpublish_track(&self, _: LocalTrackPublication) {}
 
-    pub fn remote_video_tracks(&self, _: &str) -> Vec<Arc<RemoteVideoTrack>> {
-        Default::default()
+    pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
+        if !self.is_connected() {
+            return Vec::new();
+        }
+
+        self.test_server()
+            .video_tracks(self.token())
+            .unwrap()
+            .into_iter()
+            .filter(|track| track.publisher_id() == publisher_id)
+            .collect()
     }
 
     pub fn remote_video_track_updates(&self) -> impl Stream<Item = RemoteVideoTrackUpdate> {
@@ -332,6 +364,13 @@ impl Room {
             ConnectionState::Connected { token, .. } => token,
         }
     }
+
+    fn is_connected(&self) -> bool {
+        match *self.0.lock().connection.1.borrow() {
+            ConnectionState::Disconnected => false,
+            ConnectionState::Connected { .. } => true,
+        }
+    }
 }
 
 impl Drop for Room {

crates/live_kit_server/Cargo.toml 🔗

@@ -19,7 +19,8 @@ jwt = "0.16"
 prost = "0.8"
 prost-types = "0.8"
 reqwest = "0.11"
-serde = { version = "1.0", features = ["derive", "rc"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 sha2 = "0.10"
 
 [build-dependencies]

crates/lsp/Cargo.toml 🔗

@@ -21,9 +21,10 @@ futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 lsp-types = "0.91"
 parking_lot = "0.11"
-postage = { version = "0.4.1", features = ["futures-traits"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["raw_value"] }
+postage = { workspace = true }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 smol = "1.2"
 
 [dev-dependencies]

crates/lsp/src/lsp.rs 🔗

@@ -40,6 +40,7 @@ pub struct LanguageServer {
     outbound_tx: channel::Sender<Vec<u8>>,
     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>>>>,
     executor: Arc<executor::Background>,
@@ -108,8 +109,9 @@ impl LanguageServer {
     pub fn new<T: AsRef<std::ffi::OsStr>>(
         server_id: usize,
         binary_path: &Path,
-        args: &[T],
+        arguments: &[T],
         root_path: &Path,
+        code_action_kinds: Option<Vec<CodeActionKind>>,
         cx: AsyncAppContext,
     ) -> Result<Self> {
         let working_dir = if root_path.is_dir() {
@@ -117,9 +119,10 @@ impl LanguageServer {
         } else {
             root_path.parent().unwrap_or_else(|| Path::new("/"))
         };
+
         let mut server = process::Command::new(binary_path)
             .current_dir(working_dir)
-            .args(args)
+            .args(arguments)
             .stdin(Stdio::piped())
             .stdout(Stdio::piped())
             .stderr(Stdio::inherit())
@@ -128,13 +131,13 @@ impl LanguageServer {
 
         let stdin = server.stdin.take().unwrap();
         let stout = server.stdout.take().unwrap();
-
         let mut server = Self::new_internal(
             server_id,
             stdin,
             stout,
             Some(server),
             root_path,
+            code_action_kinds,
             cx,
             |notification| {
                 log::info!(
@@ -147,6 +150,7 @@ impl LanguageServer {
                 );
             },
         );
+
         if let Some(name) = binary_path.file_name() {
             server.name = name.to_string_lossy().to_string();
         }
@@ -159,16 +163,15 @@ impl LanguageServer {
         stdout: Stdout,
         server: Option<Child>,
         root_path: &Path,
+        code_action_kinds: Option<Vec<CodeActionKind>>,
         cx: AsyncAppContext,
-        mut on_unhandled_notification: F,
+        on_unhandled_notification: F,
     ) -> Self
     where
         Stdin: AsyncWrite + Unpin + Send + 'static,
         Stdout: AsyncRead + Unpin + Send + 'static,
         F: FnMut(AnyNotification) + 'static + Send,
     {
-        let mut stdin = BufWriter::new(stdin);
-        let mut stdout = BufReader::new(stdout);
         let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
         let notification_handlers =
             Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
@@ -177,89 +180,19 @@ impl LanguageServer {
         let input_task = cx.spawn(|cx| {
             let notification_handlers = notification_handlers.clone();
             let response_handlers = response_handlers.clone();
-            async move {
-                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 message_len: usize = std::str::from_utf8(&buffer)?
-                        .strip_prefix(CONTENT_LEN_HEADER)
-                        .ok_or_else(|| anyhow!("invalid header"))?
-                        .trim_end()
-                        .parse()?;
-
-                    buffer.resize(message_len, 0);
-                    stdout.read_exact(&mut buffer).await?;
-                    log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
-
-                    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.get(), 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()));
-                            } else {
-                                handler(Ok("null"));
-                            }
-                        }
-                    } else {
-                        warn!(
-                            "Failed to deserialize 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;
-                }
-            }
+            Self::handle_input(
+                stdout,
+                on_unhandled_notification,
+                notification_handlers,
+                response_handlers,
+                cx,
+            )
             .log_err()
         });
         let (output_done_tx, output_done_rx) = barrier::channel();
         let output_task = cx.background().spawn({
             let response_handlers = response_handlers.clone();
-            async move {
-                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:{}", String::from_utf8_lossy(&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).await?;
-                    stdin.flush().await?;
-                }
-                drop(output_done_tx);
-                Ok(())
-            }
-            .log_err()
+            Self::handle_output(stdin, outbound_rx, output_done_tx, response_handlers).log_err()
         });
 
         Self {
@@ -268,6 +201,7 @@ impl LanguageServer {
             response_handlers,
             name: Default::default(),
             capabilities: Default::default(),
+            code_action_kinds,
             next_id: Default::default(),
             outbound_tx,
             executor: cx.background(),
@@ -278,6 +212,109 @@ impl LanguageServer {
         }
     }
 
+    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>>>>,
+        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 message_len: usize = std::str::from_utf8(&buffer)?
+                .strip_prefix(CONTENT_LEN_HEADER)
+                .ok_or_else(|| anyhow!("invalid header"))?
+                .trim_end()
+                .parse()?;
+
+            buffer.resize(message_len, 0);
+            stdout.read_exact(&mut buffer).await?;
+            log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
+
+            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.get(), 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()));
+                    } else {
+                        handler(Ok("null"));
+                    }
+                }
+            } else {
+                warn!(
+                    "Failed to deserialize 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_output<Stdin>(
+        stdin: Stdin,
+        outbound_rx: channel::Receiver<Vec<u8>>,
+        output_done_tx: barrier::Sender,
+        response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+    ) -> 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:{}", String::from_utf8_lossy(&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).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.
@@ -292,6 +329,9 @@ impl LanguageServer {
             capabilities: ClientCapabilities {
                 workspace: Some(WorkspaceClientCapabilities {
                     configuration: Some(true),
+                    did_change_watched_files: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: Some(true),
+                    }),
                     did_change_configuration: Some(DynamicRegistrationClientCapabilities {
                         dynamic_registration: Some(true),
                     }),
@@ -389,7 +429,7 @@ impl LanguageServer {
                     output_done.recv().await;
                     log::debug!("language server shutdown finished");
                     drop(tasks);
-                    Ok(())
+                    anyhow::Ok(())
                 }
                 .log_err(),
             )
@@ -422,6 +462,10 @@ impl LanguageServer {
         self.notification_handlers.lock().remove(T::METHOD);
     }
 
+    pub fn remove_notification_handler<T: notification::Notification>(&self) {
+        self.notification_handlers.lock().remove(T::METHOD);
+    }
+
     #[must_use]
     pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
     where
@@ -680,6 +724,7 @@ impl LanguageServer {
             stdout_reader,
             None,
             Path::new("/"),
+            None,
             cx.clone(),
             |_| {},
         );
@@ -690,6 +735,7 @@ impl LanguageServer {
                 stdin_reader,
                 None,
                 Path::new("/"),
+                None,
                 cx,
                 move |msg| {
                     notifications_tx
@@ -780,6 +826,26 @@ impl FakeLanguageServer {
         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, gpui::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,

crates/node_runtime/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "node_runtime"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+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"
+futures = "0.3"
+anyhow = "1.0.38"
+parking_lot = "0.11.1"
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
+smol = "1.2.5"

crates/node_runtime/src/node_runtime.rs 🔗

@@ -0,0 +1,166 @@
+use anyhow::{anyhow, bail, Context, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use async_tar::Archive;
+use futures::{future::Shared, FutureExt};
+use gpui::{executor::Background, Task};
+use parking_lot::Mutex;
+use serde::Deserialize;
+use smol::{fs, io::BufReader};
+use std::{
+    env::consts,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::http::HttpClient;
+
+const VERSION: &str = "v18.15.0";
+
+#[derive(Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct NpmInfo {
+    #[serde(default)]
+    dist_tags: NpmInfoDistTags,
+    versions: Vec<String>,
+}
+
+#[derive(Deserialize, Default)]
+pub struct NpmInfoDistTags {
+    latest: Option<String>,
+}
+
+pub struct NodeRuntime {
+    http: Arc<dyn HttpClient>,
+    background: Arc<Background>,
+    installation_path: Mutex<Option<Shared<Task<Result<PathBuf, Arc<anyhow::Error>>>>>>,
+}
+
+impl NodeRuntime {
+    pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
+        Arc::new(NodeRuntime {
+            http,
+            background,
+            installation_path: Mutex::new(None),
+        })
+    }
+
+    pub async fn binary_path(&self) -> Result<PathBuf> {
+        let installation_path = self.install_if_needed().await?;
+        Ok(installation_path.join("bin/node"))
+    }
+
+    pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
+        let installation_path = self.install_if_needed().await?;
+        let node_binary = installation_path.join("bin/node");
+        let npm_file = installation_path.join("bin/npm");
+
+        let output = smol::process::Command::new(node_binary)
+            .arg(npm_file)
+            .args(["-fetch-retry-mintimeout", "2000"])
+            .args(["-fetch-retry-maxtimeout", "5000"])
+            .args(["-fetch-timeout", "5000"])
+            .args(["info", name, "--json"])
+            .output()
+            .await
+            .context("failed to run npm info")?;
+
+        if !output.status.success() {
+            Err(anyhow!(
+                "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr)
+            ))?;
+        }
+
+        let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
+        info.dist_tags
+            .latest
+            .or_else(|| info.versions.pop())
+            .ok_or_else(|| anyhow!("no version found for npm package {}", name))
+    }
+
+    pub async fn npm_install_packages(
+        &self,
+        packages: impl IntoIterator<Item = (&str, &str)>,
+        directory: &Path,
+    ) -> Result<()> {
+        let installation_path = self.install_if_needed().await?;
+        let node_binary = installation_path.join("bin/node");
+        let npm_file = installation_path.join("bin/npm");
+
+        let output = smol::process::Command::new(node_binary)
+            .arg(npm_file)
+            .args(["-fetch-retry-mintimeout", "2000"])
+            .args(["-fetch-retry-maxtimeout", "5000"])
+            .args(["-fetch-timeout", "5000"])
+            .arg("install")
+            .arg("--prefix")
+            .arg(directory)
+            .args(
+                packages
+                    .into_iter()
+                    .map(|(name, version)| format!("{name}@{version}")),
+            )
+            .output()
+            .await
+            .context("failed to run npm install")?;
+        if !output.status.success() {
+            Err(anyhow!(
+                "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr)
+            ))?;
+        }
+        Ok(())
+    }
+
+    async fn install_if_needed(&self) -> Result<PathBuf> {
+        let task = self
+            .installation_path
+            .lock()
+            .get_or_insert_with(|| {
+                let http = self.http.clone();
+                self.background
+                    .spawn(async move { Self::install(http).await.map_err(Arc::new) })
+                    .shared()
+            })
+            .clone();
+
+        match task.await {
+            Ok(path) => Ok(path),
+            Err(error) => Err(anyhow!("{}", error)),
+        }
+    }
+
+    async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
+        let arch = match consts::ARCH {
+            "x86_64" => "x64",
+            "aarch64" => "arm64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+
+        let folder_name = format!("node-{VERSION}-darwin-{arch}");
+        let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
+        let node_dir = node_containing_dir.join(folder_name);
+        let node_binary = node_dir.join("bin/node");
+
+        if fs::metadata(&node_binary).await.is_err() {
+            _ = fs::remove_dir_all(&node_containing_dir).await;
+            fs::create_dir(&node_containing_dir)
+                .await
+                .context("error creating node containing dir")?;
+
+            let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz");
+            let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
+            let mut response = http
+                .get(&url, Default::default(), true)
+                .await
+                .context("error downloading Node binary tarball")?;
+
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let archive = Archive::new(decompressed_bytes);
+            archive.unpack(&node_containing_dir).await?;
+        }
+
+        anyhow::Ok(node_dir)
+    }
+}

crates/outline/Cargo.toml 🔗

@@ -18,5 +18,5 @@ settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2"

crates/outline/src/outline.rs 🔗

@@ -49,7 +49,7 @@ impl View for OutlineView {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {

crates/pando/Cargo.toml 🔗

@@ -1,21 +0,0 @@
-[package]
-name = "pando"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/pando.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-anyhow = "1.0.38"
-client = { path = "../client" }
-gpui = { path = "../gpui" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
-sqlez = { path = "../sqlez" }
-sqlez_macros = { path = "../sqlez_macros" }

crates/pando/src/pando.rs 🔗

@@ -1,15 +0,0 @@
-//! ## Goals
-//! - Opinionated Subset of Obsidian. Only the things that cant be done other ways in zed
-//! - Checked in .zp file is an sqlite db containing graph metadata
-//! - All nodes are file urls
-//! - Markdown links auto add soft linked nodes to the db
-//! - Links create positioning data regardless of if theres a file
-//! - Lock links to make structure that doesn't rotate or spread
-//! - Drag from file finder to pando item to add it in
-//! - For linked files, zoom out to see closest linking pando file
-
-//! ## Plan
-//! - [ ] Make item backed by .zp sqlite file with camera position by user account
-//! - [ ] Render grid of dots and allow scrolling around the grid
-//! - [ ] Add scale property to layer canvas and manipulate it with pinch zooming
-//! - [ ] Allow dropping files onto .zp pane. Their relative path is recorded into the file along with

crates/picker/Cargo.toml 🔗

@@ -21,7 +21,7 @@ parking_lot = "0.11.1"
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.9"

crates/picker/src/picker.rs 🔗

@@ -80,7 +80,7 @@ impl<D: PickerDelegate> View for Picker<D> {
                     None
                 } else {
                     Some(
-                        Label::new("No matches".into(), theme.no_matches.label.clone())
+                        Label::new("No matches", theme.no_matches.label.clone())
                             .contained()
                             .with_style(theme.no_matches.container)
                             .boxed(),
@@ -102,7 +102,10 @@ impl<D: PickerDelegate> View for Picker<D> {
                                         .read(cx)
                                         .render_match(ix, state, ix == selected_ix, cx)
                                 })
-                                .on_down(MouseButton::Left, move |_, cx| {
+                                // Capture mouse events
+                                .on_down(MouseButton::Left, |_, _| {})
+                                .on_up(MouseButton::Left, |_, _| {})
+                                .on_click(MouseButton::Left, move |_, cx| {
                                     cx.dispatch_action(SelectIndex(ix))
                                 })
                                 .with_cursor_style(CursorStyle::PointingHand)
@@ -126,7 +129,7 @@ impl<D: PickerDelegate> View for Picker<D> {
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 
@@ -205,6 +208,11 @@ impl<D: PickerDelegate> Picker<D> {
         self.query_editor.read(cx).text(cx)
     }
 
+    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+        self.query_editor
+            .update(cx, |editor, cx| editor.set_text(query, cx));
+    }
+
     fn on_query_editor_event(
         &mut self,
         _: ViewHandle<Editor>,
@@ -249,9 +257,13 @@ impl<D: PickerDelegate> Picker<D> {
 
     pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
-            let index = 0;
-            delegate.update(cx, |delegate, cx| delegate.set_selected_index(0, cx));
-            self.list_state.scroll_to(ScrollTarget::Show(index));
+            delegate.update(cx, |delegate, cx| {
+                if delegate.match_count() > 0 {
+                    delegate.set_selected_index(0, cx);
+                    self.list_state.scroll_to(ScrollTarget::Show(0));
+                }
+            });
+
             cx.notify();
         }
     }
@@ -259,53 +271,56 @@ impl<D: PickerDelegate> Picker<D> {
     pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
             let index = action.0;
-            self.confirmed = true;
             delegate.update(cx, |delegate, cx| {
-                delegate.set_selected_index(index, cx);
-                delegate.confirm(cx);
+                if delegate.match_count() > 0 {
+                    self.confirmed = true;
+                    delegate.set_selected_index(index, cx);
+                    delegate.confirm(cx);
+                }
             });
         }
     }
 
     pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
-            let index = delegate.update(cx, |delegate, cx| {
+            delegate.update(cx, |delegate, cx| {
                 let match_count = delegate.match_count();
-                let index = if match_count > 0 { match_count - 1 } else { 0 };
-                delegate.set_selected_index(index, cx);
-                index
+                if match_count > 0 {
+                    let index = match_count - 1;
+                    delegate.set_selected_index(index, cx);
+                    self.list_state.scroll_to(ScrollTarget::Show(index));
+                }
             });
-            self.list_state.scroll_to(ScrollTarget::Show(index));
             cx.notify();
         }
     }
 
     pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
-            let index = delegate.update(cx, |delegate, cx| {
-                let mut selected_index = delegate.selected_index();
-                if selected_index + 1 < delegate.match_count() {
-                    selected_index += 1;
-                    delegate.set_selected_index(selected_index, cx);
+            delegate.update(cx, |delegate, cx| {
+                let next_index = delegate.selected_index() + 1;
+                if next_index < delegate.match_count() {
+                    delegate.set_selected_index(next_index, cx);
+                    self.list_state.scroll_to(ScrollTarget::Show(next_index));
                 }
-                selected_index
             });
-            self.list_state.scroll_to(ScrollTarget::Show(index));
+
             cx.notify();
         }
     }
 
     pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
-            let index = delegate.update(cx, |delegate, cx| {
+            delegate.update(cx, |delegate, cx| {
                 let mut selected_index = delegate.selected_index();
                 if selected_index > 0 {
                     selected_index -= 1;
                     delegate.set_selected_index(selected_index, cx);
+                    self.list_state
+                        .scroll_to(ScrollTarget::Show(selected_index));
                 }
-                selected_index
             });
-            self.list_state.scroll_to(ScrollTarget::Show(index));
+
             cx.notify();
         }
     }

crates/plugin/Cargo.toml 🔗

@@ -5,6 +5,7 @@ edition = "2021"
 publish = false
 
 [dependencies]
-serde = "1.0"
+serde = { workspace = true }
+serde_derive = { workspace = true }
 bincode = "1.3"
 plugin_macros = { path = "../plugin_macros" }

crates/plugin_macros/Cargo.toml 🔗

@@ -11,5 +11,6 @@ proc-macro = true
 syn = { version = "1.0", features = ["full",  "extra-traits"] }
 quote = "1.0"
 proc-macro2 = "1.0"
-serde = "1.0"
+serde = { workspace = true }
+serde_derive = { workspace = true }
 bincode = "1.3"

crates/plugin_runtime/Cargo.toml 🔗

@@ -9,8 +9,9 @@ wasmtime = "0.38"
 wasmtime-wasi = "0.38"
 wasi-common = "0.38"
 anyhow = { version = "1.0", features = ["std"] }
-serde = "1.0"
-serde_json = "1.0"
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 bincode = "1.3"
 pollster = "0.2.5"
 smol = "1.2.5"

crates/project/Cargo.toml 🔗

@@ -27,6 +27,7 @@ fs = { path = "../fs" }
 fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 git = { path = "../git" }
+glob = { version = "0.3.1" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
@@ -44,12 +45,13 @@ ignore = "0.4"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 pulldown-cmark = { version = "0.9.1", default-features = false }
 rand = "0.8.3"
 regex = "1.5"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 sha2 = "0.10"
 similar = "1.3"
 smol = "1.2.5"
@@ -57,6 +59,9 @@ thiserror = "1.0.29"
 toml = "0.5"
 
 [dev-dependencies]
+ctor = "0.1"
+env_logger = "0.9"
+pretty_assertions = "1.3.0"
 client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
 db = { path = "../db", features = ["test-support"] }

crates/project/src/lsp_glob_set.rs 🔗

@@ -0,0 +1,121 @@
+use anyhow::{anyhow, Result};
+use std::path::Path;
+
+#[derive(Default)]
+pub struct LspGlobSet {
+    patterns: Vec<glob::Pattern>,
+}
+
+impl LspGlobSet {
+    pub fn clear(&mut self) {
+        self.patterns.clear();
+    }
+
+    /// Add a pattern to the glob set.
+    ///
+    /// LSP's glob syntax supports bash-style brace expansion. For example,
+    /// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
+    /// This is not a part of the standard libc glob syntax, and isn't supported
+    /// by the `glob` crate. So we pre-process the glob patterns, producing a
+    /// separate glob `Pattern` object for each part of a brace expansion.
+    pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
+        // Find all of the ranges of `pattern` that contain matched curly braces.
+        let mut expansion_ranges = Vec::new();
+        let mut expansion_start_ix = None;
+        for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
+            match c {
+                "{" => {
+                    if expansion_start_ix.is_some() {
+                        return Err(anyhow!("nested braces in glob patterns aren't supported"));
+                    }
+                    expansion_start_ix = Some(ix);
+                }
+                "}" => {
+                    if let Some(start_ix) = expansion_start_ix {
+                        expansion_ranges.push(start_ix..ix + 1);
+                    }
+                    expansion_start_ix = None;
+                }
+                _ => {}
+            }
+        }
+
+        // Starting with a single pattern, process each brace expansion by cloning
+        // the pattern once per element of the expansion.
+        let mut unexpanded_patterns = vec![];
+        let mut expanded_patterns = vec![pattern.to_string()];
+
+        for outer_range in expansion_ranges.into_iter().rev() {
+            let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
+            std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
+            for unexpanded_pattern in unexpanded_patterns.drain(..) {
+                for part in unexpanded_pattern[inner_range.clone()].split(',') {
+                    let mut expanded_pattern = unexpanded_pattern.clone();
+                    expanded_pattern.replace_range(outer_range.clone(), part);
+                    expanded_patterns.push(expanded_pattern);
+                }
+            }
+        }
+
+        // Parse the final glob patterns and add them to the set.
+        for pattern in expanded_patterns {
+            let pattern = glob::Pattern::new(&pattern)?;
+            self.patterns.push(pattern);
+        }
+
+        Ok(())
+    }
+
+    pub fn matches(&self, path: &Path) -> bool {
+        self.patterns
+            .iter()
+            .any(|pattern| pattern.matches_path(path))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_glob_set() {
+        let mut watch = LspGlobSet::default();
+        watch.add_pattern("/a/**/*.rs").unwrap();
+        watch.add_pattern("/a/**/Cargo.toml").unwrap();
+
+        assert!(watch.matches("/a/b.rs".as_ref()));
+        assert!(watch.matches("/a/b/c.rs".as_ref()));
+
+        assert!(!watch.matches("/b/c.rs".as_ref()));
+        assert!(!watch.matches("/a/b.ts".as_ref()));
+    }
+
+    #[test]
+    fn test_brace_expansion() {
+        let mut watch = LspGlobSet::default();
+        watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
+
+        assert!(watch.matches("/a/one.js".as_ref()));
+        assert!(watch.matches("/a/two.ts".as_ref()));
+        assert!(watch.matches("/a/three.tsx".as_ref()));
+
+        assert!(!watch.matches("/a/one.j".as_ref()));
+        assert!(!watch.matches("/a/two.s".as_ref()));
+        assert!(!watch.matches("/a/three.t".as_ref()));
+        assert!(!watch.matches("/a/four.t".as_ref()));
+        assert!(!watch.matches("/a/five.xt".as_ref()));
+    }
+
+    #[test]
+    fn test_multiple_brace_expansion() {
+        let mut watch = LspGlobSet::default();
+        watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
+
+        assert!(watch.matches("/a/one.bic".as_ref()));
+        assert!(watch.matches("/a/two.dole".as_ref()));
+        assert!(watch.matches("/a/three.deeee".as_ref()));
+
+        assert!(!watch.matches("/a/four.bic".as_ref()));
+        assert!(!watch.matches("/a/one.be".as_ref()));
+    }
+}

crates/project/src/project.rs 🔗

@@ -1,6 +1,8 @@
 mod ignore;
 mod lsp_command;
+mod lsp_glob_set;
 pub mod search;
+pub mod terminals;
 pub mod worktree;
 
 #[cfg(test)]
@@ -26,17 +28,17 @@ use language::{
         serialize_anchor, serialize_version,
     },
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
-    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent,
+    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
     File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
     Operation, Patch, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
     Transaction, Unclipped,
 };
 use lsp::{
-    DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
-    MarkedString,
+    DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
+    DocumentHighlightKind, LanguageServer, LanguageString, MarkedString,
 };
 use lsp_command::*;
-use parking_lot::Mutex;
+use lsp_glob_set::LspGlobSet;
 use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
@@ -61,8 +63,9 @@ use std::{
     },
     time::{Duration, Instant, SystemTime},
 };
-use terminal::{Terminal, TerminalBuilder};
-use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _};
+use terminals::Terminals;
+
+use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
 
 pub use fs::*;
 pub use worktree::*;
@@ -93,7 +96,6 @@ pub struct Project {
     language_servers: HashMap<usize, LanguageServerState>,
     language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>,
     language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
-    language_server_settings: Arc<Mutex<serde_json::Value>>,
     last_workspace_edits_by_language_server: HashMap<usize, ProjectTransaction>,
     next_language_server_id: usize,
     client: Arc<client::Client>,
@@ -123,6 +125,8 @@ pub struct Project {
     buffers_being_formatted: HashSet<usize>,
     nonce: u128,
     _maintain_buffer_languages: Task<()>,
+    _maintain_workspace_config: Task<()>,
+    terminals: Terminals,
 }
 
 enum OpenBuffer {
@@ -172,6 +176,7 @@ pub enum Event {
     },
     RemoteIdChanged(Option<u64>),
     DisconnectedFromHost,
+    Closed,
     CollaboratorUpdated {
         old_peer_id: proto::PeerId,
         new_peer_id: proto::PeerId,
@@ -185,6 +190,7 @@ pub enum LanguageServerState {
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
         server: Arc<LanguageServer>,
+        watched_paths: LspGlobSet,
         simulate_disk_based_diagnostics_completion: Option<Task<()>>,
     },
 }
@@ -424,6 +430,7 @@ impl Project {
             client_subscriptions: Vec::new(),
             _subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
             _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
+            _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
             active_entry: None,
             languages,
             client,
@@ -435,10 +442,12 @@ impl Project {
             language_server_ids: Default::default(),
             language_server_statuses: Default::default(),
             last_workspace_edits_by_language_server: Default::default(),
-            language_server_settings: Default::default(),
             buffers_being_formatted: Default::default(),
             next_language_server_id: 0,
             nonce: StdRng::from_entropy().gen(),
+            terminals: Terminals {
+                local_handles: Vec::new(),
+            },
         })
     }
 
@@ -479,6 +488,7 @@ impl Project {
                 active_entry: None,
                 collaborators: Default::default(),
                 _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
+                _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
                 languages,
                 user_store: user_store.clone(),
                 fs,
@@ -494,7 +504,6 @@ impl Project {
                 }),
                 language_servers: Default::default(),
                 language_server_ids: Default::default(),
-                language_server_settings: Default::default(),
                 language_server_statuses: response
                     .language_servers
                     .into_iter()
@@ -516,6 +525,9 @@ impl Project {
                 buffers_being_formatted: Default::default(),
                 buffer_snapshots: Default::default(),
                 nonce: StdRng::from_entropy().gen(),
+                terminals: Terminals {
+                    local_handles: Vec::new(),
+                },
             };
             for worktree in worktrees {
                 let _ = this.add_worktree(&worktree, cx);
@@ -556,7 +568,7 @@ impl Project {
 
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.background());
-        let http_client = client::test::FakeHttpClient::with_404_response();
+        let http_client = util::http::FakeHttpClient::with_404_response();
         let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let project =
@@ -1163,6 +1175,10 @@ impl Project {
         }
     }
 
+    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 {
@@ -1184,34 +1200,6 @@ impl Project {
         !self.is_local()
     }
 
-    pub fn create_terminal(
-        &mut self,
-        working_directory: Option<PathBuf>,
-        window_id: usize,
-        cx: &mut ModelContext<Self>,
-    ) -> Result<ModelHandle<Terminal>> {
-        if self.is_remote() {
-            return Err(anyhow!(
-                "creating terminals as a guest is not supported yet"
-            ));
-        } else {
-            let settings = cx.global::<Settings>();
-            let shell = settings.terminal_shell();
-            let envs = settings.terminal_env();
-            let scroll = settings.terminal_scroll();
-
-            TerminalBuilder::new(
-                working_directory.clone(),
-                shell,
-                envs,
-                settings.terminal_overrides.blinking.clone(),
-                scroll,
-                window_id,
-            )
-            .map(|builder| cx.add_model(|cx| builder.subscribe(cx)))
-        }
-    }
-
     pub fn create_buffer(
         &mut self,
         text: &str,
@@ -1243,7 +1231,9 @@ impl Project {
                     File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
                 })
                 .ok_or_else(|| anyhow!("no project entry"))?;
-            Ok((project_entry_id, buffer.into()))
+
+            let buffer: &AnyModelHandle = &buffer;
+            Ok((project_entry_id, buffer.clone()))
         })
     }
 
@@ -1483,7 +1473,7 @@ impl Project {
                 })
                 .await?;
             this.update(&mut cx, |this, cx| {
-                this.assign_language_to_buffer(&buffer, cx);
+                this.detect_language_for_buffer(&buffer, cx);
                 this.register_buffer_with_language_server(&buffer, cx);
             });
             Ok(())
@@ -1550,7 +1540,7 @@ impl Project {
         })
         .detach();
 
-        self.assign_language_to_buffer(buffer, cx);
+        self.detect_language_for_buffer(buffer, cx);
         self.register_buffer_with_language_server(buffer, cx);
         cx.observe_release(buffer, |this, buffer, cx| {
             if let Some(file) = File::from_dyn(buffer.file()) {
@@ -1837,7 +1827,7 @@ impl Project {
                         }
 
                         for buffer in plain_text_buffers {
-                            project.assign_language_to_buffer(&buffer, cx);
+                            project.detect_language_for_buffer(&buffer, cx);
                             project.register_buffer_with_language_server(&buffer, cx);
                         }
 
@@ -1850,14 +1840,64 @@ impl Project {
         })
     }
 
-    fn assign_language_to_buffer(
+    fn maintain_workspace_config(
+        languages: Arc<LanguageRegistry>,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<()> {
+        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::<Settings, _>(move |_, _| {
+            *settings_changed_tx.borrow_mut() = ();
+        });
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Some(_) = settings_changed_rx.next().await {
+                let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.read_with(&cx, |this, _| {
+                        for server_state in this.language_servers.values() {
+                            if let LanguageServerState::Running { server, .. } = server_state {
+                                server
+                                    .notify::<lsp::notification::DidChangeConfiguration>(
+                                        lsp::DidChangeConfigurationParams {
+                                            settings: workspace_config.clone(),
+                                        },
+                                    )
+                                    .ok();
+                            }
+                        }
+                    })
+                } else {
+                    break;
+                }
+            }
+
+            drop(settings_observation);
+        })
+    }
+
+    fn detect_language_for_buffer(
         &mut self,
         buffer: &ModelHandle<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 full_path = buffer.read(cx).file()?.full_path(cx);
-        let new_language = self.languages.language_for_path(&full_path)?;
+        let new_language = self
+            .languages
+            .language_for_path(&full_path)
+            .now_or_never()?
+            .ok()?;
+        self.set_language_for_buffer(buffer, new_language, cx);
+        None
+    }
+
+    pub fn set_language_for_buffer(
+        &mut self,
+        buffer: &ModelHandle<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)
@@ -1866,30 +1906,12 @@ impl Project {
             }
         });
 
-        let file = File::from_dyn(buffer.read(cx).file())?;
-        let worktree = file.worktree.read(cx).as_local()?;
-        let worktree_id = worktree.id();
-        let worktree_abs_path = worktree.abs_path().clone();
-        self.start_language_server(worktree_id, worktree_abs_path, new_language, cx);
-
-        None
-    }
-
-    fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
-        use serde_json::Value;
-
-        match (source, target) {
-            (Value::Object(source), Value::Object(target)) => {
-                for (key, value) in source {
-                    if let Some(target) = target.get_mut(&key) {
-                        Self::merge_json_value_into(value, target);
-                    } else {
-                        target.insert(key.clone(), value);
-                    }
-                }
+        if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
+            if let Some(worktree) = file.worktree.read(cx).as_local() {
+                let worktree_id = worktree.id();
+                let worktree_abs_path = worktree.abs_path().clone();
+                self.start_language_server(worktree_id, worktree_abs_path, new_language, cx);
             }
-
-            (source, target) => *target = source,
         }
     }
 
@@ -1920,17 +1942,16 @@ impl Project {
         let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
         match (&mut initialization_options, override_options) {
             (Some(initialization_options), Some(override_options)) => {
-                Self::merge_json_value_into(override_options, initialization_options);
+                merge_json_value_into(override_options, initialization_options);
             }
-
             (None, override_options) => initialization_options = override_options,
-
             _ => {}
         }
 
         self.language_server_ids
             .entry(key.clone())
             .or_insert_with(|| {
+                let languages = self.languages.clone();
                 let server_id = post_inc(&mut self.next_language_server_id);
                 let language_server = self.languages.start_language_server(
                     server_id,
@@ -1942,6 +1963,8 @@ impl Project {
                 self.language_servers.insert(
                     server_id,
                     LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
+                        let workspace_config =
+                            cx.update(|cx| languages.workspace_configuration(cx)).await;
                         let language_server = language_server?.await.log_err()?;
                         let language_server = language_server
                             .initialize(initialization_options)
@@ -1977,23 +2000,24 @@ impl Project {
 
                         language_server
                             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
-                                let settings = this.read_with(&cx, |this, _| {
-                                    this.language_server_settings.clone()
-                                });
-                                move |params, _| {
-                                    let settings = settings.lock().clone();
+                                let languages = languages.clone();
+                                move |params, mut cx| {
+                                    let languages = languages.clone();
                                     async move {
+                                        let workspace_config = cx
+                                            .update(|cx| languages.workspace_configuration(cx))
+                                            .await;
                                         Ok(params
                                             .items
                                             .into_iter()
                                             .map(|item| {
                                                 if let Some(section) = &item.section {
-                                                    settings
+                                                    workspace_config
                                                         .get(section)
                                                         .cloned()
                                                         .unwrap_or(serde_json::Value::Null)
                                                 } else {
-                                                    settings.clone()
+                                                    workspace_config.clone()
                                                 }
                                             })
                                             .collect())
@@ -2027,8 +2051,26 @@ impl Project {
                             })
                             .detach();
                         language_server
-                            .on_request::<lsp::request::RegisterCapability, _, _>(|_, _| async {
-                                Ok(())
+                            .on_request::<lsp::request::RegisterCapability, _, _>({
+                                let this = this.downgrade();
+                                move |params, mut cx| async move {
+                                    let this = this
+                                        .upgrade(&cx)
+                                        .ok_or_else(|| anyhow!("project dropped"))?;
+                                    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();
 
@@ -2071,6 +2113,14 @@ impl Project {
                             })
                             .detach();
 
+                        language_server
+                            .notify::<lsp::notification::DidChangeConfiguration>(
+                                lsp::DidChangeConfigurationParams {
+                                    settings: workspace_config,
+                                },
+                            )
+                            .ok();
+
                         this.update(&mut cx, |this, cx| {
                             // 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
@@ -2090,6 +2140,7 @@ impl Project {
                                 LanguageServerState::Running {
                                     adapter: adapter.clone(),
                                     language,
+                                    watched_paths: Default::default(),
                                     server: language_server.clone(),
                                     simulate_disk_based_diagnostics_completion: None,
                                 },
@@ -2103,13 +2154,6 @@ impl Project {
                                     progress_tokens: Default::default(),
                                 },
                             );
-                            language_server
-                                .notify::<lsp::notification::DidChangeConfiguration>(
-                                    lsp::DidChangeConfigurationParams {
-                                        settings: this.language_server_settings.lock().clone(),
-                                    },
-                                )
-                                .ok();
 
                             if let Some(project_id) = this.remote_id() {
                                 this.client
@@ -2267,8 +2311,14 @@ impl Project {
             })
             .collect();
         for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
-            let language = self.languages.language_for_path(&full_path)?;
-            self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
+            if let Some(language) = self
+                .languages
+                .language_for_path(&full_path)
+                .now_or_never()
+                .and_then(|language| language.ok())
+            {
+                self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
+            }
         }
 
         None
@@ -2483,6 +2533,23 @@ impl Project {
         }
     }
 
+    fn on_lsp_did_change_watched_files(
+        &mut self,
+        language_server_id: usize,
+        params: DidChangeWatchedFilesRegistrationOptions,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(LanguageServerState::Running { watched_paths, .. }) =
+            self.language_servers.get_mut(&language_server_id)
+        {
+            watched_paths.clear();
+            for watcher in params.watchers {
+                watched_paths.add_pattern(&watcher.glob_pattern).log_err();
+            }
+            cx.notify();
+        }
+    }
+
     async fn on_lsp_workspace_edit(
         this: WeakModelHandle<Self>,
         params: lsp::ApplyWorkspaceEditParams,
@@ -2533,21 +2600,6 @@ impl Project {
         }
     }
 
-    pub fn set_language_server_settings(&mut self, settings: serde_json::Value) {
-        for server_state in self.language_servers.values() {
-            if let LanguageServerState::Running { server, .. } = server_state {
-                server
-                    .notify::<lsp::notification::DidChangeConfiguration>(
-                        lsp::DidChangeConfigurationParams {
-                            settings: settings.clone(),
-                        },
-                    )
-                    .ok();
-            }
-        }
-        *self.language_server_settings.lock() = settings;
-    }
-
     pub fn language_server_statuses(
         &self,
     ) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
@@ -2557,7 +2609,7 @@ impl Project {
     pub fn update_diagnostics(
         &mut self,
         language_server_id: usize,
-        params: lsp::PublishDiagnosticsParams,
+        mut params: lsp::PublishDiagnosticsParams,
         disk_based_sources: &[String],
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
@@ -2569,6 +2621,10 @@ impl Project {
         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 {
@@ -2858,9 +2914,11 @@ impl Project {
                 .filter_map(|buffer_handle| {
                     let buffer = buffer_handle.read(cx);
                     let file = File::from_dyn(buffer.file())?;
-                    let buffer_abs_path = file.as_local()?.abs_path(cx);
-                    let (_, server) = self.language_server_for_buffer(buffer, cx)?;
-                    Some((buffer_handle, buffer_abs_path, server.clone()))
+                    let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
+                    let server = self
+                        .language_server_for_buffer(buffer, cx)
+                        .map(|s| s.1.clone());
+                    Some((buffer_handle, buffer_abs_path, server))
                 })
                 .collect::<Vec<_>>();
 
@@ -2875,10 +2933,10 @@ impl Project {
                 let _cleanup = defer({
                     let this = this.clone();
                     let mut cx = cx.clone();
-                    let local_buffers = &buffers_with_paths_and_servers;
+                    let buffers = &buffers_with_paths_and_servers;
                     move || {
                         this.update(&mut cx, |this, _| {
-                            for (buffer, _, _) in local_buffers {
+                            for (buffer, _, _) in buffers {
                                 this.buffers_being_formatted.remove(&buffer.id());
                             }
                         });
@@ -2887,60 +2945,138 @@ impl Project {
 
                 let mut project_transaction = ProjectTransaction::default();
                 for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
-                    let (format_on_save, formatter, tab_size) =
-                        buffer.read_with(&cx, |buffer, cx| {
-                            let settings = cx.global::<Settings>();
-                            let language_name = buffer.language().map(|language| language.name());
-                            (
-                                settings.format_on_save(language_name.as_deref()),
-                                settings.formatter(language_name.as_deref()),
-                                settings.tab_size(language_name.as_deref()),
-                            )
-                        });
+                    let (
+                        format_on_save,
+                        remove_trailing_whitespace,
+                        ensure_final_newline,
+                        formatter,
+                        tab_size,
+                    ) = buffer.read_with(&cx, |buffer, cx| {
+                        let settings = cx.global::<Settings>();
+                        let language_name = buffer.language().map(|language| language.name());
+                        (
+                            settings.format_on_save(language_name.as_deref()),
+                            settings.remove_trailing_whitespace_on_save(language_name.as_deref()),
+                            settings.ensure_final_newline_on_save(language_name.as_deref()),
+                            settings.formatter(language_name.as_deref()),
+                            settings.tab_size(language_name.as_deref()),
+                        )
+                    });
+
+                    // First, format buffer's whitespace according to the settings.
+                    let trailing_whitespace_diff = if remove_trailing_whitespace {
+                        Some(
+                            buffer
+                                .read_with(&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),
+                    }
 
-                    let transaction = match (formatter, format_on_save) {
-                        (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue,
+                    // 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) => Self::format_via_lsp(
-                            &this,
-                            &buffer,
-                            &buffer_abs_path,
-                            &language_server,
-                            tab_size,
-                            &mut cx,
-                        )
-                        .await
-                        .context("failed to format via language server")?,
+                        | (_, 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 }) => {
-                            Self::format_via_external_command(
-                                &buffer,
-                                &buffer_abs_path,
-                                &command,
-                                &arguments,
-                                &mut cx,
-                            )
-                            .await
-                            .context(format!(
-                                "failed to format via external command {:?}",
-                                command
-                            ))?
+                            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);
+                            }
                         }
                     };
 
-                    if let Some(transaction) = transaction {
-                        if !push_to_history {
-                            buffer.update(&mut cx, |buffer, _| {
-                                buffer.forget_transaction(transaction.id)
-                            });
+                    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();
+                            }
                         }
-                        project_transaction.0.insert(buffer.clone(), transaction);
-                    }
+
+                        // 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);
+                                }
+                            }
+
+                            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)
@@ -2981,7 +3117,7 @@ impl Project {
         language_server: &Arc<LanguageServer>,
         tab_size: NonZeroU32,
         cx: &mut AsyncAppContext,
-    ) -> Result<Option<Transaction>> {
+    ) -> Result<Vec<(Range<Anchor>, String)>> {
         let text_document =
             lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap());
         let capabilities = &language_server.capabilities();
@@ -3028,26 +3164,12 @@ impl Project {
         };
 
         if let Some(lsp_edits) = lsp_edits {
-            let edits = this
-                .update(cx, |this, cx| {
-                    this.edits_from_lsp(buffer, lsp_edits, None, cx)
-                })
-                .await?;
-            buffer.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();
-                    Ok(Some(transaction))
-                } else {
-                    Ok(None)
-                }
+            this.update(cx, |this, cx| {
+                this.edits_from_lsp(buffer, lsp_edits, None, cx)
             })
+            .await
         } else {
-            Ok(None)
+            Ok(Default::default())
         }
     }
 
@@ -3057,7 +3179,7 @@ impl Project {
         command: &str,
         arguments: &[String],
         cx: &mut AsyncAppContext,
-    ) -> Result<Option<Transaction>> {
+    ) -> Result<Option<Diff>> {
         let working_dir_path = buffer.read_with(cx, |buffer, cx| {
             let file = File::from_dyn(buffer.file())?;
             let worktree = file.worktree.read(cx).as_local()?;
@@ -3100,10 +3222,11 @@ impl Project {
             }
 
             let stdout = String::from_utf8(output.stdout)?;
-            let diff = buffer
-                .read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
-                .await;
-            Ok(buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx).cloned()))
+            Ok(Some(
+                buffer
+                    .read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
+                    .await,
+            ))
         } else {
             Ok(None)
         }
@@ -3226,12 +3349,14 @@ impl Project {
                                 path: path.into(),
                             };
                             let signature = this.symbol_signature(&project_path);
+                            let adapter_language = adapter_language.clone();
                             let language = this
                                 .languages
                                 .language_for_path(&project_path.path)
-                                .unwrap_or(adapter_language.clone());
+                                .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(&lsp_symbol.name, lsp_symbol.kind)
                                     .await;
@@ -3650,7 +3775,7 @@ impl Project {
             worktree = file.worktree.clone();
             buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
         } else {
-            return Task::ready(Ok(Default::default()));
+            return Task::ready(Ok(Vec::new()));
         };
         let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
 
@@ -3660,13 +3785,13 @@ impl Project {
             {
                 server.clone()
             } else {
-                return Task::ready(Ok(Default::default()));
+                return Task::ready(Ok(Vec::new()));
             };
 
             let lsp_range = range_to_lsp(range.to_point_utf16(buffer));
             cx.foreground().spawn(async move {
                 if lang_server.capabilities().code_action_provider.is_none() {
-                    return Ok(Default::default());
+                    return Ok(Vec::new());
                 }
 
                 Ok(lang_server
@@ -3679,13 +3804,7 @@ impl Project {
                         partial_result_params: Default::default(),
                         context: lsp::CodeActionContext {
                             diagnostics: relevant_diagnostics,
-                            only: Some(vec![
-                                lsp::CodeActionKind::EMPTY,
-                                lsp::CodeActionKind::QUICKFIX,
-                                lsp::CodeActionKind::REFACTOR,
-                                lsp::CodeActionKind::REFACTOR_EXTRACT,
-                                lsp::CodeActionKind::SOURCE,
-                            ]),
+                            only: lang_server.code_action_kinds(),
                         },
                     })
                     .await?
@@ -4381,7 +4500,10 @@ impl Project {
         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 => this.update_local_worktree_buffers(worktree, cx),
+                worktree::Event::UpdatedEntries(changes) => {
+                    this.update_local_worktree_buffers(&worktree, cx);
+                    this.update_local_worktree_language_servers(&worktree, changes, cx);
+                }
                 worktree::Event::UpdatedGitRepositories(updated_repos) => {
                     this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
                 }
@@ -4412,7 +4534,7 @@ impl Project {
 
     fn update_local_worktree_buffers(
         &mut self,
-        worktree_handle: ModelHandle<Worktree>,
+        worktree_handle: &ModelHandle<Worktree>,
         cx: &mut ModelContext<Self>,
     ) {
         let snapshot = worktree_handle.read(cx).snapshot();
@@ -4422,7 +4544,7 @@ impl Project {
             if let Some(buffer) = buffer.upgrade(cx) {
                 buffer.update(cx, |buffer, cx| {
                     if let Some(old_file) = File::from_dyn(buffer.file()) {
-                        if old_file.worktree != worktree_handle {
+                        if old_file.worktree != *worktree_handle {
                             return;
                         }
 
@@ -4489,11 +4611,63 @@ impl Project {
 
         for (buffer, old_path) in renamed_buffers {
             self.unregister_buffer_from_language_server(&buffer, old_path, cx);
-            self.assign_language_to_buffer(&buffer, cx);
+            self.detect_language_for_buffer(&buffer, cx);
             self.register_buffer_with_language_server(&buffer, cx);
         }
     }
 
+    fn update_local_worktree_language_servers(
+        &mut self,
+        worktree_handle: &ModelHandle<Worktree>,
+        changes: &HashMap<Arc<Path>, PathChange>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let worktree_id = worktree_handle.read(cx).id();
+        let abs_path = worktree_handle.read(cx).abs_path();
+        for ((server_worktree_id, _), server_id) in &self.language_server_ids {
+            if *server_worktree_id == worktree_id {
+                if let Some(server) = self.language_servers.get(server_id) {
+                    if let LanguageServerState::Running {
+                        server,
+                        watched_paths,
+                        ..
+                    } = server
+                    {
+                        let params = lsp::DidChangeWatchedFilesParams {
+                            changes: changes
+                                .iter()
+                                .filter_map(|(path, change)| {
+                                    let path = abs_path.join(path);
+                                    if watched_paths.matches(&path) {
+                                        Some(lsp::FileEvent {
+                                            uri: lsp::Url::from_file_path(path).unwrap(),
+                                            typ: match change {
+                                                PathChange::Added => lsp::FileChangeType::CREATED,
+                                                PathChange::Removed => lsp::FileChangeType::DELETED,
+                                                PathChange::Updated
+                                                | PathChange::AddedOrUpdated => {
+                                                    lsp::FileChangeType::CHANGED
+                                                }
+                                            },
+                                        })
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect(),
+                        };
+
+                        if !params.changes.is_empty() {
+                            server
+                                .notify::<lsp::notification::DidChangeWatchedFiles>(params)
+                                .log_err();
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     fn update_local_worktree_buffers_git_repos(
         &mut self,
         worktree: ModelHandle<Worktree>,
@@ -5158,7 +5332,7 @@ impl Project {
                 buffer.update(cx, |buffer, cx| {
                     buffer.file_updated(Arc::new(file), cx).detach();
                 });
-                this.assign_language_to_buffer(&buffer, cx);
+                this.detect_language_for_buffer(&buffer, cx);
             }
             Ok(())
         })
@@ -5779,7 +5953,7 @@ impl Project {
                                 })?;
                             }
 
-                            Ok(())
+                            anyhow::Ok(())
                         }
                         .log_err(),
                     )
@@ -6008,7 +6182,7 @@ impl Project {
                 worktree_id,
                 path: PathBuf::from(serialized_symbol.path).into(),
             };
-            let language = languages.language_for_path(&path.path);
+            let language = languages.language_for_path(&path.path).await.log_err();
             Ok(Symbol {
                 language_server_name: LanguageServerName(
                     serialized_symbol.language_server_name.into(),

crates/project/src/project_tests.rs 🔗

@@ -8,11 +8,21 @@ use language::{
     OffsetRangeExt, Point, ToPoint,
 };
 use lsp::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) {
     let dir = temp_tree(json!({
@@ -437,6 +447,111 @@ async fn test_managing_language_servers(
     );
 }
 
+#[gpui::test]
+async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
+    cx.foreground().forbid_parking();
+
+    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!({
+            "a.rs": "",
+            "b.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/a.rs", cx)
+        })
+        .await
+        .unwrap();
+
+    // 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::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
+            registrations: vec![lsp::Registration {
+                id: Default::default(),
+                method: "workspace/didChangeWatchedFiles".to_string(),
+                register_options: serde_json::to_value(
+                    lsp::DidChangeWatchedFilesRegistrationOptions {
+                        watchers: vec![lsp::FileSystemWatcher {
+                            glob_pattern: "*.{rs,c}".to_string(),
+                            kind: None,
+                        }],
+                    },
+                )
+                .ok(),
+            }],
+        })
+        .await
+        .unwrap();
+    fake_server.handle_notification::<lsp::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!(file_changes.lock().len(), 0);
+
+    // Perform some file system mutations, two of which match the watched patterns,
+    // and one of which does not.
+    fs.create_file("/the-root/c.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.create_file("/the-root/d.txt".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.remove_file("/the-root/b.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(),
+        &[
+            lsp::FileEvent {
+                uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
+                typ: lsp::FileChangeType::DELETED,
+            },
+            lsp::FileEvent {
+                uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
+                typ: lsp::FileChangeType::CREATED,
+            },
+        ]
+    );
+}
+
 #[gpui::test]
 async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     cx.foreground().forbid_parking();
@@ -1584,7 +1699,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
             buffer.text(),
             "
                 use a::{b, c};
-                
+
                 fn f() {
                     b();
                     c();
@@ -1602,7 +1717,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
     let text = "
         use a::b;
         use a::c;
-        
+
         fn f() {
             b();
             c();
@@ -1687,7 +1802,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
             buffer.text(),
             "
                 use a::{b, c};
-                
+
                 fn f() {
                     b();
                     c();
@@ -2846,7 +2961,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::WARNING,
                     message: "error 1".to_string(),
-                    group_id: 0,
+                    group_id: 1,
                     is_primary: true,
                     ..Default::default()
                 }
@@ -2856,7 +2971,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::HINT,
                     message: "error 1 hint 1".to_string(),
-                    group_id: 0,
+                    group_id: 1,
                     is_primary: false,
                     ..Default::default()
                 }
@@ -2866,7 +2981,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::HINT,
                     message: "error 2 hint 1".to_string(),
-                    group_id: 1,
+                    group_id: 0,
                     is_primary: false,
                     ..Default::default()
                 }
@@ -2876,7 +2991,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::HINT,
                     message: "error 2 hint 2".to_string(),
-                    group_id: 1,
+                    group_id: 0,
                     is_primary: false,
                     ..Default::default()
                 }
@@ -2886,7 +3001,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::ERROR,
                     message: "error 2".to_string(),
-                    group_id: 1,
+                    group_id: 0,
                     is_primary: true,
                     ..Default::default()
                 }
@@ -2898,60 +3013,61 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
         buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
         &[
             DiagnosticEntry {
-                range: Point::new(1, 8)..Point::new(1, 9),
+                range: Point::new(1, 13)..Point::new(1, 15),
                 diagnostic: Diagnostic {
-                    severity: DiagnosticSeverity::WARNING,
-                    message: "error 1".to_string(),
+                    severity: DiagnosticSeverity::HINT,
+                    message: "error 2 hint 1".to_string(),
                     group_id: 0,
-                    is_primary: true,
+                    is_primary: false,
                     ..Default::default()
                 }
             },
             DiagnosticEntry {
-                range: Point::new(1, 8)..Point::new(1, 9),
+                range: Point::new(1, 13)..Point::new(1, 15),
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::HINT,
-                    message: "error 1 hint 1".to_string(),
+                    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, 13)..Point::new(1, 15),
+                range: Point::new(1, 8)..Point::new(1, 9),
                 diagnostic: Diagnostic {
-                    severity: DiagnosticSeverity::HINT,
-                    message: "error 2 hint 1".to_string(),
+                    severity: DiagnosticSeverity::WARNING,
+                    message: "error 1".to_string(),
                     group_id: 1,
-                    is_primary: false,
+                    is_primary: true,
                     ..Default::default()
                 }
             },
             DiagnosticEntry {
-                range: Point::new(1, 13)..Point::new(1, 15),
+                range: Point::new(1, 8)..Point::new(1, 9),
                 diagnostic: Diagnostic {
                     severity: DiagnosticSeverity::HINT,
-                    message: "error 2 hint 2".to_string(),
+                    message: "error 1 hint 1".to_string(),
                     group_id: 1,
                     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: 1,
-                    is_primary: true,
-                    ..Default::default()
-                }
-            }
         ]
     );
 }

crates/project/src/terminals.rs 🔗

@@ -0,0 +1,68 @@
+use std::path::PathBuf;
+
+use gpui::{ModelContext, ModelHandle, WeakModelHandle};
+use settings::Settings;
+use terminal::{Terminal, TerminalBuilder};
+
+use crate::Project;
+
+pub struct Terminals {
+    pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
+}
+
+impl Project {
+    pub fn create_terminal(
+        &mut self,
+        working_directory: Option<PathBuf>,
+        window_id: usize,
+        cx: &mut ModelContext<Self>,
+    ) -> anyhow::Result<ModelHandle<Terminal>> {
+        if self.is_remote() {
+            return Err(anyhow::anyhow!(
+                "creating terminals as a guest is not supported yet"
+            ));
+        } else {
+            let settings = cx.global::<Settings>();
+            let shell = settings.terminal_shell();
+            let envs = settings.terminal_env();
+            let scroll = settings.terminal_scroll();
+
+            let terminal = TerminalBuilder::new(
+                working_directory.clone(),
+                shell,
+                envs,
+                settings.terminal_overrides.blinking.clone(),
+                scroll,
+                window_id,
+            )
+            .map(|builder| {
+                let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));
+
+                self.terminals
+                    .local_handles
+                    .push(terminal_handle.downgrade());
+
+                let id = terminal_handle.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.id() == id) {
+                        handles.remove(index);
+                        cx.notify();
+                    }
+                })
+                .detach();
+
+                terminal_handle
+            });
+
+            terminal
+        }
+    }
+
+    pub fn local_terminal_handles(&self) -> &Vec<WeakModelHandle<terminal::Terminal>> {
+        &self.terminals.local_handles
+    }
+}
+
+// TODO: Add a few tests for adding and removing terminal tabs

crates/project/src/worktree.rs 🔗

@@ -1,18 +1,18 @@
-use super::{ignore::IgnoreStack, DiagnosticSummary};
-use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
+use crate::{
+    copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
+};
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context, Result};
 use client::{proto, Client};
 use clock::ReplicaId;
 use collections::{HashMap, VecDeque};
-use fs::LineEnding;
-use fs::{repository::GitRepository, Fs};
+use fs::{repository::GitRepository, Fs, LineEnding};
 use futures::{
     channel::{
         mpsc::{self, UnboundedSender},
         oneshot,
     },
-    Stream, StreamExt,
+    select_biased, Stream, StreamExt,
 };
 use fuzzy::CharBag;
 use git::{DOT_GIT, GITIGNORE};
@@ -20,20 +20,19 @@ use gpui::{
     executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
     Task,
 };
-use language::File as _;
 use language::{
     proto::{
         deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
         serialize_version,
     },
-    Buffer, DiagnosticEntry, PointUtf16, Rope, RopeFingerprint, Unclipped,
+    Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
 };
 use parking_lot::Mutex;
 use postage::{
+    barrier,
     prelude::{Sink as _, Stream as _},
     watch,
 };
-
 use smol::channel::{self, Sender};
 use std::{
     any::Any,
@@ -45,18 +44,19 @@ use std::{
     mem,
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
-    sync::{atomic::AtomicUsize, Arc},
+    sync::{
+        atomic::{AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
     task::Poll,
     time::{Duration, SystemTime},
 };
 use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
-use util::paths::HOME;
-use util::{ResultExt, TryFutureExt};
+use util::{paths::HOME, ResultExt, TryFutureExt};
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
 pub struct WorktreeId(usize);
 
-#[allow(clippy::large_enum_variant)]
 pub enum Worktree {
     Local(LocalWorktree),
     Remote(RemoteWorktree),
@@ -64,10 +64,9 @@ pub enum Worktree {
 
 pub struct LocalWorktree {
     snapshot: LocalSnapshot,
-    background_snapshot: Arc<Mutex<LocalSnapshot>>,
-    last_scan_state_rx: watch::Receiver<ScanState>,
-    _background_scanner_task: Option<Task<()>>,
-    poll_task: Option<Task<()>>,
+    path_changes_tx: channel::Sender<(Vec<PathBuf>, barrier::Sender)>,
+    is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
+    _background_scanner_task: Task<()>,
     share: Option<ShareState>,
     diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
@@ -77,8 +76,8 @@ pub struct LocalWorktree {
 }
 
 pub struct RemoteWorktree {
-    pub snapshot: Snapshot,
-    pub(crate) background_snapshot: Arc<Mutex<Snapshot>>,
+    snapshot: Snapshot,
+    background_snapshot: Arc<Mutex<Snapshot>>,
     project_id: u64,
     client: Arc<Client>,
     updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
@@ -118,11 +117,11 @@ impl std::fmt::Debug for GitRepositoryEntry {
         f.debug_struct("GitRepositoryEntry")
             .field("content_path", &self.content_path)
             .field("git_dir_path", &self.git_dir_path)
-            .field("libgit_repository", &"LibGitRepository")
             .finish()
     }
 }
 
+#[derive(Debug)]
 pub struct LocalSnapshot {
     ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
     git_repositories: Vec<GitRepositoryEntry>,
@@ -157,14 +156,22 @@ impl DerefMut for LocalSnapshot {
     }
 }
 
-#[derive(Clone, Debug)]
 enum ScanState {
-    Idle,
     /// The worktree is performing its initial scan of the filesystem.
-    Initializing,
+    Initializing {
+        snapshot: LocalSnapshot,
+        barrier: Option<barrier::Sender>,
+    },
+    Initialized {
+        snapshot: LocalSnapshot,
+    },
     /// The worktree is updating in response to filesystem events.
     Updating,
-    Err(Arc<anyhow::Error>),
+    Updated {
+        snapshot: LocalSnapshot,
+        changes: HashMap<Arc<Path>, PathChange>,
+        barrier: Option<barrier::Sender>,
+    },
 }
 
 struct ShareState {
@@ -175,7 +182,7 @@ struct ShareState {
 }
 
 pub enum Event {
-    UpdatedEntries,
+    UpdatedEntries(HashMap<Arc<Path>, PathChange>),
     UpdatedGitRepositories(Vec<GitRepositoryEntry>),
 }
 
@@ -192,21 +199,87 @@ impl Worktree {
         next_entry_id: Arc<AtomicUsize>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
-        let (tree, scan_states_tx) =
-            LocalWorktree::create(client, path, visible, fs.clone(), next_entry_id, cx).await?;
-        tree.update(cx, |tree, cx| {
-            let tree = tree.as_local_mut().unwrap();
-            let abs_path = tree.abs_path().clone();
-            let background_snapshot = tree.background_snapshot.clone();
-            let background = cx.background().clone();
-            tree._background_scanner_task = Some(cx.background().spawn(async move {
-                let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
-                let scanner =
-                    BackgroundScanner::new(background_snapshot, scan_states_tx, fs, background);
-                scanner.run(events).await;
-            }));
-        });
-        Ok(tree)
+        // 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")?;
+
+        Ok(cx.add_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(),
+                removed_entry_ids: Default::default(),
+                next_entry_id,
+                snapshot: Snapshot {
+                    id: WorktreeId::from_usize(cx.model_id()),
+                    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(),
+                    scan_id: 0,
+                    completed_scan_id: 0,
+                },
+            };
+
+            if let Some(metadata) = metadata {
+                snapshot.insert_entry(
+                    Entry::new(
+                        Arc::from(Path::new("")),
+                        &metadata,
+                        &snapshot.next_entry_id,
+                        snapshot.root_char_bag,
+                    ),
+                    fs.as_ref(),
+                );
+            }
+
+            let (path_changes_tx, path_changes_rx) = channel::unbounded();
+            let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
+
+            cx.spawn_weak(|this, mut cx| async move {
+                while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
+                    this.update(&mut cx, |this, cx| {
+                        this.as_local_mut()
+                            .unwrap()
+                            .background_scanner_updated(state, cx);
+                    });
+                }
+            })
+            .detach();
+
+            let background_scanner_task = cx.background().spawn({
+                let fs = fs.clone();
+                let snapshot = snapshot.clone();
+                let background = cx.background().clone();
+                async move {
+                    let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
+                    BackgroundScanner::new(snapshot, scan_states_tx, fs, background)
+                        .run(events, path_changes_rx)
+                        .await;
+                }
+            });
+
+            Worktree::Local(LocalWorktree {
+                snapshot,
+                is_scanning: watch::channel_with(true),
+                share: None,
+                path_changes_tx,
+                _background_scanner_task: background_scanner_task,
+                diagnostics: Default::default(),
+                diagnostic_summaries: Default::default(),
+                client,
+                fs,
+                visible,
+            })
+        }))
     }
 
     pub fn remote(
@@ -216,64 +289,50 @@ impl Worktree {
         client: Arc<Client>,
         cx: &mut MutableAppContext,
     ) -> ModelHandle<Self> {
-        let remote_id = worktree.id;
-        let root_char_bag: CharBag = worktree
-            .root_name
-            .chars()
-            .map(|c| c.to_ascii_lowercase())
-            .collect();
-        let root_name = worktree.root_name.clone();
-        let visible = worktree.visible;
-
-        let abs_path = PathBuf::from(worktree.abs_path);
-        let snapshot = Snapshot {
-            id: WorktreeId(remote_id as usize),
-            abs_path: Arc::from(abs_path.deref()),
-            root_name,
-            root_char_bag,
-            entries_by_path: Default::default(),
-            entries_by_id: Default::default(),
-            scan_id: 0,
-            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();
-        let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
-            Worktree::Remote(RemoteWorktree {
-                project_id: project_remote_id,
-                replica_id,
-                snapshot: snapshot.clone(),
-                background_snapshot: background_snapshot.clone(),
-                updates_tx: Some(updates_tx),
-                snapshot_subscriptions: Default::default(),
-                client: client.clone(),
-                diagnostic_summaries: Default::default(),
-                visible,
-                disconnected: false,
-            })
-        });
+        cx.add_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(),
+                scan_id: 0,
+                completed_scan_id: 0,
+            };
 
-        cx.background()
-            .spawn(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);
+            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.background()
+                .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();
+                        }
                     }
-                    snapshot_updated_tx.send(()).await.ok();
-                }
-            })
-            .detach();
+                })
+                .detach();
 
-        cx.spawn(|mut cx| {
-            let this = worktree_handle.downgrade();
-            async move {
+            cx.spawn_weak(|this, mut cx| async move {
                 while (snapshot_updated_rx.recv().await).is_some() {
                     if let Some(this) = this.upgrade(&cx) {
                         this.update(&mut cx, |this, cx| {
-                            this.poll_snapshot(cx);
                             let this = this.as_remote_mut().unwrap();
+                            this.snapshot = this.background_snapshot.lock().clone();
+                            cx.emit(Event::UpdatedEntries(Default::default()));
+                            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();
@@ -287,11 +346,22 @@ impl Worktree {
                         break;
                     }
                 }
-            }
-        })
-        .detach();
+            })
+            .detach();
 
-        worktree_handle
+            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> {
@@ -380,13 +450,6 @@ impl Worktree {
         .map(|(path, summary)| (path.0.clone(), *summary))
     }
 
-    fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
-        match self {
-            Self::Local(worktree) => worktree.poll_snapshot(false, cx),
-            Self::Remote(worktree) => worktree.poll_snapshot(cx),
-        };
-    }
-
     pub fn abs_path(&self) -> Arc<Path> {
         match self {
             Worktree::Local(worktree) => worktree.abs_path.clone(),
@@ -396,90 +459,6 @@ impl Worktree {
 }
 
 impl LocalWorktree {
-    async fn create(
-        client: Arc<Client>,
-        path: impl Into<Arc<Path>>,
-        visible: bool,
-        fs: Arc<dyn Fs>,
-        next_entry_id: Arc<AtomicUsize>,
-        cx: &mut AsyncAppContext,
-    ) -> Result<(ModelHandle<Worktree>, UnboundedSender<ScanState>)> {
-        let abs_path = path.into();
-        let path: Arc<Path> = Arc::from(Path::new(""));
-
-        // 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 root_name = abs_path
-            .file_name()
-            .map_or(String::new(), |f| f.to_string_lossy().to_string());
-        let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
-        let metadata = fs
-            .metadata(&abs_path)
-            .await
-            .context("failed to stat worktree path")?;
-
-        let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
-        let (mut last_scan_state_tx, last_scan_state_rx) =
-            watch::channel_with(ScanState::Initializing);
-        let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
-            let mut snapshot = LocalSnapshot {
-                ignores_by_parent_abs_path: Default::default(),
-                git_repositories: Default::default(),
-                removed_entry_ids: Default::default(),
-                next_entry_id,
-                snapshot: Snapshot {
-                    id: WorktreeId::from_usize(cx.model_id()),
-                    abs_path,
-                    root_name: root_name.clone(),
-                    root_char_bag,
-                    entries_by_path: Default::default(),
-                    entries_by_id: Default::default(),
-                    scan_id: 0,
-                    completed_scan_id: 0,
-                },
-            };
-            if let Some(metadata) = metadata {
-                let entry = Entry::new(
-                    path,
-                    &metadata,
-                    &snapshot.next_entry_id,
-                    snapshot.root_char_bag,
-                );
-                snapshot.insert_entry(entry, fs.as_ref());
-            }
-
-            let tree = Self {
-                snapshot: snapshot.clone(),
-                background_snapshot: Arc::new(Mutex::new(snapshot)),
-                last_scan_state_rx,
-                _background_scanner_task: None,
-                share: None,
-                poll_task: None,
-                diagnostics: Default::default(),
-                diagnostic_summaries: Default::default(),
-                client,
-                fs,
-                visible,
-            };
-
-            cx.spawn_weak(|this, mut cx| async move {
-                while let Some(scan_state) = scan_states_rx.next().await {
-                    if let Some(this) = this.upgrade(&cx) {
-                        last_scan_state_tx.blocking_send(scan_state).ok();
-                        this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
-                    } else {
-                        break;
-                    }
-                }
-            })
-            .detach();
-
-            Worktree::Local(tree)
-        });
-
-        Ok((tree, scan_states_tx))
-    }
-
     pub fn contains_abs_path(&self, path: &Path) -> bool {
         path.starts_with(&self.abs_path)
     }
@@ -557,68 +536,54 @@ impl LocalWorktree {
         Ok(updated)
     }
 
-    fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
-        self.poll_task.take();
-
-        match self.scan_state() {
-            ScanState::Idle => {
-                let new_snapshot = self.background_snapshot.lock().clone();
-                let updated_repos = Self::changed_repos(
-                    &self.snapshot.git_repositories,
-                    &new_snapshot.git_repositories,
-                );
-                self.snapshot = new_snapshot;
-
-                if let Some(share) = self.share.as_mut() {
-                    *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
-                }
-
-                cx.emit(Event::UpdatedEntries);
-
-                if !updated_repos.is_empty() {
-                    cx.emit(Event::UpdatedGitRepositories(updated_repos));
-                }
+    fn background_scanner_updated(
+        &mut self,
+        scan_state: ScanState,
+        cx: &mut ModelContext<Worktree>,
+    ) {
+        match scan_state {
+            ScanState::Initializing { snapshot, barrier } => {
+                *self.is_scanning.0.borrow_mut() = true;
+                self.set_snapshot(snapshot, cx);
+                drop(barrier);
             }
-
-            ScanState::Initializing => {
-                let is_fake_fs = self.fs.is_fake();
-
-                let new_snapshot = self.background_snapshot.lock().clone();
-                let updated_repos = Self::changed_repos(
-                    &self.snapshot.git_repositories,
-                    &new_snapshot.git_repositories,
-                );
-                self.snapshot = new_snapshot;
-
-                self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
-                    if is_fake_fs {
-                        #[cfg(any(test, feature = "test-support"))]
-                        cx.background().simulate_random_delay().await;
-                    } else {
-                        smol::Timer::after(Duration::from_millis(100)).await;
-                    }
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
-                    }
-                }));
-
-                cx.emit(Event::UpdatedEntries);
-
-                if !updated_repos.is_empty() {
-                    cx.emit(Event::UpdatedGitRepositories(updated_repos));
-                }
+            ScanState::Initialized { snapshot } => {
+                *self.is_scanning.0.borrow_mut() = false;
+                self.set_snapshot(snapshot, cx);
             }
-
-            _ => {
-                if force {
-                    self.snapshot = self.background_snapshot.lock().clone();
-                }
+            ScanState::Updating => {
+                *self.is_scanning.0.borrow_mut() = true;
+            }
+            ScanState::Updated {
+                snapshot,
+                changes,
+                barrier,
+            } => {
+                *self.is_scanning.0.borrow_mut() = false;
+                cx.emit(Event::UpdatedEntries(changes));
+                self.set_snapshot(snapshot, cx);
+                drop(barrier);
             }
         }
-
         cx.notify();
     }
 
+    fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
+        let updated_repos = Self::changed_repos(
+            &self.snapshot.git_repositories,
+            &new_snapshot.git_repositories,
+        );
+        self.snapshot = new_snapshot;
+
+        if let Some(share) = self.share.as_mut() {
+            *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
+        }
+
+        if !updated_repos.is_empty() {
+            cx.emit(Event::UpdatedGitRepositories(updated_repos));
+        }
+    }
+
     fn changed_repos(
         old_repos: &[GitRepositoryEntry],
         new_repos: &[GitRepositoryEntry],
@@ -648,19 +613,19 @@ impl LocalWorktree {
     }
 
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
-        let mut scan_state_rx = self.last_scan_state_rx.clone();
+        let mut is_scanning_rx = self.is_scanning.1.clone();
         async move {
-            let mut scan_state = Some(scan_state_rx.borrow().clone());
-            while let Some(ScanState::Initializing | ScanState::Updating) = scan_state {
-                scan_state = scan_state_rx.recv().await;
+            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;
+                }
             }
         }
     }
 
-    fn scan_state(&self) -> ScanState {
-        self.last_scan_state_rx.borrow().clone()
-    }
-
     pub fn snapshot(&self) -> LocalSnapshot {
         self.snapshot.clone()
     }
@@ -704,9 +669,7 @@ impl LocalWorktree {
             // Eagerly populate the snapshot with an updated entry for the loaded file
             let entry = this
                 .update(&mut cx, |this, cx| {
-                    this.as_local()
-                        .unwrap()
-                        .refresh_entry(path, abs_path, None, cx)
+                    this.as_local().unwrap().refresh_entry(path, None, cx)
                 })
                 .await?;
 
@@ -797,15 +760,25 @@ impl LocalWorktree {
         is_dir: bool,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
-        self.write_entry_internal(
-            path,
+        let path = path.into();
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let write = cx.background().spawn(async move {
             if is_dir {
-                None
+                fs.create_dir(&abs_path).await
             } else {
-                Some(Default::default())
-            },
-            cx,
-        )
+                fs.save(&abs_path, &Default::default(), Default::default())
+                    .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 write_file(
@@ -815,7 +788,20 @@ impl LocalWorktree {
         line_ending: LineEnding,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
-        self.write_entry_internal(path, Some((text, line_ending)), cx)
+        let path = path.into();
+        let abs_path = self.absolutize(&path);
+        let fs = self.fs.clone();
+        let write = cx
+            .background()
+            .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(
@@ -824,36 +810,40 @@ impl LocalWorktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Option<Task<Result<()>>> {
         let entry = self.entry_for_id(entry_id)?.clone();
-        let abs_path = self.absolutize(&entry.path);
-        let delete = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_path = abs_path;
-            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
-                }
+        let abs_path = self.abs_path.clone();
+        let fs = self.fs.clone();
+
+        let delete = cx.background().spawn(async move {
+            let mut abs_path = fs.canonicalize(&abs_path).await?;
+            if entry.path.file_name().is_some() {
+                abs_path = abs_path.join(&entry.path);
             }
+            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(abs_path)
         });
 
         Some(cx.spawn(|this, mut cx| async move {
-            delete.await?;
-            this.update(&mut cx, |this, cx| {
-                let this = this.as_local_mut().unwrap();
-                {
-                    let mut snapshot = this.background_snapshot.lock();
-                    snapshot.delete_entry(entry_id);
-                }
-                this.poll_snapshot(true, cx);
+            let abs_path = delete.await?;
+            let (tx, mut rx) = barrier::channel();
+            this.update(&mut cx, |this, _| {
+                this.as_local_mut()
+                    .unwrap()
+                    .path_changes_tx
+                    .try_send((vec![abs_path], tx))
+                    .unwrap();
             });
+            rx.recv().await;
             Ok(())
         }))
     }
@@ -868,28 +858,20 @@ impl LocalWorktree {
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
-        let rename = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_new_path = abs_new_path.clone();
-            async move {
-                fs.rename(&abs_old_path, &abs_new_path, Default::default())
-                    .await
-            }
+        let fs = self.fs.clone();
+        let rename = cx.background().spawn(async move {
+            fs.rename(&abs_old_path, &abs_new_path, Default::default())
+                .await
         });
 
         Some(cx.spawn(|this, mut cx| async move {
             rename.await?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut().unwrap().refresh_entry(
-                        new_path.clone(),
-                        abs_new_path,
-                        Some(old_path),
-                        cx,
-                    )
-                })
-                .await?;
-            Ok(entry)
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entry(new_path.clone(), Some(old_path), cx)
+            })
+            .await
         }))
     }
 
@@ -903,111 +885,63 @@ impl LocalWorktree {
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
         let abs_new_path = self.absolutize(&new_path);
-        let copy = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_new_path = abs_new_path.clone();
-            async move {
-                copy_recursive(
-                    fs.as_ref(),
-                    &abs_old_path,
-                    &abs_new_path,
-                    Default::default(),
-                )
-                .await
-            }
+        let fs = self.fs.clone();
+        let copy = cx.background().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?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut().unwrap().refresh_entry(
-                        new_path.clone(),
-                        abs_new_path,
-                        None,
-                        cx,
-                    )
-                })
-                .await?;
-            Ok(entry)
+            this.update(&mut cx, |this, cx| {
+                this.as_local_mut()
+                    .unwrap()
+                    .refresh_entry(new_path.clone(), None, cx)
+            })
+            .await
         }))
     }
 
-    fn write_entry_internal(
-        &self,
-        path: impl Into<Arc<Path>>,
-        text_if_file: Option<(Rope, LineEnding)>,
-        cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<Entry>> {
-        let path = path.into();
-        let abs_path = self.absolutize(&path);
-        let write = cx.background().spawn({
-            let fs = self.fs.clone();
-            let abs_path = abs_path.clone();
-            async move {
-                if let Some((text, line_ending)) = text_if_file {
-                    fs.save(&abs_path, &text, line_ending).await
-                } else {
-                    fs.create_dir(&abs_path).await
-                }
-            }
-        });
-
-        cx.spawn(|this, mut cx| async move {
-            write.await?;
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local_mut()
-                        .unwrap()
-                        .refresh_entry(path, abs_path, None, cx)
-                })
-                .await?;
-            Ok(entry)
-        })
-    }
-
     fn refresh_entry(
         &self,
         path: Arc<Path>,
-        abs_path: PathBuf,
         old_path: Option<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
         let fs = self.fs.clone();
-        let root_char_bag;
-        let next_entry_id;
-        {
-            let snapshot = self.background_snapshot.lock();
-            root_char_bag = snapshot.root_char_bag;
-            next_entry_id = snapshot.next_entry_id.clone();
-        }
-        cx.spawn_weak(|this, mut cx| async move {
-            let metadata = fs
-                .metadata(&abs_path)
-                .await?
-                .ok_or_else(|| anyhow!("could not read saved file metadata"))?;
-            let this = this
-                .upgrade(&cx)
-                .ok_or_else(|| anyhow!("worktree was dropped"))?;
-            this.update(&mut cx, |this, cx| {
-                let this = this.as_local_mut().unwrap();
-                let inserted_entry;
-                {
-                    let mut snapshot = this.background_snapshot.lock();
-                    let mut entry = Entry::new(path, &metadata, &next_entry_id, root_char_bag);
-                    entry.is_ignored = snapshot
-                        .ignore_stack_for_abs_path(&abs_path, entry.is_dir())
-                        .is_abs_path_ignored(&abs_path, entry.is_dir());
-                    if let Some(old_path) = old_path {
-                        snapshot.remove_path(&old_path);
-                    }
-                    snapshot.scan_started();
-                    inserted_entry = snapshot.insert_entry(entry, fs.as_ref());
-                    snapshot.scan_completed();
-                }
-                this.poll_snapshot(true, cx);
-                Ok(inserted_entry)
-            })
+        let abs_root_path = self.abs_path.clone();
+        let path_changes_tx = self.path_changes_tx.clone();
+        cx.spawn_weak(move |this, mut cx| async move {
+            let abs_path = fs.canonicalize(&abs_root_path).await?;
+            let mut paths = Vec::with_capacity(2);
+            paths.push(if path.file_name().is_some() {
+                abs_path.join(&path)
+            } else {
+                abs_path.clone()
+            });
+            if let Some(old_path) = old_path {
+                paths.push(if old_path.file_name().is_some() {
+                    abs_path.join(&old_path)
+                } else {
+                    abs_path.clone()
+                });
+            }
+
+            let (tx, mut rx) = barrier::channel();
+            path_changes_tx.try_send((paths, tx)).unwrap();
+            rx.recv().await;
+            this.upgrade(&cx)
+                .ok_or_else(|| anyhow!("worktree was dropped"))?
+                .update(&mut cx, |this, _| {
+                    this.entry_for_path(path)
+                        .cloned()
+                        .ok_or_else(|| anyhow!("failed to read path after update"))
+                })
         })
     }
 
@@ -1109,12 +1043,6 @@ impl RemoteWorktree {
         self.snapshot.clone()
     }
 
-    fn poll_snapshot(&mut self, cx: &mut ModelContext<Worktree>) {
-        self.snapshot = self.background_snapshot.lock().clone();
-        cx.emit(Event::UpdatedEntries);
-        cx.notify();
-    }
-
     pub fn disconnected_from_host(&mut self) {
         self.updates_tx.take();
         self.snapshot_subscriptions.clear();
@@ -1274,28 +1202,25 @@ impl Snapshot {
         Ok(entry)
     }
 
-    fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
-        if let Some(removed_entry) = self.entries_by_id.remove(&entry_id, &()) {
-            self.entries_by_path = {
-                let mut cursor = self.entries_by_path.cursor();
-                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;
-                    }
+    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();
+            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.push_tree(cursor.suffix(&()), &());
-                new_entries_by_path
-            };
+            }
+            new_entries_by_path.push_tree(cursor.suffix(&()), &());
+            new_entries_by_path
+        };
 
-            true
-        } else {
-            false
-        }
+        Some(removed_entry.path)
     }
 
     pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {

crates/project_panel/Cargo.toml 🔗

@@ -19,7 +19,7 @@ settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 futures = "0.3"
 unicase = "2.6"
 
@@ -27,4 +27,4 @@ unicase = "2.6"
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }

crates/project_panel/src/project_panel.rs 🔗

@@ -115,8 +115,8 @@ actions!(
     [
         ExpandSelectedEntry,
         CollapseSelectedEntry,
-        AddDirectory,
-        AddFile,
+        NewDirectory,
+        NewFile,
         Copy,
         CopyPath,
         RevealInFinder,
@@ -140,8 +140,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::select_prev);
     cx.add_action(ProjectPanel::select_next);
     cx.add_action(ProjectPanel::open_entry);
-    cx.add_action(ProjectPanel::add_file);
-    cx.add_action(ProjectPanel::add_directory);
+    cx.add_action(ProjectPanel::new_file);
+    cx.add_action(ProjectPanel::new_directory);
     cx.add_action(ProjectPanel::rename);
     cx.add_async_action(ProjectPanel::delete);
     cx.add_async_action(ProjectPanel::confirm);
@@ -305,8 +305,8 @@ impl ProjectPanel {
                     ));
                 }
             }
-            menu_entries.push(ContextMenuItem::item("New File", AddFile));
-            menu_entries.push(ContextMenuItem::item("New Folder", AddDirectory));
+            menu_entries.push(ContextMenuItem::item("New File", NewFile));
+            menu_entries.push(ContextMenuItem::item("New Folder", NewDirectory));
             menu_entries.push(ContextMenuItem::item("Reveal in Finder", RevealInFinder));
             menu_entries.push(ContextMenuItem::Separator);
             menu_entries.push(ContextMenuItem::item("Copy", Copy));
@@ -531,11 +531,11 @@ impl ProjectPanel {
         });
     }
 
-    fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext<Self>) {
+    fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
         self.add_entry(false, cx)
     }
 
-    fn add_directory(&mut self, _: &AddDirectory, cx: &mut ViewContext<Self>) {
+    fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
         self.add_entry(true, cx)
     }
 
@@ -759,10 +759,12 @@ impl ProjectPanel {
                 if ix > 0 {
                     new_file_name.push(format!(" {}", ix));
                 }
-                new_path.push(new_file_name);
                 if let Some(extension) = extension.as_ref() {
-                    new_path.set_extension(&extension);
+                    new_file_name.push(".");
+                    new_file_name.push(extension);
                 }
+
+                new_path.push(new_file_name);
                 ix += 1;
             }
 
@@ -1107,7 +1109,7 @@ impl ProjectPanel {
                 .boxed(),
             )
             .with_child(if show_editor && editor.is_some() {
-                ChildView::new(editor.unwrap().clone(), cx)
+                ChildView::new(editor.as_ref().unwrap(), cx)
                     .contained()
                     .with_margin_left(style.icon_spacing)
                     .aligned()
@@ -1262,59 +1264,94 @@ impl View for ProjectPanel {
         let padding = std::mem::take(&mut container_style.padding);
         let last_worktree_root_id = self.last_worktree_root_id;
 
-        Stack::new()
-            .with_child(
-                MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
-                    UniformList::new(
-                        self.list.clone(),
-                        self.visible_entries
-                            .iter()
-                            .map(|(_, worktree_entries)| worktree_entries.len())
-                            .sum(),
-                        cx,
-                        move |this, range, items, cx| {
-                            let theme = cx.global::<Settings>().theme.clone();
-                            let mut dragged_entry_destination =
-                                this.dragged_entry_destination.clone();
-                            this.for_each_visible_entry(range, cx, |id, details, cx| {
-                                items.push(Self::render_entry(
-                                    id,
-                                    details,
-                                    &this.filename_editor,
-                                    &mut dragged_entry_destination,
-                                    &theme.project_panel,
-                                    cx,
-                                ));
-                            });
-                            this.dragged_entry_destination = dragged_entry_destination;
-                        },
-                    )
-                    .with_padding_top(padding.top)
-                    .with_padding_bottom(padding.bottom)
-                    .contained()
-                    .with_style(container_style)
-                    .expanded()
-                    .boxed()
-                })
-                .on_down(MouseButton::Right, move |e, cx| {
-                    // When deploying the context menu anywhere below the last project entry,
-                    // act as if the user clicked the root of the last worktree.
-                    if let Some(entry_id) = last_worktree_root_id {
-                        cx.dispatch_action(DeployContextMenu {
-                            entry_id,
-                            position: e.position,
-                        })
-                    }
-                })
-                .boxed(),
-            )
-            .with_child(ChildView::new(&self.context_menu, cx).boxed())
-            .boxed()
+        let has_worktree = self.visible_entries.len() != 0;
+
+        if has_worktree {
+            Stack::new()
+                .with_child(
+                    MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
+                        UniformList::new(
+                            self.list.clone(),
+                            self.visible_entries
+                                .iter()
+                                .map(|(_, worktree_entries)| worktree_entries.len())
+                                .sum(),
+                            cx,
+                            move |this, range, items, cx| {
+                                let theme = cx.global::<Settings>().theme.clone();
+                                let mut dragged_entry_destination =
+                                    this.dragged_entry_destination.clone();
+                                this.for_each_visible_entry(range, cx, |id, details, cx| {
+                                    items.push(Self::render_entry(
+                                        id,
+                                        details,
+                                        &this.filename_editor,
+                                        &mut dragged_entry_destination,
+                                        &theme.project_panel,
+                                        cx,
+                                    ));
+                                });
+                                this.dragged_entry_destination = dragged_entry_destination;
+                            },
+                        )
+                        .with_padding_top(padding.top)
+                        .with_padding_bottom(padding.bottom)
+                        .contained()
+                        .with_style(container_style)
+                        .expanded()
+                        .boxed()
+                    })
+                    .on_down(MouseButton::Right, move |e, cx| {
+                        // When deploying the context menu anywhere below the last project entry,
+                        // act as if the user clicked the root of the last worktree.
+                        if let Some(entry_id) = last_worktree_root_id {
+                            cx.dispatch_action(DeployContextMenu {
+                                entry_id,
+                                position: e.position,
+                            })
+                        }
+                    })
+                    .boxed(),
+                )
+                .with_child(ChildView::new(&self.context_menu, cx).boxed())
+                .boxed()
+        } else {
+            Flex::column()
+                .with_child(
+                    MouseEventHandler::<Self>::new(2, cx, {
+                        let button_style = theme.open_project_button.clone();
+                        let context_menu_item_style =
+                            cx.global::<Settings>().theme.context_menu.item.clone();
+                        move |state, cx| {
+                            let button_style = button_style.style_for(state, false).clone();
+                            let context_menu_item =
+                                context_menu_item_style.style_for(state, true).clone();
+
+                            theme::ui::keystroke_label(
+                                "Open a project",
+                                &button_style,
+                                &context_menu_item.keystroke,
+                                Box::new(workspace::Open),
+                                cx,
+                            )
+                            .boxed()
+                        }
+                    })
+                    .on_click(MouseButton::Left, move |_, cx| {
+                        cx.dispatch_action(workspace::Open)
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .boxed(),
+                )
+                .contained()
+                .with_style(container_style)
+                .boxed()
+        }
     }
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
         let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
+        cx.add_identifier("menu");
         cx
     }
 }
@@ -1404,15 +1441,7 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1504,15 +1533,7 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
 
         select_path(&panel, "root1", cx);
@@ -1533,7 +1554,7 @@ mod tests {
 
         // Add a file with the root folder selected. The filename editor is placed
         // before the first file in the root folder.
-        panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx));
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
         assert!(panel.read_with(cx, |panel, cx| panel.filename_editor.is_focused(cx)));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
@@ -1591,7 +1612,7 @@ mod tests {
         );
 
         select_path(&panel, "root1/b", cx);
-        panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx));
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
@@ -1690,7 +1711,7 @@ mod tests {
             ]
         );
 
-        panel.update(cx, |panel, cx| panel.add_directory(&AddDirectory, cx));
+        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..10, cx),
             &[
@@ -1783,6 +1804,80 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let settings = Settings::test(cx);
+            cx.set_global(settings);
+        });
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                "one.two.txt": "",
+                "one.txt": ""
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
+
+        panel.update(cx, |panel, cx| {
+            panel.select_next(&Default::default(), cx);
+            panel.select_next(&Default::default(), cx);
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+
+        // Regression test - file name is created correctly when
+        // the copied file's name contains multiple dots.
+        panel.update(cx, |panel, cx| {
+            panel.copy(&Default::default(), cx);
+            panel.paste(&Default::default(), cx);
+        });
+        cx.foreground().run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two copy.txt",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+
+        panel.update(cx, |panel, cx| {
+            panel.paste(&Default::default(), cx);
+        });
+        cx.foreground().run_until_parked();
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..50, cx),
+            &[
+                //
+                "v root1",
+                "      one.two copy 1.txt",
+                "      one.two copy.txt",
+                "      one.two.txt  <== selected",
+                "      one.txt",
+            ]
+        );
+    }
+
     fn toggle_expand_dir(
         panel: &ViewHandle<ProjectPanel>,
         path: impl AsRef<Path>,

crates/project_symbols/Cargo.toml 🔗

@@ -20,7 +20,7 @@ workspace = { path = "../workspace" }
 util = { path = "../util" }
 anyhow = "1.0.38"
 ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2"
 
 [dev-dependencies]

crates/project_symbols/src/project_symbols.rs 🔗

@@ -49,7 +49,7 @@ impl View for ProjectSymbolsView {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {

crates/recent_projects/Cargo.toml 🔗

@@ -19,5 +19,6 @@ settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"
-postage = { version = "0.4", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2"
+util = { path = "../util"}

crates/recent_projects/src/recent_projects.rs 🔗

@@ -103,7 +103,7 @@ impl View for RecentProjectsView {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -165,12 +165,13 @@ impl PickerDelegate for RecentProjectsView {
     }
 
     fn confirm(&mut self, cx: &mut ViewContext<Self>) {
-        let selected_match = &self.matches[self.selected_index()];
-        let workspace_location = &self.workspace_locations[selected_match.candidate_id];
-        cx.dispatch_global_action(OpenPaths {
-            paths: workspace_location.paths().as_ref().clone(),
-        });
-        cx.emit(Event::Dismissed);
+        if let Some(selected_match) = &self.matches.get(self.selected_index()) {
+            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
+            cx.dispatch_global_action(OpenPaths {
+                paths: workspace_location.paths().as_ref().clone(),
+            });
+            cx.emit(Event::Dismissed);
+        }
     }
 
     fn dismiss(&mut self, cx: &mut ViewContext<Self>) {

crates/rope/Cargo.toml 🔗

@@ -8,7 +8,7 @@ publish = false
 path = "src/rope.rs"
 
 [dependencies]
-bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "dac565a90e8f9245f48ff46225c915dc50f76920" }
+bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
 smallvec = { version = "1.6", features = ["union"] }
 sum_tree = { path = "../sum_tree" }
 arrayvec = "0.7.1"

crates/rope/src/rope.rs 🔗

@@ -31,7 +31,7 @@ const CHUNK_BASE: usize = 16;
 /// hash being equivalent to hashing all the text contained in the [Rope] at once.
 pub type RopeFingerprint = HashMatrix;
 
-#[derive(Clone, Default, Debug)]
+#[derive(Clone, Default)]
 pub struct Rope {
     chunks: SumTree<Chunk>,
 }
@@ -389,6 +389,22 @@ impl fmt::Display for Rope {
     }
 }
 
+impl fmt::Debug for Rope {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use std::fmt::Write as _;
+
+        write!(f, "\"")?;
+        let mut format_string = String::new();
+        for chunk in self.chunks() {
+            write!(&mut format_string, "{:?}", chunk)?;
+            write!(f, "{}", &format_string[1..format_string.len() - 1])?;
+            format_string.clear();
+        }
+        write!(f, "\"")?;
+        Ok(())
+    }
+}
+
 pub struct Cursor<'a> {
     rope: &'a Rope,
     chunks: sum_tree::Cursor<'a, Chunk, usize>,

crates/rpc/Cargo.toml 🔗

@@ -26,7 +26,8 @@ parking_lot = "0.11.1"
 prost = "0.8"
 rand = "0.8"
 rsa = "0.4"
-serde = { version = "1.0", features = ["derive", "rc"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 smol-timeout = "0.6"
 tracing = { version = "0.1.34", features = ["log"] }
 zstd = "0.11"

crates/rpc/proto/zed.proto 🔗

@@ -16,7 +16,7 @@ message Envelope {
         Error error = 6;
         Ping ping = 7;
         Test test = 8;
-        
+
         CreateRoom create_room = 9;
         CreateRoomResponse create_room_response = 10;
         JoinRoom join_room = 11;
@@ -206,7 +206,8 @@ message Room {
     uint64 id = 1;
     repeated Participant participants = 2;
     repeated PendingParticipant pending_participants = 3;
-    string live_kit_room = 4;
+    repeated Follower followers = 4;
+    string live_kit_room = 5;
 }
 
 message Participant {
@@ -227,6 +228,12 @@ message ParticipantProject {
     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;

crates/rpc/src/peer.rs 🔗

@@ -114,7 +114,7 @@ pub struct ConnectionState {
 
 const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
 const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
-pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
+pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10);
 
 impl Peer {
     pub fn new(epoch: u32) -> Arc<Self> {

crates/rpc/src/proto.rs 🔗

@@ -269,6 +269,7 @@ request_messages!(
     (JoinChannel, JoinChannelResponse),
     (JoinProject, JoinProjectResponse),
     (JoinRoom, JoinRoomResponse),
+    (LeaveRoom, Ack),
     (RejoinRoom, RejoinRoomResponse),
     (IncomingCall, Ack),
     (OpenBufferById, OpenBufferResponse),

crates/rpc/src/rpc.rs 🔗

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

crates/search/Cargo.toml 🔗

@@ -22,14 +22,15 @@ workspace = { path = "../workspace" }
 anyhow = "1.0"
 futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
-postage = { version = "0.4.1", features = ["futures-traits"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
+postage = { workspace = true }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 workspace = { path = "../workspace", features = ["test-support"] }
 unindent = "0.1"

crates/search/src/buffer_search.rs 🔗

@@ -273,7 +273,7 @@ impl BufferSearchBar {
             }
         }
         if let Some(active_editor) = self.active_searchable_item.as_ref() {
-            cx.focus(active_editor);
+            cx.focus(active_editor.as_any());
         }
         cx.emit(Event::UpdateLocation);
         cx.notify();
@@ -319,7 +319,7 @@ impl BufferSearchBar {
     fn render_search_option(
         &self,
         option_supported: bool,
-        icon: &str,
+        icon: &'static str,
         option: SearchOption,
         cx: &mut RenderContext<Self>,
     ) -> Option<ElementBox> {
@@ -337,7 +337,7 @@ impl BufferSearchBar {
                     .search
                     .option_button
                     .style_for(state, is_active);
-                Label::new(icon.to_string(), style.text.clone())
+                Label::new(icon, style.text.clone())
                     .contained()
                     .with_style(style.container)
                     .boxed()
@@ -359,7 +359,7 @@ impl BufferSearchBar {
 
     fn render_nav_button(
         &self,
-        icon: &str,
+        icon: &'static str,
         direction: Direction,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
@@ -385,7 +385,7 @@ impl BufferSearchBar {
                 .search
                 .option_button
                 .style_for(state, false);
-            Label::new(icon.to_string(), style.text.clone())
+            Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
                 .boxed()
@@ -458,7 +458,7 @@ impl BufferSearchBar {
 
     fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
         if let Some(active_editor) = self.active_searchable_item.as_ref() {
-            cx.focus(active_editor);
+            cx.focus(active_editor.as_any());
         }
     }
 

crates/search/src/project_search.rs 🔗

@@ -189,7 +189,7 @@ impl View for ProjectSearchView {
                 "No results"
             };
             MouseEventHandler::<Status>::new(0, cx, |_, _| {
-                Label::new(text.to_string(), theme.search.results_status.clone())
+                Label::new(text, theme.search.results_status.clone())
                     .aligned()
                     .contained()
                     .with_background_color(theme.editor.background)
@@ -222,16 +222,16 @@ impl View for ProjectSearchView {
 }
 
 impl Item for ProjectSearchView {
-    fn act_as_type(
-        &self,
+    fn act_as_type<'a>(
+        &'a self,
         type_id: TypeId,
-        self_handle: &ViewHandle<Self>,
-        _: &gpui::AppContext,
-    ) -> Option<gpui::AnyViewHandle> {
+        self_handle: &'a ViewHandle<Self>,
+        _: &'a AppContext,
+    ) -> Option<&'a AnyViewHandle> {
         if type_id == TypeId::of::<Self>() {
-            Some(self_handle.into())
+            Some(self_handle)
         } else if type_id == TypeId::of::<Editor>() {
-            Some((&self.results_editor).into())
+            Some(&self.results_editor)
         } else {
             None
         }
@@ -246,17 +246,17 @@ impl Item for ProjectSearchView {
         &self,
         _detail: Option<usize>,
         tab_theme: &theme::Tab,
-        cx: &gpui::AppContext,
+        cx: &AppContext,
     ) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        let search_theme = &settings.theme.search;
         Flex::row()
             .with_child(
                 Svg::new("icons/magnifying_glass_12.svg")
                     .with_color(tab_theme.label.text.color)
                     .constrained()
-                    .with_width(search_theme.tab_icon_width)
+                    .with_width(tab_theme.type_icon_width)
                     .aligned()
+                    .contained()
+                    .with_margin_right(tab_theme.spacing)
                     .boxed(),
             )
             .with_children(self.model.read(cx).active_query.as_ref().map(|query| {
@@ -264,8 +264,6 @@ impl Item for ProjectSearchView {
 
                 Label::new(query_text, tab_theme.label.clone())
                     .aligned()
-                    .contained()
-                    .with_margin_left(search_theme.tab_icon_spacing)
                     .boxed()
             }))
             .boxed()
@@ -279,7 +277,7 @@ impl Item for ProjectSearchView {
         false
     }
 
-    fn can_save(&self, _: &gpui::AppContext) -> bool {
+    fn can_save(&self, _: &AppContext) -> bool {
         true
     }
 
@@ -540,7 +538,7 @@ impl ProjectSearchView {
 
             let range_to_select = match_ranges[new_index].clone();
             self.results_editor.update(cx, |editor, cx| {
-                editor.unfold_ranges([range_to_select.clone()], false, cx);
+                editor.unfold_ranges([range_to_select.clone()], false, true, cx);
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.select_ranges([range_to_select])
                 });
@@ -744,7 +742,7 @@ impl ProjectSearchBar {
 
     fn render_nav_button(
         &self,
-        icon: &str,
+        icon: &'static str,
         direction: Direction,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
@@ -770,7 +768,7 @@ impl ProjectSearchBar {
                 .search
                 .option_button
                 .style_for(state, false);
-            Label::new(icon.to_string(), style.text.clone())
+            Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
                 .boxed()
@@ -792,7 +790,7 @@ impl ProjectSearchBar {
 
     fn render_option_button(
         &self,
-        icon: &str,
+        icon: &'static str,
         option: SearchOption,
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
@@ -805,7 +803,7 @@ impl ProjectSearchBar {
                 .search
                 .option_button
                 .style_for(state, is_active);
-            Label::new(icon.to_string(), style.text.clone())
+            Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
                 .boxed()
@@ -932,7 +930,7 @@ impl ToolbarItemView for ProjectSearchBar {
         self.active_project_search = None;
         if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
             let query_editor = search.read(cx).query_editor.clone();
-            cx.reparent(query_editor);
+            cx.reparent(&query_editor);
             self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
             self.active_project_search = Some(search);
             ToolbarItemLocation::PrimaryLeft {

crates/settings/Cargo.toml 🔗

@@ -22,9 +22,10 @@ futures = "0.3"
 theme = { path = "../theme" }
 util = { path = "../util" }
 json_comments = "0.2"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 schemars = "0.8"
 serde = { workspace = true }
+serde_derive = { workspace = true }
 serde_json = { workspace = true }
 serde_path_to_error = "0.1.4"
 toml = "0.5"
@@ -35,3 +36,4 @@ tree-sitter-json = "*"
 unindent = "0.1"
 gpui = { path = "../gpui", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
+pretty_assertions = "1.3.0"

crates/settings/src/keymap_file.rs 🔗

@@ -1,4 +1,4 @@
-use crate::parse_json_with_comments;
+use crate::{parse_json_with_comments, Settings};
 use anyhow::{Context, Result};
 use assets::Assets;
 use collections::BTreeMap;
@@ -45,6 +45,10 @@ impl KeymapFileContent {
         for path in ["keymaps/default.json", "keymaps/vim.json"] {
             Self::load(path, cx).unwrap();
         }
+
+        if let Some(asset_path) = cx.global::<Settings>().base_keymap.asset_path() {
+            Self::load(asset_path, cx).log_err();
+        }
     }
 
     pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> {

crates/settings/src/settings.rs 🔗

@@ -5,7 +5,7 @@ pub mod watched_json;
 use anyhow::{bail, Result};
 use gpui::{
     font_cache::{FamilyId, FontCache},
-    AssetSource,
+    fonts, AssetSource,
 };
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
@@ -18,17 +18,21 @@ use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
-use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
+use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
 use theme::{Theme, ThemeRegistry};
 use tree_sitter::Query;
-use util::ResultExt as _;
+use util::{RangeExt, ResultExt as _};
 
 pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
+pub use watched_json::watch_files;
 
 #[derive(Clone)]
 pub struct Settings {
+    pub buffer_font_family_name: String,
+    pub buffer_font_features: fonts::Features,
     pub buffer_font_family: FamilyId,
     pub default_buffer_font_size: f32,
+    pub enable_copilot_integration: bool,
     pub buffer_font_size: f32,
     pub active_pane_magnification: f32,
     pub cursor_blink: bool,
@@ -54,6 +58,72 @@ pub struct Settings {
     pub telemetry_defaults: TelemetrySettings,
     pub telemetry_overrides: TelemetrySettings,
     pub auto_update: bool,
+    pub base_keymap: BaseKeymap,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum CopilotSettings {
+    #[default]
+    On,
+    Off,
+}
+
+impl From<CopilotSettings> for bool {
+    fn from(value: CopilotSettings) -> Self {
+        match value {
+            CopilotSettings::On => true,
+            CopilotSettings::Off => false,
+        }
+    }
+}
+
+impl CopilotSettings {
+    pub fn is_on(&self) -> bool {
+        <CopilotSettings as Into<bool>>::into(*self)
+    }
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+pub enum BaseKeymap {
+    #[default]
+    VSCode,
+    JetBrains,
+    SublimeText,
+    Atom,
+    TextMate,
+}
+
+impl BaseKeymap {
+    pub const OPTIONS: [(&'static str, Self); 5] = [
+        ("VSCode (Default)", Self::VSCode),
+        ("Atom", Self::Atom),
+        ("JetBrains", Self::JetBrains),
+        ("Sublime Text", Self::SublimeText),
+        ("TextMate", Self::TextMate),
+    ];
+
+    pub fn asset_path(&self) -> Option<&'static str> {
+        match self {
+            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
+            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
+            BaseKeymap::Atom => Some("keymaps/atom.json"),
+            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
+            BaseKeymap::VSCode => None,
+        }
+    }
+
+    pub fn names() -> impl Iterator<Item = &'static str> {
+        Self::OPTIONS.iter().map(|(name, _)| *name)
+    }
+
+    pub fn from_names(option: &str) -> BaseKeymap {
+        Self::OPTIONS
+            .iter()
+            .copied()
+            .find_map(|(name, value)| (name == option).then(|| value))
+            .unwrap_or_default()
+    }
 }
 
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -66,9 +136,18 @@ impl TelemetrySettings {
     pub fn metrics(&self) -> bool {
         self.metrics.unwrap()
     }
+
     pub fn diagnostics(&self) -> bool {
         self.diagnostics.unwrap()
     }
+
+    pub fn set_metrics(&mut self, value: bool) {
+        self.metrics = Some(value);
+    }
+
+    pub fn set_diagnostics(&mut self, value: bool) {
+        self.diagnostics = Some(value);
+    }
 }
 
 #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -94,8 +173,46 @@ pub struct EditorSettings {
     pub soft_wrap: Option<SoftWrap>,
     pub preferred_line_length: Option<u32>,
     pub format_on_save: Option<FormatOnSave>,
+    pub remove_trailing_whitespace_on_save: Option<bool>,
+    pub ensure_final_newline_on_save: Option<bool>,
     pub formatter: Option<Formatter>,
     pub enable_language_server: Option<bool>,
+    pub copilot: Option<OnOff>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum OnOff {
+    On,
+    Off,
+}
+
+impl OnOff {
+    pub fn as_bool(&self) -> bool {
+        match self {
+            OnOff::On => true,
+            OnOff::Off => false,
+        }
+    }
+
+    pub fn from_bool(value: bool) -> OnOff {
+        match value {
+            true => OnOff::On,
+            false => OnOff::Off,
+        }
+    }
+}
+
+impl From<OnOff> for bool {
+    fn from(value: OnOff) -> bool {
+        value.as_bool()
+    }
+}
+
+impl From<bool> for OnOff {
+    fn from(value: bool) -> OnOff {
+        OnOff::from_bool(value)
+    }
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -170,6 +287,7 @@ pub struct TerminalSettings {
     pub working_directory: Option<WorkingDirectory>,
     pub font_size: Option<f32>,
     pub font_family: Option<String>,
+    pub font_features: Option<fonts::Features>,
     pub env: Option<HashMap<String, String>>,
     pub blinking: Option<TerminalBlink>,
     pub alternate_scroll: Option<AlternateScroll>,
@@ -272,13 +390,13 @@ impl Column for DockAnchor {
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 pub struct SettingsFileContent {
-    #[serde(default)]
-    pub projects_online_by_default: Option<bool>,
     #[serde(default)]
     pub buffer_font_family: Option<String>,
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
     #[serde(default)]
+    pub buffer_font_features: Option<fonts::Features>,
+    #[serde(default)]
     pub active_pane_magnification: Option<f32>,
     #[serde(default)]
     pub cursor_blink: Option<bool>,
@@ -315,6 +433,10 @@ pub struct SettingsFileContent {
     pub telemetry: TelemetrySettings,
     #[serde(default)]
     pub auto_update: Option<bool>,
+    #[serde(default)]
+    pub base_keymap: Option<BaseKeymap>,
+    #[serde(default)]
+    pub enable_copilot_integration: Option<bool>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -341,10 +463,16 @@ impl Settings {
         )
         .unwrap();
 
+        let buffer_font_features = defaults.buffer_font_features.unwrap();
         Self {
             buffer_font_family: font_cache
-                .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
+                .load_family(
+                    &[defaults.buffer_font_family.as_ref().unwrap()],
+                    &buffer_font_features,
+                )
                 .unwrap(),
+            buffer_font_family_name: defaults.buffer_font_family.unwrap(),
+            buffer_font_features,
             buffer_font_size: defaults.buffer_font_size.unwrap(),
             active_pane_magnification: defaults.active_pane_magnification.unwrap(),
             default_buffer_font_size: defaults.buffer_font_size.unwrap(),
@@ -361,9 +489,16 @@ impl Settings {
                 hard_tabs: required(defaults.editor.hard_tabs),
                 soft_wrap: required(defaults.editor.soft_wrap),
                 preferred_line_length: required(defaults.editor.preferred_line_length),
+                remove_trailing_whitespace_on_save: required(
+                    defaults.editor.remove_trailing_whitespace_on_save,
+                ),
+                ensure_final_newline_on_save: required(
+                    defaults.editor.ensure_final_newline_on_save,
+                ),
                 format_on_save: required(defaults.editor.format_on_save),
                 formatter: required(defaults.editor.formatter),
                 enable_language_server: required(defaults.editor.enable_language_server),
+                copilot: required(defaults.editor.copilot),
             },
             editor_overrides: Default::default(),
             git: defaults.git.unwrap(),
@@ -379,6 +514,8 @@ impl Settings {
             telemetry_defaults: defaults.telemetry,
             telemetry_overrides: Default::default(),
             auto_update: defaults.auto_update.unwrap(),
+            base_keymap: Default::default(),
+            enable_copilot_integration: defaults.enable_copilot_integration.unwrap(),
         }
     }
 
@@ -389,11 +526,24 @@ impl Settings {
         theme_registry: &ThemeRegistry,
         font_cache: &FontCache,
     ) {
-        if let Some(value) = &data.buffer_font_family {
-            if let Some(id) = font_cache.load_family(&[value]).log_err() {
+        let mut family_changed = false;
+        if let Some(value) = data.buffer_font_family {
+            self.buffer_font_family_name = value;
+            family_changed = true;
+        }
+        if let Some(value) = data.buffer_font_features {
+            self.buffer_font_features = value;
+            family_changed = true;
+        }
+        if family_changed {
+            if let Some(id) = font_cache
+                .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
+                .log_err()
+            {
                 self.buffer_font_family = id;
             }
         }
+
         if let Some(value) = &data.theme {
             if let Some(theme) = theme_registry.get(value).log_err() {
                 self.theme = theme;
@@ -416,11 +566,11 @@ impl Settings {
         merge(&mut self.vim_mode, data.vim_mode);
         merge(&mut self.autosave, data.autosave);
         merge(&mut self.default_dock_anchor, data.default_dock_anchor);
-
-        // Ensure terminal font is loaded, so we can request it in terminal_element layout
-        if let Some(terminal_font) = &data.terminal.font_family {
-            font_cache.load_family(&[terminal_font]).log_err();
-        }
+        merge(&mut self.base_keymap, data.base_keymap);
+        merge(
+            &mut self.enable_copilot_integration,
+            data.enable_copilot_integration,
+        );
 
         self.editor_overrides = data.editor;
         self.git_overrides = data.git.unwrap_or_default();
@@ -444,6 +594,14 @@ impl Settings {
         self
     }
 
+    pub fn copilot_on(&self, language: Option<&str>) -> bool {
+        if self.enable_copilot_integration {
+            self.language_setting(language, |settings| settings.copilot.map(Into::into))
+        } else {
+            false
+        }
+    }
+
     pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
         self.language_setting(language, |settings| settings.tab_size)
     }
@@ -460,6 +618,18 @@ impl Settings {
         self.language_setting(language, |settings| settings.preferred_line_length)
     }
 
+    pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
+        self.language_setting(language, |settings| {
+            settings.remove_trailing_whitespace_on_save.clone()
+        })
+    }
+
+    pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
+        self.language_setting(language, |settings| {
+            settings.ensure_final_newline_on_save.clone()
+        })
+    }
+
     pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
         self.language_setting(language, |settings| settings.format_on_save.clone())
     }
@@ -541,7 +711,12 @@ impl Settings {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &gpui::AppContext) -> Settings {
         Settings {
-            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
+            buffer_font_family_name: "Monaco".to_string(),
+            buffer_font_features: Default::default(),
+            buffer_font_family: cx
+                .font_cache()
+                .load_family(&["Monaco"], &Default::default())
+                .unwrap(),
             buffer_font_size: 14.,
             active_pane_magnification: 1.,
             default_buffer_font_size: 14.,
@@ -558,9 +733,12 @@ impl Settings {
                 hard_tabs: Some(false),
                 soft_wrap: Some(SoftWrap::None),
                 preferred_line_length: Some(80),
+                remove_trailing_whitespace_on_save: Some(true),
+                ensure_final_newline_on_save: Some(true),
                 format_on_save: Some(FormatOnSave::On),
                 formatter: Some(Formatter::LanguageServer),
                 enable_language_server: Some(true),
+                copilot: Some(OnOff::On),
             },
             editor_overrides: Default::default(),
             journal_defaults: Default::default(),
@@ -579,6 +757,8 @@ impl Settings {
             },
             telemetry_overrides: Default::default(),
             auto_update: true,
+            base_keymap: Default::default(),
+            enable_copilot_integration: true,
         }
     }
 
@@ -655,13 +835,22 @@ pub fn settings_file_json_schema(
     serde_json::to_value(root_schema).unwrap()
 }
 
-/// Expects the key to be unquoted, and the value to be valid JSON
-/// (e.g. values should be unquoted for numbers and bools, quoted for strings)
-pub fn write_top_level_setting(
-    mut settings_content: String,
-    top_level_key: &str,
-    new_val: &str,
-) -> String {
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}
+
+pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
+    Ok(serde_json::from_reader(
+        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
+    )?)
+}
+
+fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) {
+    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
+    const LANGAUGES: &'static str = "languages";
+
     let mut parser = tree_sitter::Parser::new();
     parser.set_language(tree_sitter_json::language()).unwrap();
     let tree = parser.parse(&settings_content, None).unwrap();
@@ -671,56 +860,91 @@ pub fn write_top_level_setting(
     let query = Query::new(
         tree_sitter_json::language(),
         "
-        (document
-            (object
-                (pair
-                    key: (string) @key
-                    value: (_) @value)))
-    ",
+            (pair
+                key: (string) @key
+                value: (_) @value)
+        ",
     )
     .unwrap();
 
+    let has_language_overrides = settings_content.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 = None;
+    let mut existing_value_range = 0..settings_content.len();
     let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
     for mat in matches {
         if mat.captures.len() != 2 {
             continue;
         }
 
-        let key = mat.captures[0];
-        let value = mat.captures[1];
+        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.node.start_byte());
+        first_key_start.get_or_insert_with(|| key_range.start);
 
-        if let Some(key_text) = settings_content.get(key.node.byte_range()) {
-            if key_text == format!("\"{top_level_key}\"") {
-                existing_value_range = Some(value.node.byte_range());
+        let found_key = settings_content
+            .get(key_range.clone())
+            .map(|key_text| {
+                if key_path[depth] == LANGAUGES && 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;
             }
         }
     }
 
-    match (first_key_start, existing_value_range) {
-        (None, None) => {
-            // No document, create a new object and overwrite
-            settings_content.clear();
-            write!(
-                settings_content,
-                "{{\n    \"{}\": {new_val}\n}}\n",
-                top_level_key
-            )
-            .unwrap();
-        }
-
-        (_, Some(existing_value_range)) => {
-            // Existing theme key, overwrite
-            settings_content.replace_range(existing_value_range, &new_val);
+    // We found the exact key we want, insert the new value
+    if depth == key_path.len() {
+        let new_val = serde_json::to_string_pretty(new_value)
+            .expect("Could not serialize new json field to string");
+        settings_content.replace_range(existing_value_range, &new_val);
+    } else {
+        // We have key paths, construct the sub objects
+        let new_key = if has_language_overrides && key_path[depth] == LANGAUGES {
+            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 == &LANGAUGES {
+                new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
+            } else {
+                new_value = serde_json::json!({ key.to_string(): new_value });
+            }
         }
 
-        (Some(first_key_start), None) => {
-            // No existing theme key, but other settings. Prepend new theme settings and
-            // match style of first key
+        if let Some(first_key_start) = first_key_start {
             let mut row = 0;
             let mut column = 0;
             for (ix, char) in settings_content.char_indices() {
@@ -735,142 +959,497 @@ pub fn write_top_level_setting(
                 }
             }
 
-            let content = format!(r#""{top_level_key}": {new_val},"#);
-            settings_content.insert_str(first_key_start, &content);
-
             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 content = format!(r#""{new_key}": {new_val},"#);
+                settings_content.insert_str(first_key_start, &content);
+
                 settings_content.insert_str(
                     first_key_start + content.len(),
                     &format!("\n{:width$}", ' ', width = column),
                 )
             } else {
-                settings_content.insert_str(first_key_start + content.len(), " ")
+                let new_val = serde_json::to_string(&new_value).unwrap();
+                let mut content = format!(r#""{new_key}": {new_val},"#);
+                content.push(' ');
+                settings_content.insert_str(first_key_start, &content);
+            }
+        } else {
+            new_value = serde_json::json!({ new_key.to_string(): new_value });
+            let indent_prefix_len = 4 * depth;
+            let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
+
+            settings_content.replace_range(existing_value_range, &new_val);
+            if depth == 0 {
+                settings_content.push('\n');
             }
         }
     }
+}
+
+fn to_pretty_json(
+    value: &serde_json::Value,
+    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();
 
-    settings_content
+    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
 }
 
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
+pub fn update_settings_file(
+    mut text: String,
+    mut old_file_content: SettingsFileContent,
+    update: impl FnOnce(&mut SettingsFileContent),
+) -> String {
+    let mut new_file_content = old_file_content.clone();
+
+    update(&mut new_file_content);
+
+    if new_file_content.languages.len() != old_file_content.languages.len() {
+        for language in new_file_content.languages.keys() {
+            old_file_content
+                .languages
+                .entry(language.clone())
+                .or_default();
+        }
+        for language in old_file_content.languages.keys() {
+            new_file_content
+                .languages
+                .entry(language.clone())
+                .or_default();
+        }
     }
+
+    let old_object = to_json_object(old_file_content);
+    let new_object = to_json_object(new_file_content);
+
+    fn apply_changes_to_json_text(
+        old_object: &serde_json::Map<String, Value>,
+        new_object: &serde_json::Map<String, Value>,
+        current_key_path: Vec<&str>,
+        json_text: &mut String,
+    ) {
+        for (key, old_value) in old_object.iter() {
+            // We know that these two are from the same shape of object, so we can just unwrap
+            let new_value = new_object.get(key).unwrap();
+
+            if old_value != new_value {
+                match new_value {
+                    Value::Bool(_) | Value::Number(_) | Value::String(_) => {
+                        let mut key_path = current_key_path.clone();
+                        key_path.push(key);
+                        write_settings_key(json_text, &key_path, &new_value);
+                    }
+                    Value::Object(new_sub_object) => {
+                        let mut key_path = current_key_path.clone();
+                        key_path.push(key);
+                        if let Value::Object(old_sub_object) = old_value {
+                            apply_changes_to_json_text(
+                                old_sub_object,
+                                new_sub_object,
+                                key_path,
+                                json_text,
+                            );
+                        } else {
+                            unimplemented!("This function doesn't support changing values from simple values to objects yet");
+                        }
+                    }
+                    Value::Null | Value::Array(_) => {
+                        unimplemented!("We only support objects and simple values");
+                    }
+                }
+            }
+        }
+    }
+
+    apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text);
+
+    text
 }
 
-pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
-    Ok(serde_json::from_reader(
-        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
-    )?)
+fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
+    let tmp = serde_json::to_value(settings_file).unwrap();
+    match tmp {
+        Value::Object(map) => map,
+        _ => unreachable!("SettingsFileContent represents a JSON map"),
+    }
 }
 
 #[cfg(test)]
 mod tests {
-    use crate::write_top_level_setting;
+    use super::*;
     use unindent::Unindent;
 
-    #[test]
-    fn test_write_theme_into_settings_with_theme() {
-        let settings = r#"
-            {
-                "theme": "One Dark"
-            }
-        "#
-        .unindent();
+    fn assert_new_settings<S1: Into<String>, S2: Into<String>>(
+        old_json: S1,
+        update: fn(&mut SettingsFileContent),
+        expected_new_json: S2,
+    ) {
+        let old_json = old_json.into();
+        let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
+        let new_json = update_settings_file(old_json, old_content, update);
+        pretty_assertions::assert_eq!(new_json, expected_new_json.into());
+    }
 
-        let new_settings = r#"
-            {
-                "theme": "summerfruit-light"
-            }
-        "#
-        .unindent();
+    #[test]
+    fn test_update_language_overrides_copilot() {
+        assert_new_settings(
+            r#"
+                {
+                    "language_overrides": {
+                        "JSON": {
+                            "copilot": "off"
+                        }
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| {
+                settings.languages.insert(
+                    "Rust".into(),
+                    EditorSettings {
+                        copilot: Some(OnOff::On),
+                        ..Default::default()
+                    },
+                );
+            },
+            r#"
+                {
+                    "language_overrides": {
+                        "Rust": {
+                            "copilot": "on"
+                        },
+                        "JSON": {
+                            "copilot": "off"
+                        }
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        let settings_after_theme =
-            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
+    #[test]
+    fn test_update_copilot() {
+        assert_new_settings(
+            r#"
+                {
+                    "languages": {
+                        "JSON": {
+                            "copilot": "off"
+                        }
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| {
+                settings.editor.copilot = Some(OnOff::On);
+            },
+            r#"
+                {
+                    "copilot": "on",
+                    "languages": {
+                        "JSON": {
+                            "copilot": "off"
+                        }
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        assert_eq!(settings_after_theme, new_settings)
+    #[test]
+    fn test_update_langauge_copilot() {
+        assert_new_settings(
+            r#"
+                {
+                    "languages": {
+                        "JSON": {
+                            "copilot": "off"
+                        }
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| {
+                settings.languages.insert(
+                    "Rust".into(),
+                    EditorSettings {
+                        copilot: Some(OnOff::On),
+                        ..Default::default()
+                    },
+                );
+            },
+            r#"
+                {
+                    "languages": {
+                        "Rust": {
+                            "copilot": "on"
+                        },
+                        "JSON": {
+                            "copilot": "off"
+                        }
+                    }
+                }
+            "#
+            .unindent(),
+        );
     }
 
     #[test]
-    fn test_write_theme_into_empty_settings() {
-        let settings = r#"
-            {
-            }
-        "#
-        .unindent();
+    fn test_update_telemetry_setting_multiple_fields() {
+        assert_new_settings(
+            r#"
+                {
+                    "telemetry": {
+                        "metrics": false,
+                        "diagnostics": false
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| {
+                settings.telemetry.set_diagnostics(true);
+                settings.telemetry.set_metrics(true);
+            },
+            r#"
+                {
+                    "telemetry": {
+                        "metrics": true,
+                        "diagnostics": true
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        let new_settings = r#"
-            {
-                "theme": "summerfruit-light"
-            }
-        "#
-        .unindent();
+    #[test]
+    fn test_update_telemetry_setting_weird_formatting() {
+        assert_new_settings(
+            r#"{
+                "telemetry":   { "metrics": false, "diagnostics": true }
+            }"#
+            .unindent(),
+            |settings| settings.telemetry.set_diagnostics(false),
+            r#"{
+                "telemetry":   { "metrics": false, "diagnostics": false }
+            }"#
+            .unindent(),
+        );
+    }
 
-        let settings_after_theme =
-            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
+    #[test]
+    fn test_update_telemetry_setting_other_fields() {
+        assert_new_settings(
+            r#"
+                {
+                    "telemetry": {
+                        "metrics": false,
+                        "diagnostics": true
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| settings.telemetry.set_diagnostics(false),
+            r#"
+                {
+                    "telemetry": {
+                        "metrics": false,
+                        "diagnostics": false
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        assert_eq!(settings_after_theme, new_settings)
+    #[test]
+    fn test_update_telemetry_setting_empty_telemetry() {
+        assert_new_settings(
+            r#"
+                {
+                    "telemetry": {}
+                }
+            "#
+            .unindent(),
+            |settings| settings.telemetry.set_diagnostics(false),
+            r#"
+                {
+                    "telemetry": {
+                        "diagnostics": false
+                    }
+                }
+            "#
+            .unindent(),
+        );
     }
 
     #[test]
-    fn test_write_theme_into_no_settings() {
-        let settings = "".to_string();
+    fn test_update_telemetry_setting_pre_existing() {
+        assert_new_settings(
+            r#"
+                {
+                    "telemetry": {
+                        "diagnostics": true
+                    }
+                }
+            "#
+            .unindent(),
+            |settings| settings.telemetry.set_diagnostics(false),
+            r#"
+                {
+                    "telemetry": {
+                        "diagnostics": false
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        let new_settings = r#"
-            {
-                "theme": "summerfruit-light"
-            }
-        "#
-        .unindent();
+    #[test]
+    fn test_update_telemetry_setting() {
+        assert_new_settings(
+            "{}",
+            |settings| settings.telemetry.set_diagnostics(true),
+            r#"
+                {
+                    "telemetry": {
+                        "diagnostics": true
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        let settings_after_theme =
-            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
+    #[test]
+    fn test_update_object_empty_doc() {
+        assert_new_settings(
+            "",
+            |settings| settings.telemetry.set_diagnostics(true),
+            r#"
+                {
+                    "telemetry": {
+                        "diagnostics": true
+                    }
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        assert_eq!(settings_after_theme, new_settings)
+    #[test]
+    fn test_write_theme_into_settings_with_theme() {
+        assert_new_settings(
+            r#"
+                {
+                    "theme": "One Dark"
+                }
+            "#
+            .unindent(),
+            |settings| settings.theme = Some("summerfruit-light".to_string()),
+            r#"
+                {
+                    "theme": "summerfruit-light"
+                }
+            "#
+            .unindent(),
+        );
     }
 
     #[test]
-    fn test_write_theme_into_single_line_settings_without_theme() {
-        let settings = r#"{ "a": "", "ok": true }"#.to_string();
-        let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#;
+    fn test_write_theme_into_empty_settings() {
+        assert_new_settings(
+            r#"
+                {
+                }
+            "#
+            .unindent(),
+            |settings| settings.theme = Some("summerfruit-light".to_string()),
+            r#"
+                {
+                    "theme": "summerfruit-light"
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        let settings_after_theme =
-            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
+    #[test]
+    fn write_key_no_document() {
+        assert_new_settings(
+            "",
+            |settings| settings.theme = Some("summerfruit-light".to_string()),
+            r#"
+                {
+                    "theme": "summerfruit-light"
+                }
+            "#
+            .unindent(),
+        );
+    }
 
-        assert_eq!(settings_after_theme, new_settings)
+    #[test]
+    fn test_write_theme_into_single_line_settings_without_theme() {
+        assert_new_settings(
+            r#"{ "a": "", "ok": true }"#,
+            |settings| settings.theme = Some("summerfruit-light".to_string()),
+            r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#,
+        );
     }
 
     #[test]
     fn test_write_theme_pre_object_whitespace() {
-        let settings = r#"          { "a": "", "ok": true }"#.to_string();
-        let new_settings = r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#;
-
-        let settings_after_theme =
-            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
-
-        assert_eq!(settings_after_theme, new_settings)
+        assert_new_settings(
+            r#"          { "a": "", "ok": true }"#,
+            |settings| settings.theme = Some("summerfruit-light".to_string()),
+            r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
+        );
     }
 
     #[test]
     fn test_write_theme_into_multi_line_settings_without_theme() {
-        let settings = r#"
-            {
-                "a": "b"
-            }
-        "#
-        .unindent();
-
-        let new_settings = r#"
-            {
-                "theme": "summerfruit-light",
-                "a": "b"
-            }
-        "#
-        .unindent();
-
-        let settings_after_theme =
-            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
-
-        assert_eq!(settings_after_theme, new_settings)
+        assert_new_settings(
+            r#"
+                {
+                    "a": "b"
+                }
+            "#
+            .unindent(),
+            |settings| settings.theme = Some("summerfruit-light".to_string()),
+            r#"
+                {
+                    "theme": "summerfruit-light",
+                    "a": "b"
+                }
+            "#
+            .unindent(),
+        );
     }
 }

crates/settings/src/settings_file.rs 🔗

@@ -1,9 +1,9 @@
-use crate::{watched_json::WatchedJsonFile, write_top_level_setting, SettingsFileContent};
+use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent};
 use anyhow::Result;
+use assets::Assets;
 use fs::Fs;
-use gpui::MutableAppContext;
-use serde_json::Value;
-use std::{path::Path, sync::Arc};
+use gpui::{AssetSource, MutableAppContext};
+use std::{io::ErrorKind, path::Path, sync::Arc};
 
 // TODO: Switch SettingsFile to open a worktree and buffer for synchronization
 //       And instant updates in the Zed editor
@@ -27,57 +27,45 @@ impl SettingsFile {
         }
     }
 
-    pub fn update(cx: &mut MutableAppContext, update: impl FnOnce(&mut SettingsFileContent)) {
+    async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
+        match fs.load(path).await {
+            result @ Ok(_) => result,
+            Err(err) => {
+                if let Some(e) = err.downcast_ref::<std::io::Error>() {
+                    if e.kind() == ErrorKind::NotFound {
+                        return Ok(std::str::from_utf8(
+                            Assets
+                                .load("settings/initial_user_settings.json")
+                                .unwrap()
+                                .as_ref(),
+                        )
+                        .unwrap()
+                        .to_string());
+                    }
+                }
+                return Err(err);
+            }
+        }
+    }
+
+    pub fn update(
+        cx: &mut MutableAppContext,
+        update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
+    ) {
         let this = cx.global::<SettingsFile>();
 
         let current_file_content = this.settings_file_content.current();
-        let mut new_file_content = current_file_content.clone();
-
-        update(&mut new_file_content);
 
         let fs = this.fs.clone();
         let path = this.path.clone();
 
         cx.background()
             .spawn(async move {
-                // Unwrap safety: These values are all guarnteed to be well formed, and we know
-                // that they will deserialize to our settings object. All of the following unwraps
-                // are therefore safe.
-                let tmp = serde_json::to_value(current_file_content).unwrap();
-                let old_json = tmp.as_object().unwrap();
-
-                let new_tmp = serde_json::to_value(new_file_content).unwrap();
-                let new_json = new_tmp.as_object().unwrap();
-
-                // Find changed fields
-                let mut diffs = vec![];
-                for (key, old_value) in old_json.iter() {
-                    let new_value = new_json.get(key).unwrap();
-                    if old_value != new_value {
-                        if matches!(
-                            new_value,
-                            &Value::Null | &Value::Object(_) | &Value::Array(_)
-                        ) {
-                            unimplemented!(
-                                "We only support updating basic values at the top level"
-                            );
-                        }
-
-                        let new_json = serde_json::to_string_pretty(new_value)
-                            .expect("Could not serialize new json field to string");
-
-                        diffs.push((key, new_json));
-                    }
-                }
+                let old_text = SettingsFile::load_settings(path, &fs).await?;
 
-                // Have diffs, rewrite the settings file now.
-                let mut content = fs.load(path).await?;
+                let new_text = update_settings_file(old_text, current_file_content, update);
 
-                for (key, new_value) in diffs {
-                    content = write_top_level_setting(content, key, &new_value)
-                }
-
-                fs.atomic_write(path.to_path_buf(), content).await?;
+                fs.atomic_write(path.to_path_buf(), new_text).await?;
 
                 Ok(()) as Result<()>
             })
@@ -88,10 +76,164 @@ impl SettingsFile {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap};
+    use crate::{
+        watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
+    };
     use fs::FakeFs;
+    use gpui::{actions, Action};
     use theme::ThemeRegistry;
 
+    #[gpui::test]
+    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+        let font_cache = cx.font_cache();
+
+        actions!(test, [A, B]);
+        // From the Atom keymap
+        actions!(workspace, [ActivatePreviousPane]);
+        // From the JetBrains keymap
+        actions!(pane, [ActivatePrevItem]);
+
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "Atom"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": "test::A"
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        let settings_file =
+            WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
+        let keymaps_file =
+            WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
+
+        let default_settings = cx.read(Settings::test);
+
+        cx.update(|cx| {
+            cx.add_global_action(|_: &A, _cx| {});
+            cx.add_global_action(|_: &B, _cx| {});
+            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+            watch_files(
+                default_settings,
+                settings_file,
+                ThemeRegistry::new((), font_cache),
+                keymaps_file,
+                cx,
+            )
+        });
+
+        cx.foreground().run_until_parked();
+
+        // Test loading the keymap base at all
+        cx.update(|cx| {
+            assert_key_bindings_for(
+                cx,
+                vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+                line!(),
+            );
+        });
+
+        // Test modifying the users keymap, while retaining the base keymap
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": "test::B"
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        cx.update(|cx| {
+            assert_key_bindings_for(
+                cx,
+                vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+                line!(),
+            );
+        });
+
+        // Test modifying the base, while retaining the users keymap
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "JetBrains"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        cx.update(|cx| {
+            assert_key_bindings_for(
+                cx,
+                vec![("backspace", &B), ("[", &ActivatePrevItem)],
+                line!(),
+            );
+        });
+    }
+
+    fn assert_key_bindings_for<'a>(
+        cx: &mut MutableAppContext,
+        actions: Vec<(&'static str, &'a dyn Action)>,
+        line: u32,
+    ) {
+        for (key, action) in actions {
+            // assert that...
+            assert!(
+                cx.available_actions(0, 0).any(|(_, bound_action, b)| {
+                    // action names match...
+                    bound_action.name() == action.name()
+                    && bound_action.namespace() == action.namespace()
+                    // and key strokes contain the given key
+                    && b.iter()
+                        .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+                }),
+                "On {} Failed to find {} with key binding {}",
+                line,
+                action.name(),
+                key
+            );
+        }
+    }
+
     #[gpui::test]
     async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
         let executor = cx.background();

crates/settings/src/watched_json.rs 🔗

@@ -62,7 +62,18 @@ where
     }
 }
 
-pub fn watch_settings_file(
+pub fn watch_files(
+    defaults: Settings,
+    settings_file: WatchedJsonFile<SettingsFileContent>,
+    theme_registry: Arc<ThemeRegistry>,
+    keymap_file: WatchedJsonFile<KeymapFileContent>,
+    cx: &mut MutableAppContext,
+) {
+    watch_settings_file(defaults, settings_file, theme_registry, cx);
+    watch_keymap_file(keymap_file, cx);
+}
+
+pub(crate) fn watch_settings_file(
     defaults: Settings,
     mut file: WatchedJsonFile<SettingsFileContent>,
     theme_registry: Arc<ThemeRegistry>,
@@ -77,13 +88,13 @@ pub fn watch_settings_file(
     .detach();
 }
 
-pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
+fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
     cx.clear_bindings();
     KeymapFileContent::load_defaults(cx);
     content.add_to_cx(cx).log_err();
 }
 
-pub fn settings_updated(
+fn settings_updated(
     defaults: &Settings,
     content: SettingsFileContent,
     theme_registry: &Arc<ThemeRegistry>,
@@ -95,10 +106,20 @@ pub fn settings_updated(
     cx.refresh_windows();
 }
 
-pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
+fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
     cx.spawn(|mut cx| async move {
+        let mut settings_subscription = None;
         while let Some(content) = file.0.recv().await {
-            cx.update(|cx| keymap_updated(content, cx));
+            cx.update(|cx| {
+                let old_base_keymap = cx.global::<Settings>().base_keymap;
+                keymap_updated(content.clone(), cx);
+                settings_subscription = Some(cx.observe_global::<Settings, _>(move |cx| {
+                    let settings = cx.global::<Settings>();
+                    if settings.base_keymap != old_base_keymap {
+                        keymap_updated(content.clone(), cx);
+                    }
+                }));
+            });
         }
     })
     .detach();

crates/terminal/Cargo.toml 🔗

@@ -29,7 +29,8 @@ libc = "0.2"
 anyhow = "1"
 thiserror = "1.0"
 lazy_static = "1.4.0"
-serde = { version = "1.0", features = ["derive"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 
 [dev-dependencies]
 rand = "0.8.5"

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

@@ -186,6 +186,9 @@ pub fn mouse_moved_report(point: Point, e: &MouseMovedEvent, mode: TermMode) ->
 }
 
 pub fn mouse_side(pos: Vector2F, cur_size: TerminalSize) -> alacritty_terminal::index::Direction {
+    if cur_size.cell_width as usize == 0 {
+        return Side::Right;
+    }
     let x = pos.0.x() as usize;
     let cell_x = x.saturating_sub(cur_size.cell_width as usize) % cur_size.cell_width as usize;
     let half_cell_width = (cur_size.cell_width / 2.0) as usize;

crates/terminal/src/terminal.rs 🔗

@@ -32,6 +32,7 @@ use mappings::mouse::{
 
 use procinfo::LocalProcessInfo;
 use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
+use util::truncate_and_trailoff;
 
 use std::{
     cmp::min,
@@ -1169,6 +1170,38 @@ impl Terminal {
             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())
+    }
 }
 
 impl Drop for Terminal {

crates/terminal_view/Cargo.toml 🔗

@@ -33,7 +33,8 @@ libc = "0.2"
 anyhow = "1"
 thiserror = "1.0"
 lazy_static = "1.4.0"
-serde = { version = "1.0", features = ["derive"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 
 
 

crates/terminal_view/src/terminal_button.rs 🔗

@@ -0,0 +1,196 @@
+use context_menu::{ContextMenu, ContextMenuItem};
+use gpui::{
+    elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
+    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakModelHandle,
+    WeakViewHandle,
+};
+use settings::Settings;
+use std::any::TypeId;
+use terminal::Terminal;
+use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
+
+use crate::TerminalView;
+
+#[derive(Clone, PartialEq)]
+pub struct DeployTerminalMenu;
+
+#[derive(Clone, PartialEq)]
+pub struct FocusTerminal {
+    terminal_handle: WeakModelHandle<Terminal>,
+}
+
+impl_internal_actions!(terminal, [FocusTerminal, DeployTerminalMenu]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(TerminalButton::deploy_terminal_menu);
+    cx.add_action(TerminalButton::focus_terminal);
+}
+
+pub struct TerminalButton {
+    workspace: WeakViewHandle<Workspace>,
+    popup_menu: ViewHandle<ContextMenu>,
+}
+
+impl Entity for TerminalButton {
+    type Event = ();
+}
+
+impl View for TerminalButton {
+    fn ui_name() -> &'static str {
+        "TerminalButton"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
+        let workspace = self.workspace.upgrade(cx);
+        let project = match workspace {
+            Some(workspace) => workspace.read(cx).project().read(cx),
+            None => return Empty::new().boxed(),
+        };
+
+        let focused_view = cx.focused_view_id(cx.window_id());
+        let active = focused_view
+            .map(|view_id| {
+                cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::<TerminalView>())
+            })
+            .unwrap_or(false);
+
+        let has_terminals = !project.local_terminal_handles().is_empty();
+        let terminal_count = project.local_terminal_handles().len() as i32;
+        let theme = cx.global::<Settings>().theme.clone();
+
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<Self>::new(0, cx, {
+                    let theme = theme.clone();
+                    move |state, _cx| {
+                        let style = theme
+                            .workspace
+                            .status_bar
+                            .sidebar_buttons
+                            .item
+                            .style_for(state, active);
+
+                        Flex::row()
+                            .with_child(
+                                Svg::new("icons/terminal_12.svg")
+                                    .with_color(style.icon_color)
+                                    .constrained()
+                                    .with_width(style.icon_size)
+                                    .aligned()
+                                    .named("terminals-icon"),
+                            )
+                            .with_children(has_terminals.then(|| {
+                                Label::new(terminal_count.to_string(), style.label.text.clone())
+                                    .contained()
+                                    .with_style(style.label.container)
+                                    .aligned()
+                                    .boxed()
+                            }))
+                            .constrained()
+                            .with_height(style.icon_size)
+                            .contained()
+                            .with_style(style.container)
+                            .boxed()
+                    }
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, cx| {
+                    if has_terminals {
+                        cx.dispatch_action(DeployTerminalMenu);
+                    } else {
+                        if !active {
+                            cx.dispatch_action(FocusDock);
+                        }
+                    };
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Show Terminal".into(),
+                    Some(Box::new(FocusDock)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .boxed(),
+            )
+            .with_child(
+                ChildView::new(&self.popup_menu, cx)
+                    .aligned()
+                    .top()
+                    .right()
+                    .boxed(),
+            )
+            .boxed()
+    }
+}
+
+impl TerminalButton {
+    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
+        Self {
+            workspace: workspace.downgrade(),
+            popup_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(cx);
+                menu.set_position_mode(OverlayPositionMode::Local);
+                menu
+            }),
+        }
+    }
+
+    pub fn deploy_terminal_menu(
+        &mut self,
+        _action: &DeployTerminalMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
+
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let project = workspace.read(cx).project().read(cx);
+            let local_terminal_handles = project.local_terminal_handles();
+
+            if !local_terminal_handles.is_empty() {
+                menu_options.push(ContextMenuItem::Separator)
+            }
+
+            for local_terminal_handle in local_terminal_handles {
+                if let Some(terminal) = local_terminal_handle.upgrade(cx) {
+                    menu_options.push(ContextMenuItem::item(
+                        terminal.read(cx).title(),
+                        FocusTerminal {
+                            terminal_handle: local_terminal_handle.clone(),
+                        },
+                    ))
+                }
+            }
+        }
+
+        self.popup_menu.update(cx, |menu, cx| {
+            menu.show(
+                Default::default(),
+                AnchorCorner::BottomRight,
+                menu_options,
+                cx,
+            );
+        });
+    }
+
+    pub fn focus_terminal(&mut self, action: &FocusTerminal, cx: &mut ViewContext<Self>) {
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            workspace.update(cx, |workspace, cx| {
+                let terminal = workspace
+                    .items_of_type::<TerminalView>(cx)
+                    .find(|terminal| {
+                        terminal.read(cx).model().downgrade() == action.terminal_handle
+                    });
+                if let Some(terminal) = terminal {
+                    workspace.activate_item(&terminal, cx);
+                }
+            });
+        }
+    }
+}
+
+impl StatusItemView for TerminalButton {
+    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        cx.notify();
+    }
+}

crates/terminal_view/src/terminal_element.rs 🔗

@@ -505,13 +505,22 @@ impl TerminalElement {
 
     ///Configures a text style from the current settings.
     pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
-        // Pull the font family from settings properly overriding
-        let family_id = settings
+        let font_family_name = settings
             .terminal_overrides
             .font_family
             .as_ref()
             .or(settings.terminal_defaults.font_family.as_ref())
-            .and_then(|family_name| font_cache.load_family(&[family_name]).log_err())
+            .unwrap_or(&settings.buffer_font_family_name);
+        let font_features = settings
+            .terminal_overrides
+            .font_features
+            .as_ref()
+            .or(settings.terminal_defaults.font_features.as_ref())
+            .unwrap_or(&settings.buffer_font_features);
+
+        let family_id = font_cache
+            .load_family(&[font_family_name], &font_features)
+            .log_err()
             .unwrap_or(settings.buffer_font_family);
 
         let font_size = settings
@@ -720,7 +729,7 @@ impl Element for TerminalElement {
         cx.paint_layer(clip_bounds, |cx| {
             let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
 
-            //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
+            // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
             self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
 
             cx.scene.push_cursor_region(gpui::CursorRegion {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -1,4 +1,5 @@
 mod persistence;
+pub mod terminal_button;
 pub mod terminal_element;
 
 use std::{
@@ -21,7 +22,7 @@ use gpui::{
 use project::{LocalWorktree, Project};
 use serde::Deserialize;
 use settings::{Settings, TerminalBlink, WorkingDirectory};
-use smallvec::SmallVec;
+use smallvec::{smallvec, SmallVec};
 use smol::Timer;
 use terminal::{
     alacritty_terminal::{
@@ -30,7 +31,7 @@ use terminal::{
     },
     Event, Terminal,
 };
-use util::{truncate_and_trailoff, ResultExt};
+use util::ResultExt;
 use workspace::{
     item::{Item, ItemEvent},
     notifications::NotifyResultExt,
@@ -177,8 +178,8 @@ impl TerminalView {
         }
     }
 
-    pub fn handle(&self) -> ModelHandle<Terminal> {
-        self.terminal.clone()
+    pub fn model(&self) -> &ModelHandle<Terminal> {
+        &self.terminal
     }
 
     pub fn has_new_content(&self) -> bool {
@@ -469,53 +470,50 @@ impl View for TerminalView {
         let mut context = Self::default_keymap_context();
 
         let mode = self.terminal.read(cx).last_content.mode;
-        context.map.insert(
-            "screen".to_string(),
-            (if mode.contains(TermMode::ALT_SCREEN) {
+        context.add_key(
+            "screen",
+            if mode.contains(TermMode::ALT_SCREEN) {
                 "alt"
             } else {
                 "normal"
-            })
-            .to_string(),
+            },
         );
 
         if mode.contains(TermMode::APP_CURSOR) {
-            context.set.insert("DECCKM".to_string());
+            context.add_identifier("DECCKM");
         }
         if mode.contains(TermMode::APP_KEYPAD) {
-            context.set.insert("DECPAM".to_string());
-        }
-        //Note the ! here
-        if !mode.contains(TermMode::APP_KEYPAD) {
-            context.set.insert("DECPNM".to_string());
+            context.add_identifier("DECPAM");
+        } else {
+            context.add_identifier("DECPNM");
         }
         if mode.contains(TermMode::SHOW_CURSOR) {
-            context.set.insert("DECTCEM".to_string());
+            context.add_identifier("DECTCEM");
         }
         if mode.contains(TermMode::LINE_WRAP) {
-            context.set.insert("DECAWM".to_string());
+            context.add_identifier("DECAWM");
         }
         if mode.contains(TermMode::ORIGIN) {
-            context.set.insert("DECOM".to_string());
+            context.add_identifier("DECOM");
         }
         if mode.contains(TermMode::INSERT) {
-            context.set.insert("IRM".to_string());
+            context.add_identifier("IRM");
         }
         //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
         if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
-            context.set.insert("LNM".to_string());
+            context.add_identifier("LNM");
         }
         if mode.contains(TermMode::FOCUS_IN_OUT) {
-            context.set.insert("report_focus".to_string());
+            context.add_identifier("report_focus");
         }
         if mode.contains(TermMode::ALTERNATE_SCROLL) {
-            context.set.insert("alternate_scroll".to_string());
+            context.add_identifier("alternate_scroll");
         }
         if mode.contains(TermMode::BRACKETED_PASTE) {
-            context.set.insert("bracketed_paste".to_string());
+            context.add_identifier("bracketed_paste");
         }
         if mode.intersects(TermMode::MOUSE_MODE) {
-            context.set.insert("any_mouse_reporting".to_string());
+            context.add_identifier("any_mouse_reporting");
         }
         {
             let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
@@ -527,9 +525,7 @@ impl View for TerminalView {
             } else {
                 "off"
             };
-            context
-                .map
-                .insert("mouse_reporting".to_string(), mouse_reporting.to_string());
+            context.add_key("mouse_reporting", mouse_reporting);
         }
         {
             let format = if mode.contains(TermMode::SGR_MOUSE) {
@@ -539,9 +535,7 @@ impl View for TerminalView {
             } else {
                 "normal"
             };
-            context
-                .map
-                .insert("mouse_format".to_string(), format.to_string());
+            context.add_key("mouse_format", format);
         }
         context
     }
@@ -554,46 +548,20 @@ impl Item for TerminalView {
         tab_theme: &theme::Tab,
         cx: &gpui::AppContext,
     ) -> ElementBox {
-        let title = self
-            .terminal()
-            .read(cx)
-            .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());
+        let title = self.terminal().read(cx).title();
 
         Flex::row()
             .with_child(
-                Label::new(title, tab_theme.label.clone())
+                gpui::elements::Svg::new("icons/terminal_12.svg")
+                    .with_color(tab_theme.label.text.color)
+                    .constrained()
+                    .with_width(tab_theme.type_icon_width)
                     .aligned()
                     .contained()
+                    .with_margin_right(tab_theme.spacing)
                     .boxed(),
             )
+            .with_child(Label::new(title, tab_theme.label.clone()).aligned().boxed())
             .boxed()
     }
 
@@ -616,43 +584,6 @@ impl Item for TerminalView {
         None
     }
 
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
-
-    fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
-        false
-    }
-
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
-    fn can_save(&self, _cx: &gpui::AppContext) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save should not have been called");
-    }
-
-    fn save_as(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _abs_path: std::path::PathBuf,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save_as should not have been called");
-    }
-
-    fn reload(
-        &mut self,
-        _project: gpui::ModelHandle<Project>,
-        _cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        gpui::Task::ready(Ok(()))
-    }
-
     fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
         self.has_bell()
     }
@@ -667,10 +598,10 @@ impl Item for TerminalView {
 
     fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
         match event {
-            Event::BreadcrumbsChanged => smallvec::smallvec![ItemEvent::UpdateBreadcrumbs],
-            Event::TitleChanged | Event::Wakeup => smallvec::smallvec![ItemEvent::UpdateTab],
-            Event::CloseTerminal => smallvec::smallvec![ItemEvent::CloseItem],
-            _ => smallvec::smallvec![],
+            Event::BreadcrumbsChanged => smallvec![ItemEvent::UpdateBreadcrumbs],
+            Event::TitleChanged | Event::Wakeup => smallvec![ItemEvent::UpdateTab],
+            Event::CloseTerminal => smallvec![ItemEvent::CloseItem],
+            _ => smallvec![],
         }
     }
 
@@ -680,8 +611,8 @@ impl Item for TerminalView {
 
     fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
         Some(vec![Text::new(
-            self.terminal().read(cx).breadcrumb_text.to_string(),
-            theme.breadcrumbs.text.clone(),
+            self.terminal().read(cx).breadcrumb_text.clone(),
+            theme.workspace.breadcrumbs.default.text.clone(),
         )
         .boxed()])
     }
@@ -692,7 +623,7 @@ impl Item for TerminalView {
 
     fn deserialize(
         project: ModelHandle<Project>,
-        _workspace: WeakViewHandle<Workspace>,
+        workspace: WeakViewHandle<Workspace>,
         workspace_id: workspace::WorkspaceId,
         item_id: workspace::ItemId,
         cx: &mut ViewContext<Pane>,
@@ -702,14 +633,25 @@ impl Item for TerminalView {
             let cwd = TERMINAL_DB
                 .get_working_directory(item_id, workspace_id)
                 .log_err()
-                .flatten();
+                .flatten()
+                .or_else(|| {
+                    cx.read(|cx| {
+                        let strategy = cx.global::<Settings>().terminal_strategy();
+                        workspace
+                            .upgrade(cx)
+                            .map(|workspace| {
+                                get_working_directory(workspace.read(cx), cx, strategy)
+                            })
+                            .flatten()
+                    })
+                });
 
             cx.update(|cx| {
                 let terminal = project.update(cx, |project, cx| {
                     project.create_terminal(cwd, window_id, cx)
                 })?;
 
-                Ok(cx.add_view(pane, |cx| TerminalView::new(terminal, workspace_id, cx)))
+                Ok(cx.add_view(&pane, |cx| TerminalView::new(terminal, workspace_id, cx)))
             })
         })
     }
@@ -1009,15 +951,7 @@ mod tests {
         let params = cx.update(AppState::test);
 
         let project = Project::test(params.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         (project, workspace)
     }

crates/text/Cargo.toml 🔗

@@ -22,7 +22,7 @@ digest = { version = "0.9", features = ["std"] }
 lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = { version = "0.8.3", optional = true }
 smallvec = { version = "1.6", features = ["union"] }
 util = { path = "../util" }

crates/text/src/text.rs 🔗

@@ -1591,6 +1591,14 @@ impl BufferSnapshot {
         self.text_for_range(range).flat_map(str::chars)
     }
 
+    pub fn reversed_chars_for_range<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> impl Iterator<Item = char> + '_ {
+        self.reversed_chunks_in_range(range)
+            .flat_map(|chunk| chunk.chars().rev())
+    }
+
     pub fn contains_str_at<T>(&self, position: T, needle: &str) -> bool
     where
         T: ToOffset,

crates/theme/Cargo.toml 🔗

@@ -13,7 +13,8 @@ gpui = { path = "../gpui" }
 anyhow = "1.0.38"
 indexmap = "1.6.2"
 parking_lot = "0.11.1"
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 serde_path_to_error = "0.1.4"
 toml = "0.5"

crates/theme/src/theme.rs 🔗

@@ -9,6 +9,9 @@ use gpui::{
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use std::{collections::HashMap, sync::Arc};
+use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
+
+pub mod ui;
 
 pub use theme_registry::*;
 
@@ -20,6 +23,7 @@ pub struct Theme {
     pub context_menu: ContextMenu,
     pub contacts_popover: ContactsPopover,
     pub contact_list: ContactList,
+    pub copilot: Copilot,
     pub contact_finder: ContactFinder,
     pub project_panel: ProjectPanel,
     pub command_palette: CommandPalette,
@@ -27,7 +31,6 @@ pub struct Theme {
     pub editor: Editor,
     pub search: Search,
     pub project_diagnostics: ProjectDiagnostics,
-    pub breadcrumbs: ContainedText,
     pub shared_screen: ContainerStyle,
     pub contact_notification: ContactNotification,
     pub update_notification: UpdateNotification,
@@ -37,6 +40,7 @@ pub struct Theme {
     pub tooltip: TooltipStyle,
     pub terminal: TerminalStyle,
     pub feedback: FeedbackStyle,
+    pub welcome: WelcomeStyle,
     pub color_scheme: ColorScheme,
 }
 
@@ -49,6 +53,7 @@ pub struct ThemeMeta {
 #[derive(Deserialize, Default)]
 pub struct Workspace {
     pub background: Color,
+    pub blank_pane: BlankPaneStyle,
     pub titlebar: Titlebar,
     pub tab_bar: TabBar,
     pub pane_divider: Border,
@@ -57,6 +62,8 @@ pub struct Workspace {
     pub sidebar: Sidebar,
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
+    pub breadcrumb_height: f32,
+    pub breadcrumbs: Interactive<ContainedText>,
     pub disconnected_overlay: ContainedText,
     pub modal: ContainerStyle,
     pub notification: ContainerStyle,
@@ -68,34 +75,100 @@ pub struct Workspace {
     pub drop_target_overlay_color: Color,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct BlankPaneStyle {
+    pub logo: SvgStyle,
+    pub logo_shadow: SvgStyle,
+    pub logo_container: ContainerStyle,
+    pub keyboard_hints: ContainerStyle,
+    pub keyboard_hint: Interactive<ContainedText>,
+    pub keyboard_hint_width: f32,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct Titlebar {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub height: f32,
     pub title: TextStyle,
-    pub avatar_width: f32,
-    pub avatar_margin: f32,
+    pub item_spacing: f32,
+    pub face_pile_spacing: f32,
     pub avatar_ribbon: AvatarRibbon,
+    pub follower_avatar_overlap: f32,
+    pub leader_selection: ContainerStyle,
     pub offline_icon: OfflineIcon,
-    pub avatar: ImageStyle,
-    pub inactive_avatar: ImageStyle,
+    pub leader_avatar: AvatarStyle,
+    pub follower_avatar: AvatarStyle,
+    pub inactive_avatar_grayscale: bool,
     pub sign_in_prompt: Interactive<ContainedText>,
     pub outdated_warning: ContainedText,
     pub share_button: Interactive<ContainedText>,
     pub call_control: Interactive<IconButton>,
     pub toggle_contacts_button: Interactive<IconButton>,
+    pub user_menu_button: Interactive<IconButton>,
     pub toggle_contacts_badge: ContainerStyle,
 }
 
+#[derive(Copy, Clone, Deserialize, Default)]
+pub struct AvatarStyle {
+    #[serde(flatten)]
+    pub image: ImageStyle,
+    pub outer_width: f32,
+    pub outer_corner_radius: f32,
+}
+
+#[derive(Deserialize, Default, Clone)]
+pub struct Copilot {
+    pub out_link_icon: Interactive<IconStyle>,
+    pub modal: ModalStyle,
+    pub auth: CopilotAuth,
+}
+
+#[derive(Deserialize, Default, Clone)]
+pub struct CopilotAuth {
+    pub content_width: f32,
+    pub prompting: CopilotAuthPrompting,
+    pub not_authorized: CopilotAuthNotAuthorized,
+    pub authorized: CopilotAuthAuthorized,
+    pub cta_button: ButtonStyle,
+    pub header: IconStyle,
+}
+
+#[derive(Deserialize, Default, Clone)]
+pub struct CopilotAuthPrompting {
+    pub subheading: ContainedText,
+    pub hint: ContainedText,
+    pub device_code: DeviceCode,
+}
+
+#[derive(Deserialize, Default, Clone)]
+pub struct DeviceCode {
+    pub text: TextStyle,
+    pub cta: ButtonStyle,
+    pub left: f32,
+    pub left_container: ContainerStyle,
+    pub right: f32,
+    pub right_container: Interactive<ContainerStyle>,
+}
+
+#[derive(Deserialize, Default, Clone)]
+pub struct CopilotAuthNotAuthorized {
+    pub subheading: ContainedText,
+    pub warning: ContainedText,
+}
+
+#[derive(Deserialize, Default, Clone)]
+pub struct CopilotAuthAuthorized {
+    pub subheading: ContainedText,
+    pub hint: ContainedText,
+}
+
 #[derive(Deserialize, Default)]
 pub struct ContactsPopover {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub height: f32,
     pub width: f32,
-    pub invite_row_height: f32,
-    pub invite_row: Interactive<ContainedLabel>,
 }
 
 #[derive(Deserialize, Default)]
@@ -203,7 +276,8 @@ pub struct Tab {
     pub label: LabelStyle,
     pub description: ContainedText,
     pub spacing: f32,
-    pub icon_width: f32,
+    pub close_icon_width: f32,
+    pub type_icon_width: f32,
     pub icon_close: Color,
     pub icon_close_active: Color,
     pub icon_dirty: Color,
@@ -246,8 +320,6 @@ pub struct Search {
     pub match_background: Color,
     pub match_index: ContainedText,
     pub results_status: TextStyle,
-    pub tab_icon_width: f32,
-    pub tab_icon_spacing: f32,
     pub dismiss_button: Interactive<IconButton>,
 }
 
@@ -266,10 +338,10 @@ pub struct StatusBar {
     pub height: f32,
     pub item_spacing: f32,
     pub cursor_position: TextStyle,
+    pub active_language: Interactive<ContainedText>,
     pub auto_update_progress_message: TextStyle,
     pub auto_update_done_message: TextStyle,
     pub lsp_status: Interactive<StatusBarLspStatus>,
-    pub feedback: Interactive<TextStyle>,
     pub sidebar_buttons: StatusBarSidebarButtons,
     pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
     pub diagnostic_message: Interactive<ContainedText>,
@@ -316,12 +388,13 @@ pub struct Sidebar {
     pub container: ContainerStyle,
 }
 
-#[derive(Clone, Copy, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default)]
 pub struct SidebarItem {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub icon_color: Color,
     pub icon_size: f32,
+    pub label: ContainedText,
 }
 
 #[derive(Deserialize, Default)]
@@ -334,6 +407,7 @@ pub struct ProjectPanel {
     pub cut_entry: Interactive<ProjectPanelEntry>,
     pub filename_editor: FieldEditor,
     pub indent_width: f32,
+    pub open_project_button: Interactive<ContainedText>,
 }
 
 #[derive(Clone, Debug, Deserialize, Default)]
@@ -381,7 +455,7 @@ pub struct InviteLink {
     pub icon: Icon,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Clone, Copy, Default)]
 pub struct Icon {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -540,6 +614,7 @@ pub struct Editor {
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
     pub syntax: Arc<SyntaxTheme>,
+    pub suggestion: HighlightStyle,
     pub diagnostic_path_header: DiagnosticPathHeader,
     pub diagnostic_header: DiagnosticHeader,
     pub error_diagnostic: DiagnosticStyle,
@@ -552,6 +627,7 @@ pub struct Editor {
     pub invalid_hint_diagnostic: DiagnosticStyle,
     pub autocomplete: AutocompleteStyle,
     pub code_actions: CodeActions,
+    pub folds: Folds,
     pub unnecessary_code_fade: f32,
     pub hover_popover: HoverPopover,
     pub link_definition: HighlightStyle,
@@ -621,13 +697,35 @@ pub struct FieldEditor {
     pub selection: SelectionStyle,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct InteractiveColor {
+    pub color: Color,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct CodeActions {
     #[serde(default)]
-    pub indicator: Color,
+    pub indicator: Interactive<InteractiveColor>,
     pub vertical_scale: f32,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct Folds {
+    pub indicator: Interactive<InteractiveColor>,
+    pub ellipses: FoldEllipses,
+    pub fold_background: Color,
+    pub icon_margin_scale: f32,
+    pub folded_icon: String,
+    pub foldable_icon: String,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct FoldEllipses {
+    pub text_color: Color,
+    pub background: Interactive<InteractiveColor>,
+    pub corner_radius_factor: f32,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct DiffStyle {
     pub inserted: Color,
@@ -642,7 +740,9 @@ pub struct DiffStyle {
 pub struct Interactive<T> {
     pub default: T,
     pub hover: Option<T>,
+    pub hover_and_active: Option<T>,
     pub clicked: Option<T>,
+    pub click_and_active: Option<T>,
     pub active: Option<T>,
     pub disabled: Option<T>,
 }
@@ -650,7 +750,17 @@ pub struct Interactive<T> {
 impl<T> Interactive<T> {
     pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
         if active {
-            self.active.as_ref().unwrap_or(&self.default)
+            if state.hovered() {
+                self.hover_and_active
+                    .as_ref()
+                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
+            } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
+                self.click_and_active
+                    .as_ref()
+                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
+            } else {
+                self.active.as_ref().unwrap_or(&self.default)
+            }
         } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
             self.clicked.as_ref().unwrap()
         } else if state.hovered() {
@@ -675,7 +785,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
             #[serde(flatten)]
             default: Value,
             hover: Option<Value>,
+            hover_and_active: Option<Value>,
             clicked: Option<Value>,
+            click_and_active: Option<Value>,
             active: Option<Value>,
             disabled: Option<Value>,
         }
@@ -702,7 +814,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
         };
 
         let hover = deserialize_state(json.hover)?;
+        let hover_and_active = deserialize_state(json.hover_and_active)?;
         let clicked = deserialize_state(json.clicked)?;
+        let click_and_active = deserialize_state(json.click_and_active)?;
         let active = deserialize_state(json.active)?;
         let disabled = deserialize_state(json.disabled)?;
         let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
@@ -710,7 +824,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
         Ok(Interactive {
             default,
             hover,
+            hover_and_active,
             clicked,
+            click_and_active,
             active,
             disabled,
         })
@@ -811,16 +927,30 @@ pub struct TerminalStyle {
 pub struct FeedbackStyle {
     pub submit_button: Interactive<ContainedText>,
     pub button_margin: f32,
-    pub info_text: ContainedText,
+    pub info_text_default: ContainedText,
+    pub link_text_default: ContainedText,
+    pub link_text_hover: ContainedText,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct WelcomeStyle {
+    pub page_width: f32,
+    pub logo: SvgStyle,
+    pub logo_subheading: ContainedText,
+    pub usage_note: ContainedText,
+    pub checkbox: CheckboxStyle,
+    pub checkbox_container: ContainerStyle,
+    pub button: Interactive<ContainedText>,
+    pub button_group: ContainerStyle,
+    pub heading_group: ContainerStyle,
+    pub checkbox_group: ContainerStyle,
 }
 
 #[derive(Clone, Deserialize, Default)]
 pub struct ColorScheme {
     pub name: String,
     pub is_light: bool,
-
     pub ramps: RampSet,
-
     pub lowest: Layer,
     pub middle: Layer,
     pub highest: Layer,

crates/theme/src/theme_registry.rs 🔗

@@ -2,6 +2,7 @@ use crate::{Theme, ThemeMeta};
 use anyhow::{Context, Result};
 use gpui::{fonts, AssetSource, FontCache};
 use parking_lot::Mutex;
+use serde::Deserialize;
 use serde_json::Value;
 use std::{collections::HashMap, sync::Arc};
 
@@ -56,12 +57,16 @@ impl ThemeRegistry {
             .with_context(|| format!("failed to load theme file {}", asset_path))?;
 
         // Allocate into the heap directly, the Theme struct is too large to fit in the stack.
-        let mut theme: Arc<Theme> = fonts::with_font_cache(self.font_cache.clone(), || {
-            serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json))
+        let mut theme = fonts::with_font_cache(self.font_cache.clone(), || {
+            let mut theme = Box::new(Theme::default());
+            let mut deserializer = serde_json::Deserializer::from_slice(&theme_json);
+            let result = Theme::deserialize_in_place(&mut deserializer, &mut theme);
+            result.map(|_| theme)
         })?;
 
         // Reset name to be the file path, so that we can use it to access the stored themes
-        Arc::get_mut(&mut theme).unwrap().meta.name = name.into();
+        theme.meta.name = name.into();
+        let theme: Arc<Theme> = theme.into();
         self.themes.lock().insert(name.to_string(), theme.clone());
         Ok(theme)
     }

crates/theme/src/ui.rs 🔗

@@ -0,0 +1,290 @@
+use std::borrow::Cow;
+
+use gpui::{
+    color::Color,
+    elements::{
+        ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
+        MouseEventHandler, ParentElement, Stack, Svg,
+    },
+    fonts::TextStyle,
+    geometry::vector::{vec2f, Vector2F},
+    scene::MouseClick,
+    Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View,
+};
+use serde::Deserialize;
+
+use crate::{ContainedText, Interactive};
+
+#[derive(Clone, Deserialize, Default)]
+pub struct CheckboxStyle {
+    pub icon: SvgStyle,
+    pub label: ContainedText,
+    pub default: ContainerStyle,
+    pub checked: ContainerStyle,
+    pub hovered: ContainerStyle,
+    pub hovered_and_checked: ContainerStyle,
+}
+
+pub fn checkbox<T: 'static, V: View>(
+    label: &'static str,
+    style: &CheckboxStyle,
+    checked: bool,
+    cx: &mut RenderContext<V>,
+    change: fn(checked: bool, cx: &mut EventContext) -> (),
+) -> MouseEventHandler<T> {
+    let label = Label::new(label, style.label.text.clone())
+        .contained()
+        .with_style(style.label.container)
+        .boxed();
+
+    checkbox_with_label(label, style, checked, cx, change)
+}
+
+pub fn checkbox_with_label<T: 'static, V: View>(
+    label: ElementBox,
+    style: &CheckboxStyle,
+    checked: bool,
+    cx: &mut RenderContext<V>,
+    change: fn(checked: bool, cx: &mut EventContext) -> (),
+) -> MouseEventHandler<T> {
+    MouseEventHandler::<T>::new(0, cx, |state, _| {
+        let indicator = if checked {
+            svg(&style.icon)
+        } else {
+            Empty::new()
+                .constrained()
+                .with_width(style.icon.dimensions.width)
+                .with_height(style.icon.dimensions.height)
+        };
+
+        Flex::row()
+            .with_children([
+                indicator
+                    .contained()
+                    .with_style(if checked {
+                        if state.hovered() {
+                            style.hovered_and_checked
+                        } else {
+                            style.checked
+                        }
+                    } else {
+                        if state.hovered() {
+                            style.hovered
+                        } else {
+                            style.default
+                        }
+                    })
+                    .boxed(),
+                label,
+            ])
+            .align_children_center()
+            .boxed()
+    })
+    .on_click(gpui::MouseButton::Left, move |_, cx| change(!checked, cx))
+    .with_cursor_style(gpui::CursorStyle::PointingHand)
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct SvgStyle {
+    pub color: Color,
+    pub asset: String,
+    pub dimensions: Dimensions,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct Dimensions {
+    pub width: f32,
+    pub height: f32,
+}
+
+impl Dimensions {
+    pub fn to_vec(&self) -> Vector2F {
+        vec2f(self.width, self.height)
+    }
+}
+
+pub fn svg(style: &SvgStyle) -> ConstrainedBox {
+    Svg::new(style.asset.clone())
+        .with_color(style.color)
+        .constrained()
+        .with_width(style.dimensions.width)
+        .with_height(style.dimensions.height)
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct IconStyle {
+    icon: SvgStyle,
+    container: ContainerStyle,
+}
+
+pub fn icon(style: &IconStyle) -> Container {
+    svg(&style.icon).contained().with_style(style.container)
+}
+
+pub fn keystroke_label<V: View>(
+    label_text: &'static str,
+    label_style: &ContainedText,
+    keystroke_style: &ContainedText,
+    action: Box<dyn Action>,
+    cx: &mut RenderContext<V>,
+) -> Container {
+    // FIXME: Put the theme in it's own global so we can
+    // query the keystroke style on our own
+    keystroke_label_for(
+        cx.window_id(),
+        cx.handle().id(),
+        label_text,
+        label_style,
+        keystroke_style,
+        action,
+    )
+}
+
+pub fn keystroke_label_for(
+    window_id: usize,
+    view_id: usize,
+    label_text: &'static str,
+    label_style: &ContainedText,
+    keystroke_style: &ContainedText,
+    action: Box<dyn Action>,
+) -> Container {
+    Flex::row()
+        .with_child(
+            Label::new(label_text, label_style.text.clone())
+                .contained()
+                .boxed(),
+        )
+        .with_child({
+            KeystrokeLabel::new(
+                window_id,
+                view_id,
+                action,
+                keystroke_style.container,
+                keystroke_style.text.clone(),
+            )
+            .flex_float()
+            .boxed()
+        })
+        .contained()
+        .with_style(label_style.container)
+}
+
+pub type ButtonStyle = Interactive<ContainedText>;
+
+pub fn cta_button<L, A, V>(
+    label: L,
+    action: A,
+    max_width: f32,
+    style: &ButtonStyle,
+    cx: &mut RenderContext<V>,
+) -> ElementBox
+where
+    L: Into<Cow<'static, str>>,
+    A: 'static + Action + Clone,
+    V: View,
+{
+    cta_button_with_click(label, max_width, style, cx, move |_, cx| {
+        cx.dispatch_action(action.clone())
+    })
+    .boxed()
+}
+
+pub fn cta_button_with_click<L, V, F>(
+    label: L,
+    max_width: f32,
+    style: &ButtonStyle,
+    cx: &mut RenderContext<V>,
+    f: F,
+) -> MouseEventHandler<F>
+where
+    L: Into<Cow<'static, str>>,
+    V: View,
+    F: Fn(MouseClick, &mut EventContext) + 'static,
+{
+    MouseEventHandler::<F>::new(0, cx, |state, _| {
+        let style = style.style_for(state, false);
+        Label::new(label, style.text.to_owned())
+            .aligned()
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_max_width(max_width)
+            .boxed()
+    })
+    .on_click(MouseButton::Left, f)
+    .with_cursor_style(gpui::CursorStyle::PointingHand)
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct ModalStyle {
+    close_icon: Interactive<IconStyle>,
+    container: ContainerStyle,
+    titlebar: ContainerStyle,
+    title_text: Interactive<TextStyle>,
+    dimensions: Dimensions,
+}
+
+impl ModalStyle {
+    pub fn dimensions(&self) -> Vector2F {
+        self.dimensions.to_vec()
+    }
+}
+
+pub fn modal<V, I, F>(
+    title: I,
+    style: &ModalStyle,
+    cx: &mut RenderContext<V>,
+    build_modal: F,
+) -> ElementBox
+where
+    V: View,
+    I: Into<Cow<'static, str>>,
+    F: FnOnce(&mut gpui::RenderContext<V>) -> ElementBox,
+{
+    const TITLEBAR_HEIGHT: f32 = 28.;
+    // let active = cx.window_is_active(cx.window_id());
+
+    Flex::column()
+        .with_child(
+            Stack::new()
+                .with_children([
+                    Label::new(
+                        title,
+                        style
+                            .title_text
+                            .style_for(&mut MouseState::default(), false)
+                            .clone(),
+                    )
+                    .boxed(),
+                    // FIXME: Get a better tag type
+                    MouseEventHandler::<V>::new(999999, cx, |state, _cx| {
+                        let style = style.close_icon.style_for(state, false);
+                        icon(style).boxed()
+                    })
+                    .on_click(gpui::MouseButton::Left, move |_, cx| {
+                        let window_id = cx.window_id();
+                        cx.remove_window(window_id);
+                    })
+                    .with_cursor_style(gpui::CursorStyle::PointingHand)
+                    .aligned()
+                    .right()
+                    .boxed(),
+                ])
+                .contained()
+                .with_style(style.titlebar)
+                .constrained()
+                .with_height(TITLEBAR_HEIGHT)
+                .boxed(),
+        )
+        .with_child(
+            Container::new(build_modal(cx))
+                .with_style(style.container)
+                .constrained()
+                .with_width(style.dimensions().x())
+                .with_height(style.dimensions().y() - TITLEBAR_HEIGHT)
+                .boxed(),
+        )
+        .constrained()
+        .with_height(style.dimensions().y())
+        .boxed()
+}

crates/theme_selector/Cargo.toml 🔗

@@ -19,6 +19,5 @@ workspace = { path = "../workspace" }
 util = { path = "../util" }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 smol = "1.2.5"
-

crates/theme_selector/src/theme_selector.rs 🔗

@@ -47,12 +47,7 @@ impl ThemeSelector {
         let mut theme_names = registry
             .list(**cx.default_global::<StaffMode>())
             .collect::<Vec<_>>();
-        theme_names.sort_unstable_by(|a, b| {
-            a.is_light
-                .cmp(&b.is_light)
-                .reverse()
-                .then(a.name.cmp(&b.name))
-        });
+        theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
         let matches = theme_names
             .iter()
             .map(|meta| StringMatch {
@@ -261,7 +256,7 @@ impl View for ThemeSelector {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.picker.clone(), cx).boxed()
+        ChildView::new(&self.picker, cx).boxed()
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {

crates/theme_testbench/src/theme_testbench.rs 🔗

@@ -11,12 +11,8 @@ use gpui::{
 };
 use project::Project;
 use settings::Settings;
-use smallvec::SmallVec;
 use theme::{ColorScheme, Layer, Style, StyleSet};
-use workspace::{
-    item::{Item, ItemEvent},
-    register_deserializable_item, Pane, Workspace,
-};
+use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
 
 actions!(theme, [DeployThemeTestbench]);
 
@@ -308,53 +304,12 @@ impl Item for ThemeTestbench {
         style: &theme::Tab,
         _: &AppContext,
     ) -> gpui::ElementBox {
-        Label::new("Theme Testbench".into(), style.label.clone())
+        Label::new("Theme Testbench", style.label.clone())
             .aligned()
             .contained()
             .boxed()
     }
 
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
-
-    fn is_singleton(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
-    fn can_save(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _: gpui::ModelHandle<Project>,
-        _: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save should not have been called");
-    }
-
-    fn save_as(
-        &mut self,
-        _: gpui::ModelHandle<Project>,
-        _: std::path::PathBuf,
-        _: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        unreachable!("save_as should not have been called");
-    }
-
-    fn reload(
-        &mut self,
-        _: gpui::ModelHandle<Project>,
-        _: &mut ViewContext<Self>,
-    ) -> gpui::Task<gpui::anyhow::Result<()>> {
-        gpui::Task::ready(Ok(()))
-    }
-
-    fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-        SmallVec::new()
-    }
-
     fn serialized_item_kind() -> Option<&'static str> {
         Some("ThemeTestBench")
     }

crates/util/Cargo.toml 🔗

@@ -6,24 +6,27 @@ publish = false
 
 [lib]
 path = "src/util.rs"
-doctest = false
+doctest = true
 
 [features]
-test-support = ["serde_json", "tempdir", "git2"]
+test-support = ["tempdir", "git2"]
 
 [dependencies]
 anyhow = "1.0.38"
 backtrace = "0.3"
-futures = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 lazy_static = "1.4.0"
+futures = "0.3"
+isahc = "1.7"
+smol = "1.2.5"
+url = "2.2"
 rand = { workspace = true }
 tempdir = { version = "0.3.7", optional = true }
-serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
 git2 = { version = "0.15", default-features = false, optional = true }
 dirs = "3.0"
 
 [dev-dependencies]
 tempdir = { version = "0.3.7" }
-serde_json = { version = "1.0", features = ["preserve_order"] }
 git2 = { version = "0.15", default-features = false }

crates/util/src/fs.rs 🔗

@@ -0,0 +1,28 @@
+use std::path::Path;
+
+use smol::{fs, stream::StreamExt};
+
+use crate::ResultExt;
+
+// Removes all files and directories matching the given predicate
+pub async fn remove_matching<F>(dir: &Path, predicate: F)
+where
+    F: Fn(&Path) -> bool,
+{
+    if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
+        while let Some(entry) = entries.next().await {
+            if let Some(entry) = entry.log_err() {
+                let entry_path = entry.path();
+                if predicate(entry_path.as_path()) {
+                    if let Ok(metadata) = fs::metadata(&entry_path).await {
+                        if metadata.is_file() {
+                            fs::remove_file(&entry_path).await.log_err();
+                        } else {
+                            fs::remove_dir_all(&entry_path).await.log_err();
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

crates/util/src/github.rs 🔗

@@ -0,0 +1,46 @@
+use crate::http::HttpClient;
+use anyhow::{Context, Result};
+use futures::AsyncReadExt;
+use serde::Deserialize;
+use std::sync::Arc;
+
+pub struct GitHubLspBinaryVersion {
+    pub name: String,
+    pub url: String,
+}
+
+#[derive(Deserialize)]
+pub struct GithubRelease {
+    pub name: String,
+    pub assets: Vec<GithubReleaseAsset>,
+}
+
+#[derive(Deserialize)]
+pub struct GithubReleaseAsset {
+    pub name: String,
+    pub browser_download_url: String,
+}
+
+pub async fn latest_github_release(
+    repo_name_with_owner: &str,
+    http: Arc<dyn HttpClient>,
+) -> Result<GithubRelease, anyhow::Error> {
+    let mut response = http
+        .get(
+            &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"),
+            Default::default(),
+            true,
+        )
+        .await
+        .context("error fetching latest release")?;
+    let mut body = Vec::new();
+    response
+        .body_mut()
+        .read_to_end(&mut body)
+        .await
+        .context("error reading latest release")?;
+
+    let release: GithubRelease =
+        serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?;
+    Ok(release)
+}

crates/util/src/http.rs 🔗

@@ -0,0 +1,117 @@
+pub use anyhow::{anyhow, Result};
+use futures::future::BoxFuture;
+use isahc::config::{Configurable, RedirectPolicy};
+pub use isahc::{
+    http::{Method, Uri},
+    Error,
+};
+pub use isahc::{AsyncBody, Request, Response};
+use smol::future::FutureExt;
+#[cfg(feature = "test-support")]
+use std::fmt;
+use std::{sync::Arc, time::Duration};
+pub use url::Url;
+
+pub trait HttpClient: Send + Sync {
+    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
+
+    fn get<'a>(
+        &'a self,
+        uri: &str,
+        body: AsyncBody,
+        follow_redirects: bool,
+    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
+        let request = isahc::Request::builder()
+            .redirect_policy(if follow_redirects {
+                RedirectPolicy::Follow
+            } else {
+                RedirectPolicy::None
+            })
+            .method(Method::GET)
+            .uri(uri)
+            .body(body);
+        match request {
+            Ok(request) => self.send(request),
+            Err(error) => async move { Err(error.into()) }.boxed(),
+        }
+    }
+
+    fn post_json<'a>(
+        &'a self,
+        uri: &str,
+        body: AsyncBody,
+    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
+        let request = isahc::Request::builder()
+            .method(Method::POST)
+            .uri(uri)
+            .header("Content-Type", "application/json")
+            .body(body);
+        match request {
+            Ok(request) => self.send(request),
+            Err(error) => async move { Err(error.into()) }.boxed(),
+        }
+    }
+}
+
+pub fn client() -> Arc<dyn HttpClient> {
+    Arc::new(
+        isahc::HttpClient::builder()
+            .connect_timeout(Duration::from_secs(5))
+            .low_speed_timeout(100, Duration::from_secs(5))
+            .build()
+            .unwrap(),
+    )
+}
+
+impl HttpClient for isahc::HttpClient {
+    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
+        Box::pin(async move { self.send_async(req).await })
+    }
+}
+
+#[cfg(feature = "test-support")]
+pub struct FakeHttpClient {
+    handler: Box<
+        dyn 'static
+            + Send
+            + Sync
+            + Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>,
+    >,
+}
+
+#[cfg(feature = "test-support")]
+impl FakeHttpClient {
+    pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
+    where
+        Fut: 'static + Send + futures::Future<Output = Result<Response<AsyncBody>, Error>>,
+        F: 'static + Send + Sync + Fn(Request<AsyncBody>) -> Fut,
+    {
+        Arc::new(Self {
+            handler: Box::new(move |req| Box::pin(handler(req))),
+        })
+    }
+
+    pub fn with_404_response() -> Arc<dyn HttpClient> {
+        Self::create(|_| async move {
+            Ok(Response::builder()
+                .status(404)
+                .body(Default::default())
+                .unwrap())
+        })
+    }
+}
+
+#[cfg(feature = "test-support")]
+impl fmt::Debug for FakeHttpClient {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("FakeHttpClient").finish()
+    }
+}
+
+#[cfg(feature = "test-support")]
+impl HttpClient for FakeHttpClient {
+    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
+        let future = (self.handler)(req);
+        Box::pin(async move { future.await.map(Into::into) })
+    }
+}

crates/util/src/paths.rs 🔗

@@ -1,10 +1,12 @@
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 
 lazy_static::lazy_static! {
     pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
     pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
     pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
+    pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
     pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
+    pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot");
     pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db");
     pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
     pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
@@ -22,3 +24,49 @@ pub mod legacy {
         pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
     }
 }
+
+/// Compacts a given file path by replacing the user's home directory
+/// prefix with a tilde (`~`).
+///
+/// # Arguments
+///
+/// * `path` - A reference to a `Path` representing the file path to compact.
+///
+/// # Examples
+///
+/// ```
+/// use std::path::{Path, PathBuf};
+/// use util::paths::compact;
+/// let path: PathBuf = [
+///     util::paths::HOME.to_string_lossy().to_string(),
+///     "some_file.txt".to_string(),
+///  ]
+///  .iter()
+///  .collect();
+/// if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+///     assert_eq!(compact(&path).to_str(), Some("~/some_file.txt"));
+/// } else {
+///     assert_eq!(compact(&path).to_str(), path.to_str());
+/// }
+/// ```
+///
+/// # Returns
+///
+/// * A `PathBuf` containing the compacted file path. If the input path
+///   does not have the user's home directory prefix, or if we are not on
+///   Linux or macOS, the original path is returned unchanged.
+pub fn compact(path: &Path) -> PathBuf {
+    if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
+        match path.strip_prefix(HOME.as_path()) {
+            Ok(relative_path) => {
+                let mut shortened_path = PathBuf::new();
+                shortened_path.push("~");
+                shortened_path.push(relative_path);
+                shortened_path
+            }
+            Err(_) => path.to_path_buf(),
+        }
+    } else {
+        path.to_path_buf()
+    }
+}

crates/util/src/test/marked_text.rs 🔗

@@ -87,21 +87,21 @@ pub fn marked_text_ranges_by(
 /// 1. To mark a range of text, surround it with the `«` and `»` angle brackets,
 ///    which can be typed on a US keyboard with the `alt-|` and `alt-shift-|` keys.
 ///
-///    ```
+///    ```text
 ///    foo «selected text» bar
 ///    ```
 ///
 /// 2. To mark a single position in the text, use the `ˇ` caron,
 ///    which can be typed on a US keyboard with the `alt-shift-t` key.
 ///
-///    ```
+///    ```text
 ///    the cursors are hereˇ and hereˇ.
 ///    ```
 ///
 /// 3. To mark a range whose direction is meaningful (like a selection),
 ///    put a caron character beside one of its bounds, on the inside:
 ///
-///    ```
+///    ```text
 ///    one «ˇreversed» selection and one «forwardˇ» selection
 ///    ```
 pub fn marked_text_ranges(

crates/util/src/util.rs 🔗

@@ -1,4 +1,7 @@
 pub mod channel;
+pub mod fs;
+pub mod github;
+pub mod http;
 pub mod paths;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
@@ -83,6 +86,24 @@ where
     }
 }
 
+pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
+    use serde_json::Value;
+
+    match (source, target) {
+        (Value::Object(source), Value::Object(target)) => {
+            for (key, value) in source {
+                if let Some(target) = target.get_mut(&key) {
+                    merge_json_value_into(value, target);
+                } else {
+                    target.insert(key.clone(), value);
+                }
+            }
+        }
+
+        (source, target) => *target = source,
+    }
+}
+
 pub trait ResultExt {
     type Ok;
 
@@ -124,11 +145,15 @@ pub trait TryFutureExt {
     fn warn_on_err(self) -> LogErrorFuture<Self>
     where
         Self: Sized;
+    fn unwrap(self) -> UnwrapFuture<Self>
+    where
+        Self: Sized;
 }
 
-impl<F, T> TryFutureExt for F
+impl<F, T, E> TryFutureExt for F
 where
-    F: Future<Output = anyhow::Result<T>>,
+    F: Future<Output = Result<T, E>>,
+    E: std::fmt::Debug,
 {
     fn log_err(self) -> LogErrorFuture<Self>
     where
@@ -143,17 +168,25 @@ where
     {
         LogErrorFuture(self, log::Level::Warn)
     }
+
+    fn unwrap(self) -> UnwrapFuture<Self>
+    where
+        Self: Sized,
+    {
+        UnwrapFuture(self)
+    }
 }
 
 pub struct LogErrorFuture<F>(F, log::Level);
 
-impl<F, T> Future for LogErrorFuture<F>
+impl<F, T, E> Future for LogErrorFuture<F>
 where
-    F: Future<Output = anyhow::Result<T>>,
+    F: Future<Output = Result<T, E>>,
+    E: std::fmt::Debug,
 {
     type Output = Option<T>;
 
-    fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
         let level = self.1;
         let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
         match inner.poll(cx) {
@@ -169,6 +202,24 @@ where
     }
 }
 
+pub struct UnwrapFuture<F>(F);
+
+impl<F, T, E> Future for UnwrapFuture<F>
+where
+    F: Future<Output = Result<T, E>>,
+    E: std::fmt::Debug,
+{
+    type Output = T;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+        let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
+        match inner.poll(cx) {
+            Poll::Ready(result) => Poll::Ready(result.unwrap()),
+            Poll::Pending => Poll::Pending,
+        }
+    }
+}
+
 struct Defer<F: FnOnce()>(Option<F>);
 
 impl<F: FnOnce()> Drop for Defer<F> {
@@ -237,7 +288,7 @@ macro_rules! iife {
     };
 }
 
-/// Async lImmediately invoked function expression. Good for using the ? operator
+/// Async Immediately invoked function expression. Good for using the ? operator
 /// in functions which do not return an Option or Result. Async version of above
 #[macro_export]
 macro_rules! async_iife {
@@ -250,6 +301,7 @@ pub trait RangeExt<T> {
     fn sorted(&self) -> Self;
     fn to_inclusive(&self) -> RangeInclusive<T>;
     fn overlaps(&self, other: &Range<T>) -> bool;
+    fn contains_inclusive(&self, other: &Range<T>) -> bool;
 }
 
 impl<T: Ord + Clone> RangeExt<T> for Range<T> {
@@ -262,10 +314,11 @@ impl<T: Ord + Clone> RangeExt<T> for Range<T> {
     }
 
     fn overlaps(&self, other: &Range<T>) -> bool {
-        self.contains(&other.start)
-            || self.contains(&other.end)
-            || other.contains(&self.start)
-            || other.contains(&self.end)
+        self.start < other.end && other.start < self.end
+    }
+
+    fn contains_inclusive(&self, other: &Range<T>) -> bool {
+        self.start <= other.start && other.end <= self.end
     }
 }
 
@@ -279,10 +332,11 @@ impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
     }
 
     fn overlaps(&self, other: &Range<T>) -> bool {
-        self.contains(&other.start)
-            || self.contains(&other.end)
-            || other.contains(&self.start())
-            || other.contains(&self.end())
+        self.start() < &other.end && &other.start <= self.end()
+    }
+
+    fn contains_inclusive(&self, other: &Range<T>) -> bool {
+        self.start() <= &other.start && &other.end <= self.end()
     }
 }
 

crates/vim/Cargo.toml 🔗

@@ -12,7 +12,8 @@ doctest = false
 neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
 
 [dependencies]
-serde = { version = "1.0", features = ["derive", "rc"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
 itertools = "0.10"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 
@@ -20,7 +21,7 @@ async-compat = { version = "0.2.1", "optional" = true }
 async-trait = { version = "0.1", "optional" = true }
 nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
 tokio = { version = "1.15", "optional" = true }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 
 assets = { path = "../assets" }
 collections = { path = "../collections" }

crates/vim/src/motion.rs 🔗

@@ -36,6 +36,7 @@ pub enum Motion {
     Matching,
     FindForward { before: bool, text: Arc<str> },
     FindBackward { after: bool, text: Arc<str> },
+    NextLineStart,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -74,6 +75,7 @@ actions!(
         StartOfDocument,
         EndOfDocument,
         Matching,
+        NextLineStart,
     ]
 );
 impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
@@ -111,6 +113,7 @@ pub fn init(cx: &mut MutableAppContext) {
          &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
+    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
 }
 
 pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -138,15 +141,43 @@ pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
 impl Motion {
     pub fn linewise(&self) -> bool {
         use Motion::*;
-        matches!(
-            self,
-            Down | Up | StartOfDocument | EndOfDocument | CurrentLine
-        )
+        match self {
+            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
+            EndOfLine
+            | NextWordEnd { .. }
+            | Matching
+            | FindForward { .. }
+            | Left
+            | Backspace
+            | Right
+            | StartOfLine
+            | NextWordStart { .. }
+            | PreviousWordStart { .. }
+            | FirstNonWhitespace
+            | FindBackward { .. } => false,
+        }
     }
 
     pub fn infallible(&self) -> bool {
         use Motion::*;
-        matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
+        match self {
+            StartOfDocument | EndOfDocument | CurrentLine => true,
+            Down
+            | Up
+            | EndOfLine
+            | NextWordEnd { .. }
+            | Matching
+            | FindForward { .. }
+            | Left
+            | Backspace
+            | Right
+            | StartOfLine
+            | NextWordStart { .. }
+            | PreviousWordStart { .. }
+            | FirstNonWhitespace
+            | FindBackward { .. }
+            | NextLineStart => false,
+        }
     }
 
     pub fn inclusive(&self) -> bool {
@@ -160,7 +191,8 @@ impl Motion {
             | EndOfLine
             | NextWordEnd { .. }
             | Matching
-            | FindForward { .. } => true,
+            | FindForward { .. }
+            | NextLineStart => true,
             Left
             | Backspace
             | Right
@@ -214,6 +246,7 @@ impl Motion {
                 find_backward(map, point, *after, text.clone(), times),
                 SelectionGoal::None,
             ),
+            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
         };
 
         (new_point != point || infallible).then_some((new_point, goal))
@@ -543,3 +576,8 @@ fn find_backward(
         })
         .unwrap_or(from)
 }
+
+fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
+    let new_row = (point.row() + times as u32).min(map.max_buffer_row());
+    map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
+}

crates/vim/src/normal.rs 🔗

@@ -473,6 +473,7 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut MutableAppContext) {
 
 #[cfg(test)]
 mod test {
+    use gpui::TestAppContext;
     use indoc::indoc;
 
     use crate::{
@@ -515,15 +516,15 @@ mod test {
         .await;
     }
 
-    // #[gpui::test]
-    // async fn test_enter(cx: &mut gpui::TestAppContext) {
-    //     let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
-    //     cx.assert_all(indoc! {"
-    //         ˇThe qˇuick broˇwn
-    //         ˇfox jumps"
-    //     })
-    //     .await;
-    // }
+    #[gpui::test]
+    async fn test_enter(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
+        cx.assert_all(indoc! {"
+            ˇThe qˇuick broˇwn
+            ˇfox jumps"
+        })
+        .await;
+    }
 
     #[gpui::test]
     async fn test_k(cx: &mut gpui::TestAppContext) {
@@ -1030,7 +1031,7 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_percent(cx: &mut gpui::TestAppContext) {
+    async fn test_percent(cx: &mut TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
         cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
         cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")

crates/vim/src/state.rs 🔗

@@ -73,34 +73,30 @@ impl VimState {
 
     pub fn keymap_context_layer(&self) -> KeymapContext {
         let mut context = KeymapContext::default();
-        context.map.insert(
-            "vim_mode".to_string(),
+        context.add_key(
+            "vim_mode",
             match self.mode {
                 Mode::Normal => "normal",
                 Mode::Visual { .. } => "visual",
                 Mode::Insert => "insert",
-            }
-            .to_string(),
+            },
         );
 
         if self.vim_controlled() {
-            context.set.insert("VimControl".to_string());
+            context.add_identifier("VimControl");
         }
 
         let active_operator = self.operator_stack.last();
 
         if let Some(active_operator) = active_operator {
             for context_flag in active_operator.context_flags().into_iter() {
-                context.set.insert(context_flag.to_string());
+                context.add_identifier(*context_flag);
             }
         }
 
-        context.map.insert(
-            "vim_operator".to_string(),
-            active_operator
-                .map(|op| op.id())
-                .unwrap_or_else(|| "none")
-                .to_string(),
+        context.add_key(
+            "vim_operator",
+            active_operator.map(|op| op.id()).unwrap_or_else(|| "none"),
         );
 
         context

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

@@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut};
 
 use collections::{HashMap, HashSet};
 use gpui::ContextHandle;
-use language::{OffsetRangeExt, Point};
+use language::OffsetRangeExt;
 use util::test::marked_text_offsets;
 
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@@ -85,7 +85,7 @@ impl<'a> NeovimBackedTestContext<'a> {
                 let mut marked_text = unmarked_text.clone();
                 marked_text.insert(*cursor_offset, 'ˇ');
 
-                // None represents all keybindings being exempted for that initial state
+                // None represents all key bindings being exempted for that initial state
                 self.exemptions.insert(marked_text, None);
             }
         }
@@ -108,11 +108,7 @@ impl<'a> NeovimBackedTestContext<'a> {
 
     pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
         let context_handle = self.set_state(marked_text, Mode::Normal);
-
-        let selection = self.editor(|editor, cx| editor.selections.newest::<Point>(cx));
-        let text = self.buffer_text();
-        self.neovim.set_state(selection, &text).await;
-
+        self.neovim.set_state(marked_text).await;
         context_handle
     }
 

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

@@ -9,7 +9,7 @@ use async_trait::async_trait;
 #[cfg(feature = "neovim")]
 use gpui::keymap_matcher::Keystroke;
 
-use language::{Point, Selection};
+use language::Point;
 
 #[cfg(feature = "neovim")]
 use lazy_static::lazy_static;
@@ -36,11 +36,11 @@ lazy_static! {
     static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
 pub enum NeovimData {
-    Text(String),
-    Selection { start: (u32, u32), end: (u32, u32) },
-    Mode(Option<Mode>),
+    Put { state: String },
+    Key(String),
+    Get { state: String, mode: Option<Mode> },
 }
 
 pub struct NeovimConnection {
@@ -117,18 +117,30 @@ impl NeovimConnection {
 
         let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
 
+        self.data
+            .push_back(NeovimData::Key(keystroke_text.to_string()));
         self.nvim
             .input(&key)
             .await
             .expect("Could not input keystroke");
     }
 
-    // If not running with a live neovim connection, this is a no-op
     #[cfg(not(feature = "neovim"))]
-    pub async fn send_keystroke(&mut self, _keystroke_text: &str) {}
+    pub async fn send_keystroke(&mut self, keystroke_text: &str) {
+        if matches!(self.data.front(), Some(NeovimData::Get { .. })) {
+            self.data.pop_front();
+        }
+        assert_eq!(
+            self.data.pop_front(),
+            Some(NeovimData::Key(keystroke_text.to_string())),
+            "operation does not match recorded script. re-record with --features=neovim"
+        );
+    }
 
     #[cfg(feature = "neovim")]
-    pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
+    pub async fn set_state(&mut self, marked_text: &str) {
+        let (text, selection) = parse_state(&marked_text);
+
         let nvim_buffer = self
             .nvim
             .get_current_buf()
@@ -162,18 +174,41 @@ impl NeovimConnection {
         if !selection.is_empty() {
             panic!("Setting neovim state with non empty selection not yet supported");
         }
-        let cursor = selection.head();
+        let cursor = selection.start;
         nvim_window
             .set_cursor((cursor.row as i64 + 1, cursor.column as i64))
             .await
             .expect("Could not set nvim cursor position");
+
+        if let Some(NeovimData::Get { mode, state }) = self.data.back() {
+            if *mode == Some(Mode::Normal) && *state == marked_text {
+                return;
+            }
+        }
+        self.data.push_back(NeovimData::Put {
+            state: marked_text.to_string(),
+        })
     }
 
     #[cfg(not(feature = "neovim"))]
-    pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
+    pub async fn set_state(&mut self, marked_text: &str) {
+        if let Some(NeovimData::Get { mode, state: text }) = self.data.front() {
+            if *mode == Some(Mode::Normal) && *text == marked_text {
+                return;
+            }
+            self.data.pop_front();
+        }
+        assert_eq!(
+            self.data.pop_front(),
+            Some(NeovimData::Put {
+                state: marked_text.to_string()
+            }),
+            "operation does not match recorded script. re-record with --features=neovim"
+        );
+    }
 
     #[cfg(feature = "neovim")]
-    pub async fn text(&mut self) -> String {
+    pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
         let nvim_buffer = self
             .nvim
             .get_current_buf()
@@ -185,22 +220,6 @@ impl NeovimConnection {
             .expect("Could not get buffer text")
             .join("\n");
 
-        self.data.push_back(NeovimData::Text(text.clone()));
-
-        text
-    }
-
-    #[cfg(not(feature = "neovim"))]
-    pub async fn text(&mut self) -> String {
-        if let Some(NeovimData::Text(text)) = self.data.pop_front() {
-            text
-        } else {
-            panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
-        }
-    }
-
-    #[cfg(feature = "neovim")]
-    pub async fn selection(&mut self) -> Range<Point> {
         let cursor_row: u32 = self
             .nvim
             .command_output("echo line('.')")
@@ -218,7 +237,30 @@ impl NeovimConnection {
             .unwrap()
             - 1; // Neovim columns start at 1
 
-        let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
+        let nvim_mode_text = self
+            .nvim
+            .get_mode()
+            .await
+            .expect("Could not get mode")
+            .into_iter()
+            .find_map(|(key, value)| {
+                if key.as_str() == Some("mode") {
+                    Some(value.as_str().unwrap().to_owned())
+                } else {
+                    None
+                }
+            })
+            .expect("Could not find mode value");
+
+        let mode = match nvim_mode_text.as_ref() {
+            "i" => Some(Mode::Insert),
+            "n" => Some(Mode::Normal),
+            "v" => Some(Mode::Visual { line: false }),
+            "V" => Some(Mode::Visual { line: true }),
+            _ => None,
+        };
+
+        let (start, end) = if let Some(Mode::Visual { .. }) = mode {
             self.nvim
                 .input("<escape>")
                 .await
@@ -243,72 +285,54 @@ impl NeovimConnection {
 
             if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
                 (
-                    (end_row as u32 - 1, end_col as u32),
-                    (start_row as u32 - 1, start_col as u32),
+                    Point::new(end_row as u32 - 1, end_col as u32),
+                    Point::new(start_row as u32 - 1, start_col as u32),
                 )
             } else {
                 (
-                    (start_row as u32 - 1, start_col as u32),
-                    (end_row as u32 - 1, end_col as u32),
+                    Point::new(start_row as u32 - 1, start_col as u32),
+                    Point::new(end_row as u32 - 1, end_col as u32),
                 )
             }
         } else {
-            ((cursor_row, cursor_col), (cursor_row, cursor_col))
+            (
+                Point::new(cursor_row, cursor_col),
+                Point::new(cursor_row, cursor_col),
+            )
         };
 
-        self.data.push_back(NeovimData::Selection { start, end });
+        let state = NeovimData::Get {
+            mode,
+            state: encode_range(&text, start..end),
+        };
+
+        if self.data.back() != Some(&state) {
+            self.data.push_back(state.clone());
+        }
 
-        Point::new(start.0, start.1)..Point::new(end.0, end.1)
+        (mode, text, start..end)
     }
 
     #[cfg(not(feature = "neovim"))]
-    pub async fn selection(&mut self) -> Range<Point> {
-        // Selection code fetches the mode. This emulates that.
-        let _mode = self.mode().await;
-        if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
-            Point::new(start.0, start.1)..Point::new(end.0, end.1)
+    pub async fn state(&mut self) -> (Option<Mode>, String, Range<Point>) {
+        if let Some(NeovimData::Get { state: text, mode }) = self.data.front() {
+            let (text, range) = parse_state(text);
+            (*mode, text, range)
         } else {
-            panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
+            panic!("operation does not match recorded script. re-record with --features=neovim");
         }
     }
 
-    #[cfg(feature = "neovim")]
-    pub async fn mode(&mut self) -> Option<Mode> {
-        let nvim_mode_text = self
-            .nvim
-            .get_mode()
-            .await
-            .expect("Could not get mode")
-            .into_iter()
-            .find_map(|(key, value)| {
-                if key.as_str() == Some("mode") {
-                    Some(value.as_str().unwrap().to_owned())
-                } else {
-                    None
-                }
-            })
-            .expect("Could not find mode value");
-
-        let mode = match nvim_mode_text.as_ref() {
-            "i" => Some(Mode::Insert),
-            "n" => Some(Mode::Normal),
-            "v" => Some(Mode::Visual { line: false }),
-            "V" => Some(Mode::Visual { line: true }),
-            _ => None,
-        };
-
-        self.data.push_back(NeovimData::Mode(mode.clone()));
-
-        mode
+    pub async fn selection(&mut self) -> Range<Point> {
+        self.state().await.2
     }
 
-    #[cfg(not(feature = "neovim"))]
     pub async fn mode(&mut self) -> Option<Mode> {
-        if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
-            mode
-        } else {
-            panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
-        }
+        self.state().await.0
+    }
+
+    pub async fn text(&mut self) -> String {
+        self.state().await.1
     }
 
     fn test_data_path(test_case_id: &str) -> PathBuf {
@@ -325,8 +349,27 @@ impl NeovimConnection {
             "Could not read test data. Is it generated? Try running test with '--features neovim'",
         );
 
-        serde_json::from_str(&json)
-            .expect("Test data corrupted. Try regenerating it with '--features neovim'")
+        let mut result = VecDeque::new();
+        for line in json.lines() {
+            result.push_back(
+                serde_json::from_str(line)
+                    .expect("invalid test data. regenerate it with '--features neovim'"),
+            );
+        }
+        result
+    }
+
+    #[cfg(feature = "neovim")]
+    fn write_test_data(test_case_id: &str, data: &VecDeque<NeovimData>) {
+        let path = Self::test_data_path(test_case_id);
+        let mut json = Vec::new();
+        for entry in data {
+            serde_json::to_writer(&mut json, entry).unwrap();
+            json.push(b'\n');
+        }
+        std::fs::create_dir_all(path.parent().unwrap())
+            .expect("could not create test data directory");
+        std::fs::write(path, json).expect("could not write out test data");
     }
 }
 
@@ -349,11 +392,7 @@ impl DerefMut for NeovimConnection {
 #[cfg(feature = "neovim")]
 impl Drop for NeovimConnection {
     fn drop(&mut self) {
-        let path = Self::test_data_path(&self.test_case_id);
-        std::fs::create_dir_all(path.parent().unwrap())
-            .expect("Could not create test data directory");
-        let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
-        std::fs::write(path, json).expect("Could not write out test data");
+        Self::write_test_data(&self.test_case_id, &self.data);
     }
 }
 
@@ -383,3 +422,52 @@ impl Handler for NvimHandler {
     ) {
     }
 }
+
+fn parse_state(marked_text: &str) -> (String, Range<Point>) {
+    let (text, ranges) = util::test::marked_text_ranges(marked_text, true);
+    let byte_range = ranges[0].clone();
+    let mut point_range = Point::zero()..Point::zero();
+    let mut ix = 0;
+    let mut position = Point::zero();
+    for c in text.chars().chain(['\0']) {
+        if ix == byte_range.start {
+            point_range.start = position;
+        }
+        if ix == byte_range.end {
+            point_range.end = position;
+        }
+        let len_utf8 = c.len_utf8();
+        ix += len_utf8;
+        if c == '\n' {
+            position.row += 1;
+            position.column = 0;
+        } else {
+            position.column += len_utf8 as u32;
+        }
+    }
+    (text, point_range)
+}
+
+#[cfg(feature = "neovim")]
+fn encode_range(text: &str, range: Range<Point>) -> String {
+    let mut byte_range = 0..0;
+    let mut ix = 0;
+    let mut position = Point::zero();
+    for c in text.chars().chain(['\0']) {
+        if position == range.start {
+            byte_range.start = ix;
+        }
+        if position == range.end {
+            byte_range.end = ix;
+        }
+        let len_utf8 = c.len_utf8();
+        ix += len_utf8;
+        if c == '\n' {
+            position.row += 1;
+            position.column = 0;
+        } else {
+            position.column += len_utf8 as u32;
+        }
+    }
+    util::test::generate_marked_text(text, &[byte_range], true)
+}

crates/vim/src/vim.rs 🔗

@@ -12,7 +12,7 @@ mod visual;
 
 use std::sync::Arc;
 
-use command_palette::CommandPaletteFilter;
+use collections::CommandPaletteFilter;
 use editor::{Bias, Cancel, Editor, EditorMode};
 use gpui::{
     actions, impl_actions, MutableAppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,

crates/vim/test_data/neovim_backed_test_context_works.json 🔗

@@ -1 +1,3 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"This is a test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"This is a tesˇt"}}
+{"Get":{"state":"This is a tesˇt","mode":"Normal"}}

crates/vim/test_data/test_a.json 🔗

@@ -1 +1,6 @@
-[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"a"}
+{"Get":{"state":"The quˇick","mode":"Insert"}}
+{"Put":{"state":"The quicˇk"}}
+{"Key":"a"}
+{"Get":{"state":"The quickˇ","mode":"Insert"}}

crates/vim/test_data/test_backspace.json 🔗

@@ -1 +1,9 @@
-[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"backspace"}
+{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}

crates/vim/test_data/test_cc.json 🔗

@@ -1 +1,24 @@
-[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The quˇick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"c"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_change_0.json 🔗

@@ -1 +1,8 @@
-[{"Text":"uick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"c"}
+{"Key":"0"}
+{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"0"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_change_b.json 🔗

@@ -1 +1,24 @@
-[{"Text":"st Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test1 test3"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Test \ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test \n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test test"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"ˇst Test","mode":"Insert"}}
+{"Put":{"state":"Test ˇtest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"ˇtest","mode":"Insert"}}
+{"Put":{"state":"Test1 test2 ˇtest3"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇtest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"b"}
+{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test-test ˇtest"}}
+{"Key":"c"}
+{"Key":"shift-b"}
+{"Get":{"state":"Test ˇtest","mode":"Insert"}}

crates/vim/test_data/test_change_backspace.json 🔗

@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"Tˇst","mode":"Insert"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"ˇest","mode":"Insert"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"ˇTest","mode":"Insert"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"c"}
+{"Key":"backspace"}
+{"Get":{"state":"Testˇtest","mode":"Insert"}}

crates/vim/test_data/test_change_e.json 🔗

@@ -1 +1,24 @@
-[{"Text":"Te Test"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Teˇ Test","mode":"Insert"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Tˇ test","mode":"Insert"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test tesˇ","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"e"}
+{"Get":{"state":"Test test\nˇ","mode":"Insert"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"c"}
+{"Key":"shift-e"}
+{"Get":{"state":"Test teˇ test","mode":"Insert"}}

crates/vim/test_data/test_change_end_of_document.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
+{"Key":"c"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}

crates/vim/test_data/test_change_end_of_line.json 🔗

@@ -1 +1,8 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"c"}
+{"Key":"$"}
+{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"c"}
+{"Key":"$"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_change_gg.json 🔗

@@ -1 +1,20 @@
-[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"c"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}}

crates/vim/test_data/test_change_h.json 🔗

@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"Tˇst","mode":"Insert"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"ˇest","mode":"Insert"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"ˇTest","mode":"Insert"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"c"}
+{"Key":"h"}
+{"Get":{"state":"Test\nˇtest","mode":"Insert"}}

crates/vim/test_data/test_change_j.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\nˇ"}}
+{"Key":"c"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}

crates/vim/test_data/test_change_k.json 🔗

@@ -1 +1,16 @@
-[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"ˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over"}}
+{"Key":"c"}
+{"Key":"k"}
+{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}}

crates/vim/test_data/test_change_l.json 🔗

@@ -1 +1,8 @@
-[{"Text":"Tet"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"Tes"},{"Mode":"Insert"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"l"}
+{"Get":{"state":"Teˇt","mode":"Insert"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"c"}
+{"Key":"l"}
+{"Get":{"state":"Tesˇ","mode":"Insert"}}

crates/vim/test_data/test_change_w.json 🔗

@@ -1 +1,28 @@
-[{"Text":"Te"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Teˇ","mode":"Insert"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Tˇ test","mode":"Insert"}}
+{"Put":{"state":"Testˇ  test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Testˇtest","mode":"Insert"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"c"}
+{"Key":"shift-w"}
+{"Get":{"state":"Test teˇ test","mode":"Insert"}}

crates/vim/test_data/test_dd.json 🔗

@@ -1 +1,24 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"brownˇ fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\nbrown ˇfox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"The quick\nˇbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_0.json 🔗

@@ -1 +1,8 @@
-[{"Text":"uick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"d"}
+{"Key":"0"}
+{"Get":{"state":"ˇuick\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"0"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_b.json 🔗

@@ -1 +1,24 @@
-[{"Text":"st Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test1 test3"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test \ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test \n\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test test"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"ˇst Test","mode":"Normal"}}
+{"Put":{"state":"Test ˇtest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"ˇtest","mode":"Normal"}}
+{"Put":{"state":"Test1 test2 ˇtest3"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Test1 ˇtest3","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇtest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Testˇ \ntest","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"d"}
+{"Key":"b"}
+{"Get":{"state":"Testˇ \n\ntest","mode":"Normal"}}
+{"Put":{"state":"Test test-test ˇtest"}}
+{"Key":"d"}
+{"Key":"shift-b"}
+{"Get":{"state":"Test ˇtest","mode":"Normal"}}

crates/vim/test_data/test_delete_e.json 🔗

@@ -1 +1,20 @@
-[{"Text":"Te Test"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"T test"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test te test"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst Test"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Teˇ Test","mode":"Normal"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Tˇ test","mode":"Normal"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"e"}
+{"Get":{"state":"Test teˇs","mode":"Normal"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"d"}
+{"Key":"shift-e"}
+{"Get":{"state":"Test teˇ test","mode":"Normal"}}

crates/vim/test_data/test_delete_end_of_document.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The qˇuick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The qˇuick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\njumpsˇ over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}}
+{"Key":"d"}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\nbrown fox\nˇjumps over","mode":"Normal"}}

crates/vim/test_data/test_delete_end_of_line.json 🔗

@@ -1 +1,8 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"d"}
+{"Key":"$"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_gg.json 🔗

@@ -1 +1,20 @@
-[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"jumpsˇ over\nthe lazy","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"brownˇ fox\njumps over\nthe lazy","mode":"Normal"}}
+{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}}
+{"Key":"d"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇbrown fox\njumps over\nthe lazy","mode":"Normal"}}

crates/vim/test_data/test_delete_h.json 🔗

@@ -1 +1,16 @@
-[{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"Tˇst","mode":"Normal"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"ˇTest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"ˇTest","mode":"Normal"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"d"}
+{"Key":"h"}
+{"Get":{"state":"Test\nˇtest","mode":"Normal"}}

crates/vim/test_data/test_delete_j.json 🔗

@@ -1 +1,16 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quˇick","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"jumpsˇ over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\nˇ"}}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}}

crates/vim/test_data/test_delete_k.json 🔗

@@ -1 +1,16 @@
-[{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"jumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"The quˇick","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"ˇbrown fox\njumps over"}}
+{"Key":"d"}
+{"Key":"k"}
+{"Get":{"state":"ˇbrown fox\njumps over","mode":"Normal"}}

crates/vim/test_data/test_delete_l.json 🔗

@@ -1 +1,16 @@
-[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇs","mode":"Normal"}}
+{"Put":{"state":"Tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"Teˇs\ntest","mode":"Normal"}}

crates/vim/test_data/test_delete_left.json 🔗

@@ -1 +1,15 @@
-[{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"ˇTest","mode":"Normal"}}
+{"Put":{"state":"Tˇest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Tˇst","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Test\nˇtest"}}
+{"Key":"shift-x"}
+{"Get":{"state":"Test\nˇtest","mode":"Normal"}}

crates/vim/test_data/test_delete_to_end_of_line.json 🔗

@@ -1 +1,6 @@
-[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"shift-d"}
+{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"shift-d"}
+{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_delete_w.json 🔗

@@ -1 +1,20 @@
-[{"Text":"Te"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Ttest"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test tetest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]
+{"Put":{"state":"Teˇst"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Tˇe","mode":"Normal"}}
+{"Put":{"state":"Tˇest test"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Tˇtest","mode":"Normal"}}
+{"Put":{"state":"Test teˇst\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}}
+{"Put":{"state":"Test tesˇt\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇs\ntest","mode":"Normal"}}
+{"Put":{"state":"Test teˇst-test test"}}
+{"Key":"d"}
+{"Key":"shift-w"}
+{"Get":{"state":"Test teˇtest","mode":"Normal"}}

crates/vim/test_data/test_end_of_document.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,5],"end":[3,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,11],"end":[3,11]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,11],"end":[3,11]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}}
+{"Key":"2"}
+{"Key":"shift-g"}
+{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}}

crates/vim/test_data/test_enter.json 🔗

@@ -0,0 +1,11 @@
+{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick brown\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick broˇwn\nfox jumps"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Key":"enter"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}

crates/vim/test_data/test_gg.json 🔗

@@ -1 +1,21 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"The quicˇk\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"ˇ\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}}
+{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}}
+{"Key":"2"}
+{"Key":"g"}
+{"Key":"g"}
+{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}}

crates/vim/test_data/test_h.json 🔗

@@ -1 +1,9 @@
-[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"h"}
+{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"h"}
+{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"h"}
+{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}}

crates/vim/test_data/test_h_through_unicode.json 🔗

@@ -1 +1,12 @@
-[{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
+{"Put":{"state":"Testˇ├──┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Tesˇt├──┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├ˇ──┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Testˇ├──┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├──ˇ┐Test"}}
+{"Key":"h"}
+{"Get":{"state":"Test├─ˇ─┐Test","mode":"Normal"}}
+{"Put":{"state":"Test├──┐ˇTest"}}
+{"Key":"h"}
+{"Get":{"state":"Test├──ˇ┐Test","mode":"Normal"}}

crates/vim/test_data/test_insert_end_of_line.json 🔗

@@ -1 +1,9 @@
-[{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ\nThe quick\nbrown fox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"ˇ\nThe quick\nbrown fox ","mode":"Insert"}}
+{"Put":{"state":"\nThe qˇuick\nbrown fox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"\nThe quickˇ\nbrown fox ","mode":"Insert"}}
+{"Put":{"state":"\nThe quick\nbrown ˇfox "}}
+{"Key":"shift-a"}
+{"Get":{"state":"\nThe quick\nbrown fox ˇ","mode":"Insert"}}

crates/vim/test_data/test_insert_first_non_whitespace.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇThe quick","mode":"Insert"}}
+{"Put":{"state":" The qˇuick"}}
+{"Key":"shift-i"}
+{"Get":{"state":" ˇThe quick","mode":"Insert"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Insert"}}
+{"Put":{"state":"ˇ\nThe quick"}}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}}

crates/vim/test_data/test_insert_line_above.json 🔗

@@ -1 +1,18 @@
-[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\n","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"shift-o"}
+{"Get":{"state":"ˇ\nThe quick\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"shift-o"}
+{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_j.json 🔗

@@ -1 +1,12 @@
-[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick brown\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox jˇumps","mode":"Normal"}}
+{"Put":{"state":"The quick broˇwn\nfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nfox jumpˇs","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nˇfox jumps"}}
+{"Key":"j"}
+{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}

crates/vim/test_data/test_jump_to_end.json 🔗

@@ -1 +1,14 @@
-[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,16],"end":[3,16]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick\n\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The ˇquick\n\nbrown fox jumps\nover the lazy dog"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg","mode":"Normal"}}
+{"Put":{"state":"The quiˇck\n\nbrown"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nbrowˇn","mode":"Normal"}}
+{"Put":{"state":"The quiˇck\n\n"}}
+{"Key":"shift-g"}
+{"Get":{"state":"The quick\n\nˇ","mode":"Normal"}}

crates/vim/test_data/test_jump_to_first_non_whitespace.json 🔗

@@ -1 +1,18 @@
-[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"    \nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"}]
+{"Put":{"state":"The qˇuick"}}
+{"Key":"^"}
+{"Get":{"state":"ˇThe quick","mode":"Normal"}}
+{"Put":{"state":" The qˇuick"}}
+{"Key":"^"}
+{"Get":{"state":" ˇThe quick","mode":"Normal"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"^"}
+{"Get":{"state":"ˇ","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox"}}
+{"Key":"^"}
+{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"ˇ\nThe quick"}}
+{"Key":"^"}
+{"Get":{"state":"ˇ\nThe quick","mode":"Normal"}}
+{"Put":{"state":"   ˇ \nThe quick"}}
+{"Key":"^"}
+{"Get":{"state":"   ˇ \nThe quick","mode":"Normal"}}

crates/vim/test_data/test_k.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"The qˇuick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown fox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fˇox jumps"}}
+{"Key":"k"}
+{"Get":{"state":"The quiˇck\nbrown fox jumps","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox jumˇps"}}
+{"Key":"k"}
+{"Get":{"state":"The quicˇk\nbrown fox jumps","mode":"Normal"}}

crates/vim/test_data/test_l.json 🔗

@@ -1 +1,15 @@
-[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,1],"end":[1,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇThe quick\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"Tˇhe quick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quˇick\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quicˇk\nbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇbrown"}}
+{"Key":"l"}
+{"Get":{"state":"The quick\nbˇrown","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrowˇn"}}
+{"Key":"l"}
+{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}}

crates/vim/test_data/test_neovim.json 🔗

@@ -1 +1,16 @@
-[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
+{"Key":"i"}
+{"Get":{"state":"ˇ","mode":"Insert"}}
+{"Key":"shift-T"}
+{"Key":"e"}
+{"Key":"s"}
+{"Key":"t"}
+{"Key":" "}
+{"Key":"t"}
+{"Key":"e"}
+{"Key":"s"}
+{"Key":"t"}
+{"Key":"escape"}
+{"Key":"0"}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"ˇtest","mode":"Normal"}}

crates/vim/test_data/test_o.json 🔗

@@ -1 +1,18 @@
-[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"ˇ"}}
+{"Key":"o"}
+{"Get":{"state":"\nˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nˇ","mode":"Insert"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"o"}
+{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Insert"}}

crates/vim/test_data/test_p.json 🔗

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

crates/vim/test_data/test_visual_change.json 🔗

@@ -1 +1,41 @@
-[{"Text":"The quick "},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps he lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"c"}
+{"Get":{"state":"The quick ˇ","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇazy dog","mode":"Insert"}}

crates/vim/test_data/test_visual_delete.json 🔗

@@ -1 +1,44 @@
-[{"Text":"The quick "},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The ver\nthe lquick brown\nfox jumps oazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quick ˇbrown"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"x"}
+{"Get":{"state":"The quickˇ ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"The ver\nthe lˇquick brown\nfox jumps oazy dog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"w"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Normal"}}
+{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}}
+{"Key":"v"}
+{"Key":"b"}
+{"Key":"k"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nˇazy dog","mode":"Normal"}}

crates/vim/test_data/test_visual_line_change.json 🔗

@@ -1 +1,35 @@
-[{"Text":"\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Key":"escape"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"\nfox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇ\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"ˇ\nthe lazy dog","mode":"Insert"}}
+{"Key":"escape"}
+{"Key":"j"}
+{"Key":"p"}
+{"Get":{"state":"\nthe lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"c"}
+{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}}

crates/vim/test_data/test_visual_line_delete.json 🔗

@@ -1 +1,31 @@
-[{"Text":"fox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"fox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"the lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"the laˇzy dog","mode":"Normal"}}
+{"Key":"p"}
+{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quˇick brown","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"x"}
+{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}}

crates/vim/test_data/test_x.json 🔗

@@ -1 +1,12 @@
-[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]
+{"Put":{"state":"ˇTest"}}
+{"Key":"x"}
+{"Get":{"state":"ˇest","mode":"Normal"}}
+{"Put":{"state":"Teˇst"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇt","mode":"Normal"}}
+{"Put":{"state":"Tesˇt"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇs","mode":"Normal"}}
+{"Put":{"state":"Tesˇt\ntest"}}
+{"Key":"x"}
+{"Get":{"state":"Teˇs\ntest","mode":"Normal"}}

crates/welcome/Cargo.toml 🔗

@@ -0,0 +1,27 @@
+[package]
+name = "welcome"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/welcome.rs"
+
+[features]
+test-support = []
+
+[dependencies]
+anyhow = "1.0.38"
+log = "0.4"
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+db = { path = "../db" }
+install_cli = { path = "../install_cli" }
+project = { path = "../project" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+theme_selector = { path = "../theme_selector" }
+util = { path = "../util" }
+picker = { path = "../picker" }
+workspace = { path = "../workspace" }

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -0,0 +1,175 @@
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions,
+    elements::{ChildView, Element as _, Label},
+    AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle,
+};
+use picker::{Picker, PickerDelegate};
+use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
+use workspace::Workspace;
+
+pub struct BaseKeymapSelector {
+    matches: Vec<StringMatch>,
+    picker: ViewHandle<Picker<Self>>,
+    selected_index: usize,
+}
+
+actions!(welcome, [ToggleBaseKeymapSelector]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    Picker::<BaseKeymapSelector>::init(cx);
+    cx.add_action({
+        move |workspace, _: &ToggleBaseKeymapSelector, cx| BaseKeymapSelector::toggle(workspace, cx)
+    });
+}
+
+pub enum Event {
+    Dismissed,
+}
+
+impl BaseKeymapSelector {
+    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        workspace.toggle_modal(cx, |_, cx| {
+            let this = cx.add_view(|cx| Self::new(cx));
+            cx.subscribe(&this, Self::on_event).detach();
+            this
+        });
+    }
+
+    fn new(cx: &mut ViewContext<Self>) -> Self {
+        let base = cx.global::<Settings>().base_keymap;
+        let selected_index = BaseKeymap::OPTIONS
+            .iter()
+            .position(|(_, value)| *value == base)
+            .unwrap_or(0);
+
+        let this = cx.weak_handle();
+        Self {
+            picker: cx.add_view(|cx| Picker::new("Select a base keymap", this, cx)),
+            matches: Vec::new(),
+            selected_index,
+        }
+    }
+
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<BaseKeymapSelector>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => {
+                workspace.dismiss_modal(cx);
+            }
+        }
+    }
+}
+
+impl Entity for BaseKeymapSelector {
+    type Event = Event;
+}
+
+impl View for BaseKeymapSelector {
+    fn ui_name() -> &'static str {
+        "BaseKeymapSelector"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        ChildView::new(&self.picker, cx).boxed()
+    }
+
+    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(&self.picker);
+        }
+    }
+}
+
+impl PickerDelegate for BaseKeymapSelector {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+        let background = cx.background().clone();
+        let candidates = BaseKeymap::names()
+            .enumerate()
+            .map(|(id, name)| StringMatchCandidate {
+                id,
+                char_bag: name.into(),
+                string: name.into(),
+            })
+            .collect::<Vec<_>>();
+
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, cx| {
+                this.matches = matches;
+                this.selected_index = this
+                    .selected_index
+                    .min(this.matches.len().saturating_sub(1));
+                cx.notify();
+            });
+        })
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(selection) = self.matches.get(self.selected_index) {
+            let base_keymap = BaseKeymap::from_names(&selection.string);
+            SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
+        }
+        cx.emit(Event::Dismissed);
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed)
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut gpui::MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> gpui::ElementBox {
+        let theme = &cx.global::<Settings>().theme;
+        let keymap_match = &self.matches[ix];
+        let style = theme.picker.item.style_for(mouse_state, selected);
+
+        Label::new(keymap_match.string.clone(), style.label.clone())
+            .with_highlights(keymap_match.positions.clone())
+            .contained()
+            .with_style(style.container)
+            .boxed()
+    }
+}

crates/welcome/src/welcome.rs 🔗

@@ -0,0 +1,235 @@
+mod base_keymap_picker;
+
+use std::sync::Arc;
+
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    elements::{Flex, Label, ParentElement},
+    Element, ElementBox, Entity, MutableAppContext, Subscription, View, ViewContext,
+};
+use settings::{settings_file::SettingsFile, Settings};
+
+use workspace::{
+    item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
+    WorkspaceId,
+};
+
+use crate::base_keymap_picker::ToggleBaseKeymapSelector;
+
+pub const FIRST_OPEN: &str = "first_open";
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
+        let welcome_page = cx.add_view(WelcomePage::new);
+        workspace.add_item(Box::new(welcome_page), cx)
+    });
+
+    base_keymap_picker::init(cx);
+}
+
+pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
+    open_new(&app_state, cx, |workspace, cx| {
+        workspace.toggle_sidebar(SidebarSide::Left, cx);
+        let welcome_page = cx.add_view(|cx| WelcomePage::new(cx));
+        workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
+        cx.focus(&welcome_page);
+        cx.notify();
+    })
+    .detach();
+
+    db::write_and_log(cx, || {
+        KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
+    });
+}
+
+pub struct WelcomePage {
+    _settings_subscription: Subscription,
+}
+
+impl Entity for WelcomePage {
+    type Event = ();
+}
+
+impl View for WelcomePage {
+    fn ui_name() -> &'static str {
+        "WelcomePage"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let self_handle = cx.handle();
+        let settings = cx.global::<Settings>();
+        let theme = settings.theme.clone();
+
+        let width = theme.welcome.page_width;
+
+        let (diagnostics, metrics) = {
+            let telemetry = settings.telemetry();
+            (telemetry.diagnostics(), telemetry.metrics())
+        };
+
+        enum Metrics {}
+        enum Diagnostics {}
+
+        PaneBackdrop::new(
+            self_handle.id(),
+            Flex::column()
+                .with_children([
+                    Flex::column()
+                        .with_children([
+                            theme::ui::svg(&theme.welcome.logo)
+                                .aligned()
+                                .contained()
+                                .aligned()
+                                .boxed(),
+                            Label::new(
+                                "Code at the speed of thought",
+                                theme.welcome.logo_subheading.text.clone(),
+                            )
+                            .aligned()
+                            .contained()
+                            .with_style(theme.welcome.logo_subheading.container)
+                            .boxed(),
+                        ])
+                        .contained()
+                        .with_style(theme.welcome.heading_group)
+                        .constrained()
+                        .with_width(width)
+                        .boxed(),
+                    Flex::column()
+                        .with_children([
+                            theme::ui::cta_button(
+                                "Choose a theme",
+                                theme_selector::Toggle,
+                                width,
+                                &theme.welcome.button,
+                                cx,
+                            ),
+                            theme::ui::cta_button(
+                                "Choose a keymap",
+                                ToggleBaseKeymapSelector,
+                                width,
+                                &theme.welcome.button,
+                                cx,
+                            ),
+                            theme::ui::cta_button(
+                                "Install the CLI",
+                                install_cli::Install,
+                                width,
+                                &theme.welcome.button,
+                                cx,
+                            ),
+                        ])
+                        .contained()
+                        .with_style(theme.welcome.button_group)
+                        .constrained()
+                        .with_width(width)
+                        .boxed(),
+                    Flex::column()
+                        .with_children([
+                            theme::ui::checkbox_with_label::<Metrics, Self>(
+                                Flex::column()
+                                    .with_children([
+                                        Label::new(
+                                            "Send anonymous usage data",
+                                            theme.welcome.checkbox.label.text.clone(),
+                                        )
+                                        .contained()
+                                        .with_style(theme.welcome.checkbox.label.container)
+                                        .boxed(),
+                                        Label::new(
+                                            "Help > View Telemetry",
+                                            theme.welcome.usage_note.text.clone(),
+                                        )
+                                        .contained()
+                                        .with_style(theme.welcome.usage_note.container)
+                                        .boxed(),
+                                    ])
+                                    .boxed(),
+                                &theme.welcome.checkbox,
+                                metrics,
+                                cx,
+                                |checked, cx| {
+                                    SettingsFile::update(cx, move |file| {
+                                        file.telemetry.set_metrics(checked)
+                                    })
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container)
+                            .boxed(),
+                            theme::ui::checkbox::<Diagnostics, Self>(
+                                "Send crash reports",
+                                &theme.welcome.checkbox,
+                                diagnostics,
+                                cx,
+                                |checked, cx| {
+                                    SettingsFile::update(cx, move |file| {
+                                        file.telemetry.set_diagnostics(checked)
+                                    })
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container)
+                            .boxed(),
+                        ])
+                        .contained()
+                        .with_style(theme.welcome.checkbox_group)
+                        .constrained()
+                        .with_width(width)
+                        .boxed(),
+                ])
+                .constrained()
+                .with_max_width(width)
+                .contained()
+                .with_uniform_padding(10.)
+                .aligned()
+                .boxed(),
+        )
+        .boxed()
+    }
+}
+
+impl WelcomePage {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        let handle = cx.weak_handle();
+
+        let settings_subscription = cx.observe_global::<Settings, _>(move |cx| {
+            if let Some(handle) = handle.upgrade(cx) {
+                handle.update(cx, |_, cx| cx.notify())
+            }
+        });
+
+        WelcomePage {
+            _settings_subscription: settings_subscription,
+        }
+    }
+}
+
+impl Item for WelcomePage {
+    fn tab_content(
+        &self,
+        _detail: Option<usize>,
+        style: &theme::Tab,
+        _cx: &gpui::AppContext,
+    ) -> gpui::ElementBox {
+        Flex::row()
+            .with_child(
+                Label::new("Welcome to Zed!", style.label.clone())
+                    .aligned()
+                    .contained()
+                    .boxed(),
+            )
+            .boxed()
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self> {
+        Some(WelcomePage::new(cx))
+    }
+}

crates/workspace/Cargo.toml 🔗

@@ -27,10 +27,12 @@ context_menu = { path = "../context_menu" }
 drag_and_drop = { path = "../drag_and_drop" }
 fs = { path = "../fs" }
 gpui = { path = "../gpui" }
+install_cli = { path = "../install_cli" }
 language = { path = "../language" }
 menu = { path = "../menu" }
 project = { path = "../project" }
 settings = { path = "../settings" }
+terminal = { path = "../terminal" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 async-recursion = "1.0.0"
@@ -41,9 +43,10 @@ lazy_static = "1.4"
 env_logger = "0.9.1"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+postage = { workspace = true }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 smallvec = { version = "1.6", features = ["union"] }
 indoc = "1.0.4"
 uuid = { version = "1.1.2", features = ["v4"] }
@@ -55,4 +58,4 @@ gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
-db = { path = "../db", features = ["test-support"] }
+db = { path = "../db", features = ["test-support"] }

crates/workspace/src/dock.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
 use settings::{DockAnchor, Settings};
 use theme::Theme;
 
-use crate::{sidebar::SidebarSide, ItemHandle, Pane, Workspace};
+use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
 pub use toggle_dock_button::ToggleDockButton;
 
 #[derive(PartialEq, Clone, Deserialize)]
@@ -39,25 +39,28 @@ impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Dock::focus_dock);
     cx.add_action(Dock::hide_dock);
-    cx.add_action(Dock::move_dock);
+    cx.add_action(
+        |workspace: &mut Workspace, &MoveDock(dock_anchor), cx: &mut ViewContext<Workspace>| {
+            Dock::move_dock(workspace, dock_anchor, true, cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
-            Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx)
+            Dock::move_dock(workspace, DockAnchor::Right, true, cx);
         },
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
-            Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx)
+            Dock::move_dock(workspace, DockAnchor::Bottom, true, cx)
         },
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
-            Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
+            Dock::move_dock(workspace, DockAnchor::Expanded, true, cx)
         },
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &AddTabToDock, cx: &mut ViewContext<Workspace>| {
-            eprintln!("Add tab to dock");
             if let Some(active_item) = workspace.active_item(cx) {
                 let item_id = active_item.id();
 
@@ -82,7 +85,6 @@ pub fn init(cx: &mut MutableAppContext) {
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &RemoveTabFromDock, cx: &mut ViewContext<Workspace>| {
-            eprintln!("Removing tab from dock");
             if let Some(active_item) = workspace.active_item(cx) {
                 let item_id = active_item.id();
 
@@ -179,12 +181,21 @@ pub struct Dock {
 
 impl Dock {
     pub fn new(
+        workspace_id: usize,
         default_item_factory: DockDefaultItemFactory,
+        background_actions: BackgroundActions,
         cx: &mut ViewContext<Workspace>,
     ) -> Self {
         let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
 
-        let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
+        let pane = cx.add_view(|cx| {
+            Pane::new(
+                workspace_id,
+                Some(position.anchor()),
+                background_actions,
+                cx,
+            )
+        });
         pane.update(cx, |pane, cx| {
             pane.set_active(false, cx);
         });
@@ -217,6 +228,7 @@ impl Dock {
     pub(crate) fn set_dock_position(
         workspace: &mut Workspace,
         new_position: DockPosition,
+        focus: bool,
         cx: &mut ViewContext<Workspace>,
     ) {
         workspace.dock.position = new_position;
@@ -237,19 +249,23 @@ impl Dock {
             let pane = workspace.dock.pane.clone();
             if pane.read(cx).items().next().is_none() {
                 if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
-                    Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
+                    Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx);
                 } else {
                     workspace.dock.position = workspace.dock.position.hide();
                 }
             } else {
-                cx.focus(pane);
+                if focus {
+                    cx.focus(&pane);
+                }
             }
         } else if let Some(last_active_center_pane) = workspace
             .last_active_center_pane
             .as_ref()
             .and_then(|pane| pane.upgrade(cx))
         {
-            cx.focus(last_active_center_pane);
+            if focus {
+                cx.focus(&last_active_center_pane);
+            }
         }
         cx.emit(crate::Event::DockAnchorChanged);
         workspace.serialize_workspace(cx);
@@ -257,11 +273,11 @@ impl Dock {
     }
 
     pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
-        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
+        Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
     }
 
-    pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
-        Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
+    pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext<Workspace>) {
+        Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx);
     }
 
     pub fn hide_on_sidebar_shown(
@@ -277,19 +293,20 @@ impl Dock {
     }
 
     fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
-        Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
+        Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
     }
 
     fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
-        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
+        Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
     }
 
-    fn move_dock(
+    pub fn move_dock(
         workspace: &mut Workspace,
-        &MoveDock(new_anchor): &MoveDock,
+        new_anchor: DockAnchor,
+        focus: bool,
         cx: &mut ViewContext<Workspace>,
     ) {
-        Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
+        Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx);
     }
 
     pub fn render(
@@ -330,7 +347,7 @@ impl Dock {
 
                     enum DockResizeHandle {}
 
-                    let resizable = Container::new(ChildView::new(self.pane.clone(), cx).boxed())
+                    let resizable = Container::new(ChildView::new(&self.pane, cx).boxed())
                         .with_style(panel_style)
                         .with_resize_handle::<DockResizeHandle, _>(
                             resize_side as usize,
@@ -484,6 +501,7 @@ mod tests {
                 0,
                 project.clone(),
                 default_item_factory,
+                || &[],
                 cx,
             )
         });
@@ -612,7 +630,14 @@ mod tests {
             cx.update(|cx| init(cx));
             let project = Project::test(fs, [], cx).await;
             let (window_id, workspace) = cx.add_window(|cx| {
-                Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+                Workspace::new(
+                    Default::default(),
+                    0,
+                    project,
+                    default_item_factory,
+                    || &[],
+                    cx,
+                )
             });
 
             workspace.update(cx, |workspace, cx| {

crates/workspace/src/dock/toggle_dock_button.rs 🔗

@@ -42,6 +42,7 @@ impl View for ToggleDockButton {
 
         let workspace = workspace.unwrap();
         let dock_position = workspace.read(cx).dock.position;
+        let dock_pane = workspace.read(cx.app).dock_pane().clone();
 
         let theme = cx.global::<Settings>().theme.clone();
 
@@ -67,7 +68,6 @@ impl View for ToggleDockButton {
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .on_up(MouseButton::Left, move |event, cx| {
-            let dock_pane = workspace.read(cx.app).dock_pane();
             let drop_index = dock_pane.read(cx.app).items_len() + 1;
             handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
         });

crates/workspace/src/item.rs 🔗

@@ -49,9 +49,11 @@ pub trait Item: View {
     }
     fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
         -> ElementBox;
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item));
-    fn is_singleton(&self, cx: &AppContext) -> bool;
-    fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
+    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::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,
@@ -64,23 +66,31 @@ pub trait Item: View {
     fn has_conflict(&self, _: &AppContext) -> bool {
         false
     }
-    fn can_save(&self, cx: &AppContext) -> bool;
+    fn can_save(&self, _cx: &AppContext) -> bool {
+        false
+    }
     fn save(
         &mut self,
-        project: ModelHandle<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>>;
+        _project: ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        unimplemented!("save() must be implemented if can_save() returns true")
+    }
     fn save_as(
         &mut self,
-        project: ModelHandle<Project>,
-        abs_path: PathBuf,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>>;
+        _project: ModelHandle<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: ModelHandle<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>>;
+        _project: ModelHandle<Project>,
+        _cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        unimplemented!("reload() must be implemented if can_save() returns true")
+    }
     fn git_diff_recalc(
         &mut self,
         _project: ModelHandle<Project>,
@@ -88,7 +98,9 @@ pub trait Item: View {
     ) -> Task<Result<()>> {
         Task::ready(Ok(()))
     }
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]>;
+    fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+        SmallVec::new()
+    }
     fn should_close_item_on_event(_: &Self::Event) -> bool {
         false
     }
@@ -98,14 +110,14 @@ pub trait Item: View {
     fn is_edit_event(_: &Self::Event) -> bool {
         false
     }
-    fn act_as_type(
-        &self,
+    fn act_as_type<'a>(
+        &'a self,
         type_id: TypeId,
-        self_handle: &ViewHandle<Self>,
-        _: &AppContext,
-    ) -> Option<AnyViewHandle> {
+        self_handle: &'a ViewHandle<Self>,
+        _: &'a AppContext,
+    ) -> Option<&AnyViewHandle> {
         if TypeId::of::<Self>() == type_id {
-            Some(self_handle.into())
+            Some(self_handle)
         } else {
             None
         }
@@ -124,15 +136,24 @@ pub trait Item: View {
 
     fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
 
-    fn serialized_item_kind() -> Option<&'static str>;
+    fn serialized_item_kind() -> Option<&'static str> {
+        None
+    }
 
     fn deserialize(
-        project: ModelHandle<Project>,
-        workspace: WeakViewHandle<Workspace>,
-        workspace_id: WorkspaceId,
-        item_id: ItemId,
-        cx: &mut ViewContext<Pane>,
-    ) -> Task<Result<ViewHandle<Self>>>;
+        _project: ModelHandle<Project>,
+        _workspace: WeakViewHandle<Workspace>,
+        _workspace_id: WorkspaceId,
+        _item_id: ItemId,
+        _cx: &mut ViewContext<Pane>,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        unimplemented!(
+            "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
+        )
+    }
+    fn show_toolbar(&self) -> bool {
+        true
+    }
 }
 
 pub trait ItemHandle: 'static + fmt::Debug {
@@ -166,7 +187,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
     fn id(&self) -> usize;
     fn window_id(&self) -> usize;
-    fn to_any(&self) -> AnyViewHandle;
+    fn as_any(&self) -> &AnyViewHandle;
     fn is_dirty(&self, cx: &AppContext) -> bool;
     fn has_conflict(&self, cx: &AppContext) -> bool;
     fn can_save(&self, cx: &AppContext) -> bool;
@@ -184,7 +205,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
         project: ModelHandle<Project>,
         cx: &mut MutableAppContext,
     ) -> Task<Result<()>>;
-    fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
+    fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a AppContext) -> Option<&'a AnyViewHandle>;
     fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
     fn on_release(
         &self,
@@ -195,6 +216,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
     fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
     fn serialized_item_kind(&self) -> Option<&'static str>;
+    fn show_toolbar(&self, cx: &AppContext) -> bool;
 }
 
 pub trait WeakItemHandle {
@@ -205,12 +227,12 @@ pub trait WeakItemHandle {
 
 impl dyn ItemHandle {
     pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
-        self.to_any().downcast()
+        self.as_any().clone().downcast()
     }
 
     pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
         self.act_as_type(TypeId::of::<T>(), cx)
-            .and_then(|t| t.downcast())
+            .and_then(|t| t.clone().downcast())
     }
 }
 
@@ -491,8 +513,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.window_id()
     }
 
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
+    fn as_any(&self) -> &AnyViewHandle {
+        self
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -536,14 +558,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
     }
 
-    fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
+    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.to_any();
+            let item = self.as_any();
             Some(builders.get(&item.view_type())?.1(item))
         } else {
             None
@@ -573,17 +595,21 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
     fn serialized_item_kind(&self) -> Option<&'static str> {
         T::serialized_item_kind()
     }
+
+    fn show_toolbar(&self, cx: &AppContext) -> bool {
+        self.read(cx).show_toolbar()
+    }
 }
 
 impl From<Box<dyn ItemHandle>> for AnyViewHandle {
     fn from(val: Box<dyn ItemHandle>) -> Self {
-        val.to_any()
+        val.as_any().clone()
     }
 }
 
 impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
     fn from(val: &Box<dyn ItemHandle>) -> Self {
-        val.to_any()
+        val.as_any().clone()
     }
 }
 

crates/workspace/src/notifications.rs 🔗

@@ -16,7 +16,7 @@ pub trait Notification: View {
 
 pub trait NotificationHandle {
     fn id(&self) -> usize;
-    fn to_any(&self) -> AnyViewHandle;
+    fn as_any(&self) -> &AnyViewHandle;
 }
 
 impl<T: Notification> NotificationHandle for ViewHandle<T> {
@@ -24,14 +24,14 @@ impl<T: Notification> NotificationHandle for ViewHandle<T> {
         self.id()
     }
 
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
+    fn as_any(&self) -> &AnyViewHandle {
+        self
     }
 }
 
 impl From<&dyn NotificationHandle> for AnyViewHandle {
     fn from(val: &dyn NotificationHandle) -> Self {
-        val.to_any()
+        val.as_any().clone()
     }
 }
 
@@ -97,7 +97,7 @@ impl Workspace {
             let notification = build_notification(cx);
             cx.subscribe(&notification, move |this, handle, event, cx| {
                 if handle.read(cx).should_dismiss_notification_on_event(event) {
-                    this.dismiss_notification(type_id, id, cx);
+                    this.dismiss_notification_internal(type_id, id, cx);
                 }
             })
             .detach();
@@ -107,7 +107,18 @@ impl Workspace {
         }
     }
 
-    fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
+    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
+        let type_id = TypeId::of::<V>();
+
+        self.dismiss_notification_internal(type_id, id, cx)
+    }
+
+    fn dismiss_notification_internal(
+        &mut self,
+        type_id: TypeId,
+        id: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.notifications
             .retain(|(existing_type_id, existing_id, _)| {
                 if (*existing_type_id, *existing_id) == (type_id, id) {
@@ -122,6 +133,8 @@ impl Workspace {
 
 pub mod simple_message_notification {
 
+    use std::borrow::Cow;
+
     use gpui::{
         actions,
         elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
@@ -139,7 +152,13 @@ pub mod simple_message_notification {
     actions!(message_notifications, [CancelMessageNotification]);
 
     #[derive(Clone, Default, Deserialize, PartialEq)]
-    pub struct OsOpen(pub String);
+    pub struct OsOpen(pub Cow<'static, str>);
+
+    impl OsOpen {
+        pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
+            OsOpen(url.into())
+        }
+    }
 
     impl_actions!(message_notifications, [OsOpen]);
 
@@ -147,15 +166,15 @@ pub mod simple_message_notification {
         cx.add_action(MessageNotification::dismiss);
         cx.add_action(
             |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
-                cx.platform().open_url(open_action.0.as_str());
+                cx.platform().open_url(open_action.0.as_ref());
             },
         )
     }
 
     pub struct MessageNotification {
-        message: String,
+        message: Cow<'static, str>,
         click_action: Option<Box<dyn Action>>,
-        click_message: Option<String>,
+        click_message: Option<Cow<'static, str>>,
     }
 
     pub enum MessageNotificationEvent {
@@ -167,23 +186,35 @@ pub mod simple_message_notification {
     }
 
     impl MessageNotification {
-        pub fn new_message<S: AsRef<str>>(message: S) -> MessageNotification {
+        pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
             Self {
-                message: message.as_ref().to_string(),
+                message: message.into(),
                 click_action: None,
                 click_message: None,
             }
         }
 
-        pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
+        pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
+            message: S1,
+            click_action: Box<dyn Action>,
+            click_message: S2,
+        ) -> Self {
+            Self {
+                message: message.into(),
+                click_action: Some(click_action),
+                click_message: Some(click_message.into()),
+            }
+        }
+
+        pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
             message: S1,
             click_action: A,
             click_message: S2,
         ) -> Self {
             Self {
-                message: message.as_ref().to_string(),
+                message: message.into(),
                 click_action: Some(Box::new(click_action) as Box<dyn Action>),
-                click_message: Some(click_message.as_ref().to_string()),
+                click_message: Some(click_message.into()),
             }
         }
 
@@ -210,6 +241,8 @@ pub mod simple_message_notification {
             let click_message = self.click_message.as_ref().map(|message| message.clone());
             let message = self.message.clone();
 
+            let has_click_action = click_action.is_some();
+
             MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
                 Flex::column()
                     .with_child(
@@ -243,6 +276,7 @@ pub mod simple_message_notification {
                                 .on_click(MouseButton::Left, move |_, cx| {
                                     cx.dispatch_action(CancelMessageNotification)
                                 })
+                                .with_cursor_style(CursorStyle::PointingHand)
                                 .aligned()
                                 .constrained()
                                 .with_height(
@@ -259,9 +293,13 @@ pub mod simple_message_notification {
                         let style = theme.action_message.style_for(state, false);
                         if let Some(click_message) = click_message {
                             Some(
-                                Text::new(click_message, style.text.clone())
-                                    .contained()
-                                    .with_style(style.container)
+                                Flex::row()
+                                    .with_child(
+                                        Text::new(click_message, style.text.clone())
+                                            .contained()
+                                            .with_style(style.container)
+                                            .boxed(),
+                                    )
                                     .boxed(),
                             )
                         } else {
@@ -272,12 +310,20 @@ pub mod simple_message_notification {
                     .contained()
                     .boxed()
             })
-            .with_cursor_style(CursorStyle::PointingHand)
+            // Since we're not using a proper overlay, we have to capture these extra events
+            .on_down(MouseButton::Left, |_, _| {})
+            .on_up(MouseButton::Left, |_, _| {})
             .on_click(MouseButton::Left, move |_, cx| {
                 if let Some(click_action) = click_action.as_ref() {
-                    cx.dispatch_any_action(click_action.boxed_clone())
+                    cx.dispatch_any_action(click_action.boxed_clone());
+                    cx.dispatch_action(CancelMessageNotification)
                 }
             })
+            .with_cursor_style(if has_click_action {
+                CursorStyle::PointingHand
+            } else {
+                CursorStyle::Arrow
+            })
             .boxed()
         }
     }

crates/workspace/src/pane.rs 🔗

@@ -21,10 +21,11 @@ use gpui::{
         vector::{vec2f, Vector2F},
     },
     impl_actions, impl_internal_actions,
+    keymap_matcher::KeymapContext,
     platform::{CursorStyle, NavigationDirection},
     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
-    ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    ModelHandle, MouseButton, MouseRegion, MutableAppContext, PromptLevel, Quad, RenderContext,
+    Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
@@ -81,19 +82,13 @@ pub struct GoForward {
 }
 
 #[derive(Clone, PartialEq)]
-pub struct DeploySplitMenu {
-    position: Vector2F,
-}
+pub struct DeploySplitMenu;
 
 #[derive(Clone, PartialEq)]
-pub struct DeployDockMenu {
-    position: Vector2F,
-}
+pub struct DeployDockMenu;
 
 #[derive(Clone, PartialEq)]
-pub struct DeployNewMenu {
-    position: Vector2F,
-}
+pub struct DeployNewMenu;
 
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
 impl_internal_actions!(
@@ -109,6 +104,8 @@ impl_internal_actions!(
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
+pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
+
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
         pane.activate_item(action.0, true, true, cx);
@@ -165,8 +162,8 @@ pub fn init(cx: &mut MutableAppContext) {
     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));
     cx.add_action(Pane::deploy_split_menu);
-    cx.add_action(Pane::deploy_new_menu);
     cx.add_action(Pane::deploy_dock_menu);
+    cx.add_action(Pane::deploy_new_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });
@@ -212,8 +209,10 @@ pub struct Pane {
     autoscroll: bool,
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
-    tab_bar_context_menu: ViewHandle<ContextMenu>,
+    tab_bar_context_menu: TabBarContextMenu,
     docked: Option<DockAnchor>,
+    _background_actions: BackgroundActions,
+    _workspace_id: usize,
 }
 
 pub struct ItemNavHistory {
@@ -269,10 +268,40 @@ enum ItemType {
     All,
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum TabBarContextMenuKind {
+    New,
+    Split,
+    Dock,
+}
+
+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
+    }
+}
+
 impl Pane {
-    pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        workspace_id: usize,
+        docked: Option<DockAnchor>,
+        background_actions: BackgroundActions,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let handle = cx.weak_handle();
         let context_menu = cx.add_view(ContextMenu::new);
+        context_menu.update(cx, |menu, _| {
+            menu.set_position_mode(OverlayPositionMode::Local)
+        });
+
         Self {
             items: Vec::new(),
             activation_history: Vec::new(),
@@ -289,8 +318,13 @@ impl Pane {
                 pane: handle.clone(),
             })),
             toolbar: cx.add_view(|_| Toolbar::new(handle)),
-            tab_bar_context_menu: context_menu,
+            tab_bar_context_menu: TabBarContextMenu {
+                kind: TabBarContextMenuKind::New,
+                handle: context_menu,
+            },
             docked,
+            _background_actions: background_actions,
+            _workspace_id: workspace_id,
         }
     }
 
@@ -379,7 +413,7 @@ impl Pane {
         mode: NavigationMode,
         cx: &mut ViewContext<Workspace>,
     ) -> Task<()> {
-        cx.focus(pane.clone());
+        cx.focus(&pane);
 
         let to_load = pane.update(cx, |pane, cx| {
             loop {
@@ -562,7 +596,7 @@ impl Pane {
             // If the item already exists, move it to the desired destination and activate it
             pane.update(cx, |pane, cx| {
                 if existing_item_index != insertion_index {
-                    cx.reparent(&item);
+                    cx.reparent(item.as_any());
                     let existing_item_is_active = existing_item_index == pane.active_item_index;
 
                     // If the caller didn't specify a destination and the added item is already
@@ -592,7 +626,7 @@ impl Pane {
             });
         } else {
             pane.update(cx, |pane, cx| {
-                cx.reparent(&item);
+                cx.reparent(item.as_any());
                 pane.items.insert(insertion_index, item);
                 if insertion_index <= pane.active_item_index {
                     pane.active_item_index += 1;
@@ -615,7 +649,7 @@ impl Pane {
     pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
         self.items
             .iter()
-            .filter_map(|item| item.to_any().downcast())
+            .filter_map(|item| item.as_any().clone().downcast())
     }
 
     pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
@@ -1014,7 +1048,7 @@ impl Pane {
 
     pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(active_item) = self.active_item() {
-            cx.focus(active_item);
+            cx.focus(active_item.as_any());
         }
     }
 
@@ -1057,17 +1091,17 @@ impl Pane {
             cx,
         );
 
-        cx.focus(to);
+        cx.focus(&to);
     }
 
     pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
         cx.emit(Event::Split(direction));
     }
 
-    fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
-        self.tab_bar_context_menu.update(cx, |menu, cx| {
+    fn deploy_split_menu(&mut self, _: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
+        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
             menu.show(
-                action.position,
+                Default::default(),
                 AnchorCorner::TopRight,
                 vec![
                     ContextMenuItem::item("Split Right", SplitRight),
@@ -1078,12 +1112,14 @@ impl Pane {
                 cx,
             );
         });
+
+        self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
     }
 
-    fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
-        self.tab_bar_context_menu.update(cx, |menu, cx| {
+    fn deploy_dock_menu(&mut self, _: &DeployDockMenu, cx: &mut ViewContext<Self>) {
+        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
             menu.show(
-                action.position,
+                Default::default(),
                 AnchorCorner::TopRight,
                 vec![
                     ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
@@ -1093,12 +1129,14 @@ impl Pane {
                 cx,
             );
         });
+
+        self.tab_bar_context_menu.kind = TabBarContextMenuKind::Dock;
     }
 
-    fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
-        self.tab_bar_context_menu.update(cx, |menu, cx| {
+    fn deploy_new_menu(&mut self, _: &DeployNewMenu, cx: &mut ViewContext<Self>) {
+        self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
             menu.show(
-                action.position,
+                Default::default(),
                 AnchorCorner::TopRight,
                 vec![
                     ContextMenuItem::item("New File", NewFile),
@@ -1108,6 +1146,8 @@ impl Pane {
                 cx,
             );
         });
+
+        self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
     }
 
     pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
@@ -1149,40 +1189,53 @@ impl Pane {
             let tab_active = ix == self.active_item_index;
 
             row.add_child({
-                enum Tab {}
-                let mut receiver = dragged_item_receiver::<Tab, _>(ix, ix, true, None, cx, {
-                    let item = item.clone();
-                    let pane = pane.clone();
-                    let detail = detail.clone();
-
-                    let theme = cx.global::<Settings>().theme.clone();
-
-                    move |mouse_state, cx| {
-                        let tab_style = theme.workspace.tab_bar.tab_style(pane_active, tab_active);
-                        let hovered = mouse_state.hovered();
-                        Self::render_tab(&item, pane, ix == 0, detail, hovered, tab_style, cx)
-                    }
-                });
+                enum TabDragReceiver {}
+                let mut receiver =
+                    dragged_item_receiver::<TabDragReceiver, _>(ix, ix, true, None, cx, {
+                        let item = item.clone();
+                        let pane = pane.clone();
+                        let detail = detail.clone();
+
+                        let theme = cx.global::<Settings>().theme.clone();
+
+                        move |mouse_state, cx| {
+                            let tab_style =
+                                theme.workspace.tab_bar.tab_style(pane_active, tab_active);
+                            let hovered = mouse_state.hovered();
+
+                            enum Tab {}
+                            MouseEventHandler::<Tab>::new(ix, cx, |_, cx| {
+                                Self::render_tab(
+                                    &item,
+                                    pane.clone(),
+                                    ix == 0,
+                                    detail,
+                                    hovered,
+                                    tab_style,
+                                    cx,
+                                )
+                            })
+                            .on_down(MouseButton::Left, move |_, cx| {
+                                cx.dispatch_action(ActivateItem(ix));
+                            })
+                            .on_click(MouseButton::Middle, {
+                                let item = item.clone();
+                                move |_, cx: &mut EventContext| {
+                                    cx.dispatch_action(CloseItem {
+                                        item_id: item.id(),
+                                        pane: pane.clone(),
+                                    })
+                                }
+                            })
+                            .boxed()
+                        }
+                    });
 
                 if !pane_active || !tab_active {
                     receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
                 }
 
                 receiver
-                    .on_down(MouseButton::Left, move |_, cx| {
-                        cx.dispatch_action(ActivateItem(ix));
-                        cx.propagate_event();
-                    })
-                    .on_click(MouseButton::Middle, {
-                        let item = item.clone();
-                        let pane = pane.clone();
-                        move |_, cx: &mut EventContext| {
-                            cx.dispatch_action(CloseItem {
-                                item_id: item.id(),
-                                pane: pane.clone(),
-                            })
-                        }
-                    })
                     .as_draggable(
                         DraggedItem {
                             item,
@@ -1354,7 +1407,7 @@ impl Pane {
                     } else {
                         Empty::new().boxed()
                     })
-                    .with_width(tab_style.icon_width)
+                    .with_width(tab_style.close_icon_width)
                     .boxed(),
                 )
                 .boxed(),
@@ -1373,34 +1426,59 @@ impl Pane {
     ) -> ElementBox {
         Flex::row()
             // New menu
-            .with_child(tab_bar_button(0, "icons/plus_12.svg", cx, |position| {
-                DeployNewMenu { position }
-            }))
+            .with_child(render_tab_bar_button(
+                0,
+                "icons/plus_12.svg",
+                cx,
+                DeployNewMenu,
+                self.tab_bar_context_menu
+                    .handle_if_kind(TabBarContextMenuKind::New),
+            ))
             .with_child(
                 self.docked
                     .map(|anchor| {
                         // Add the dock menu button if this pane is a dock
                         let dock_icon = icon_for_dock_anchor(anchor);
 
-                        tab_bar_button(1, dock_icon, cx, |position| DeployDockMenu { position })
+                        render_tab_bar_button(
+                            1,
+                            dock_icon,
+                            cx,
+                            DeployDockMenu,
+                            self.tab_bar_context_menu
+                                .handle_if_kind(TabBarContextMenuKind::Dock),
+                        )
                     })
                     .unwrap_or_else(|| {
                         // Add the split menu if this pane is not a dock
-                        tab_bar_button(2, "icons/split_12.svg", cx, |position| DeploySplitMenu {
-                            position,
-                        })
+                        render_tab_bar_button(
+                            2,
+                            "icons/split_12.svg",
+                            cx,
+                            DeploySplitMenu,
+                            self.tab_bar_context_menu
+                                .handle_if_kind(TabBarContextMenuKind::Split),
+                        )
                     }),
             )
             // Add the close dock button if this pane is a dock
             .with_children(
                 self.docked
-                    .map(|_| tab_bar_button(3, "icons/x_mark_8.svg", cx, |_| HideDock)),
+                    .map(|_| render_tab_bar_button(3, "icons/x_mark_8.svg", cx, HideDock, None)),
             )
             .contained()
             .with_style(theme.workspace.tab_bar.pane_button_container)
             .flex(1., false)
             .boxed()
     }
+
+    fn render_blank_pane(&mut self, theme: &Theme, _cx: &mut RenderContext<Self>) -> ElementBox {
+        let background = theme.workspace.background;
+        Empty::new()
+            .contained()
+            .with_background_color(background)
+            .boxed()
+    }
 }
 
 impl Entity for Pane {
@@ -1437,7 +1515,7 @@ impl View for Pane {
                                             .with_style(theme.workspace.tab_bar.container)
                                             .boxed()
                                     })
-                                    .on_click(MouseButton::Left, move |_, cx| {
+                                    .on_down(MouseButton::Left, move |_, cx| {
                                         cx.dispatch_action(ActivateItem(active_item_index));
                                     })
                                     .boxed(),
@@ -1471,13 +1549,14 @@ impl View for Pane {
                                     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().boxed()
+                                                }))
                                                 .with_child(
-                                                    ChildView::new(&toolbar, cx).expanded().boxed(),
-                                                )
-                                                .with_child(
-                                                    ChildView::new(active_item, cx)
+                                                    ChildView::new(active_item.as_any(), cx)
                                                         .flex(1., true)
                                                         .boxed(),
                                                 )
@@ -1493,11 +1572,8 @@ impl View for Pane {
                         enum EmptyPane {}
                         let theme = cx.global::<Settings>().theme.clone();
 
-                        dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
-                            Empty::new()
-                                .contained()
-                                .with_background_color(theme.workspace.background)
-                                .boxed()
+                        dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, cx| {
+                            self.render_blank_pane(&theme, cx)
                         })
                         .on_down(MouseButton::Left, |_, cx| {
                             cx.focus_parent_view();
@@ -1523,11 +1599,14 @@ impl View for Pane {
                 })
                 .boxed(),
             )
-            .with_child(ChildView::new(&self.tab_bar_context_menu, cx).boxed())
             .named("pane")
     }
 
     fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.toolbar.update(cx, |toolbar, cx| {
+            toolbar.pane_focus_update(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,
@@ -1536,50 +1615,71 @@ impl View for Pane {
                     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);
+                        cx.focus(&last_focused_view);
                         return;
                     } else {
                         self.last_focused_view_by_item.remove(&active_item.id());
                     }
                 }
 
-                cx.focus(active_item);
-            } else if focused != self.tab_bar_context_menu {
+                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.toolbar.update(cx, |toolbar, cx| {
+            toolbar.pane_focus_update(false, cx);
+        });
+    }
+
+    fn keymap_context(&self, _: &AppContext) -> KeymapContext {
+        let mut keymap = Self::default_keymap_context();
+        if self.docked.is_some() {
+            keymap.add_identifier("docked");
+        }
+        keymap
+    }
 }
 
-fn tab_bar_button<A: Action>(
+fn render_tab_bar_button<A: Action + Clone>(
     index: usize,
     icon: &'static str,
     cx: &mut RenderContext<Pane>,
-    action_builder: impl 'static + Fn(Vector2F) -> A,
+    action: A,
+    context_menu: Option<ViewHandle<ContextMenu>>,
 ) -> ElementBox {
     enum TabBarButton {}
 
-    MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
-        let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
-        let style = theme.pane_button.style_for(mouse_state, false);
-        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)
-            // .aligned()
-            .boxed()
-    })
-    .with_cursor_style(CursorStyle::PointingHand)
-    .on_click(MouseButton::Left, move |e, cx| {
-        cx.dispatch_action(action_builder(e.region.lower_right()));
-    })
-    .flex(1., false)
-    .boxed()
+    Stack::new()
+        .with_child(
+            MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
+                let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
+                let style = theme.pane_button.style_for(mouse_state, false);
+                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)
+                    .boxed()
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, cx| {
+                cx.dispatch_action(action.clone());
+            })
+            .boxed(),
+        )
+        .with_children(
+            context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right().boxed()),
+        )
+        .flex(1., false)
+        .boxed()
 }
 
 impl ItemNavHistory {
@@ -1683,6 +1783,93 @@ impl NavHistory {
     }
 }
 
+pub struct PaneBackdrop {
+    child_view: usize,
+    child: ElementBox,
+}
+impl PaneBackdrop {
+    pub fn new(pane_item_view: usize, child: ElementBox) -> Self {
+        PaneBackdrop {
+            child,
+            child_view: pane_item_view,
+        }
+    }
+}
+
+impl Element for PaneBackdrop {
+    type LayoutState = ();
+
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        cx: &mut gpui::LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        let size = self.child.layout(constraint, cx);
+        (size, ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        visible_bounds: RectF,
+        _: &mut Self::LayoutState,
+        cx: &mut gpui::PaintContext,
+    ) -> Self::PaintState {
+        let background = cx.global::<Settings>().theme.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::MouseButton::Left,
+                move |_, cx| {
+                    let window_id = cx.window_id;
+                    cx.focus(window_id, Some(child_view_id))
+                },
+            ),
+        );
+
+        cx.paint_layer(Some(bounds), |cx| {
+            self.child.paint(bounds.origin(), visible_bounds, cx)
+        })
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        _bounds: RectF,
+        _visible_bounds: RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        cx: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
+    fn debug(
+        &self,
+        _bounds: RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        cx: &gpui::DebugContext,
+    ) -> serde_json::Value {
+        gpui::json::json!({
+            "type": "Pane Back Drop",
+            "view": self.child_view,
+            "child": self.child.debug(cx),
+        })
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::sync::Arc;
@@ -1699,9 +1886,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1789,9 +1974,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1867,9 +2050,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // singleton view
@@ -1978,8 +2159,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) =
-            cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labled_item(&workspace, &pane, "A", cx);
@@ -2098,8 +2278,8 @@ mod tests {
                 .enumerate()
                 .map(|(ix, item)| {
                     let mut state = item
-                        .to_any()
-                        .downcast::<TestItem>()
+                        .as_any()
+                        .downcast_ref::<TestItem>()
                         .unwrap()
                         .read(cx)
                         .label

crates/workspace/src/searchable.rs 🔗

@@ -213,13 +213,13 @@ fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T
 
 impl From<Box<dyn SearchableItemHandle>> for AnyViewHandle {
     fn from(this: Box<dyn SearchableItemHandle>) -> Self {
-        this.to_any()
+        this.as_any().clone()
     }
 }
 
 impl From<&Box<dyn SearchableItemHandle>> for AnyViewHandle {
     fn from(this: &Box<dyn SearchableItemHandle>) -> Self {
-        this.to_any()
+        this.as_any().clone()
     }
 }
 
@@ -234,7 +234,7 @@ impl Eq for Box<dyn SearchableItemHandle> {}
 pub trait WeakSearchableItemHandle: WeakItemHandle {
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
 
-    fn to_any(self) -> AnyWeakViewHandle;
+    fn into_any(self) -> AnyWeakViewHandle;
 }
 
 impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
@@ -242,8 +242,8 @@ impl<T: SearchableItem> WeakSearchableItemHandle for WeakViewHandle<T> {
         Some(Box::new(self.upgrade(cx)?))
     }
 
-    fn to_any(self) -> AnyWeakViewHandle {
-        self.into()
+    fn into_any(self) -> AnyWeakViewHandle {
+        self.into_any()
     }
 }
 

crates/workspace/src/shared_screen.rs 🔗

@@ -1,23 +1,18 @@
 use crate::{
-    item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
+    item::{Item, ItemEvent},
+    ItemNavHistory, WorkspaceId,
 };
-use anyhow::{anyhow, Result};
 use call::participant::{Frame, RemoteVideoTrack};
 use client::{proto::PeerId, User};
 use futures::StreamExt;
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    AppContext, Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AppContext, Entity, MouseButton, RenderContext, Task, View, ViewContext,
 };
-use project::Project;
 use settings::Settings;
 use smallvec::SmallVec;
-use std::{
-    path::PathBuf,
-    sync::{Arc, Weak},
-};
+use std::sync::{Arc, Weak};
 
 pub enum Event {
     Close,
@@ -113,7 +108,7 @@ impl Item for SharedScreen {
                 Svg::new("icons/disable_screen_sharing_12.svg")
                     .with_color(style.label.text.color)
                     .constrained()
-                    .with_width(style.icon_width)
+                    .with_width(style.type_icon_width)
                     .aligned()
                     .contained()
                     .with_margin_right(style.spacing)
@@ -130,12 +125,6 @@ impl Item for SharedScreen {
             .boxed()
     }
 
-    fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
-
-    fn is_singleton(&self, _: &AppContext) -> bool {
-        false
-    }
-
     fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
         self.nav_history = Some(history);
     }
@@ -149,52 +138,9 @@ impl Item for SharedScreen {
         Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
     }
 
-    fn can_save(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn save(
-        &mut self,
-        _: ModelHandle<project::Project>,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Item::save called on SharedScreen")))
-    }
-
-    fn save_as(
-        &mut self,
-        _: ModelHandle<project::Project>,
-        _: PathBuf,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Item::save_as called on SharedScreen")))
-    }
-
-    fn reload(
-        &mut self,
-        _: ModelHandle<project::Project>,
-        _: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
-    }
-
     fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
         match event {
             Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
         }
     }
-
-    fn serialized_item_kind() -> Option<&'static str> {
-        None
-    }
-
-    fn deserialize(
-        _project: ModelHandle<Project>,
-        _workspace: WeakViewHandle<Workspace>,
-        _workspace_id: WorkspaceId,
-        _item_id: ItemId,
-        _cx: &mut ViewContext<Pane>,
-    ) -> Task<Result<ViewHandle<Self>>> {
-        unreachable!("Shared screen can not be deserialized")
-    }
 }

crates/workspace/src/sidebar.rs 🔗

@@ -23,7 +23,7 @@ pub trait SidebarItemHandle {
     fn id(&self) -> usize;
     fn should_show_badge(&self, cx: &AppContext) -> bool;
     fn is_focused(&self, cx: &AppContext) -> bool;
-    fn to_any(&self) -> AnyViewHandle;
+    fn as_any(&self) -> &AnyViewHandle;
 }
 
 impl<T> SidebarItemHandle for ViewHandle<T>
@@ -42,14 +42,14 @@ where
         ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
     }
 
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
+    fn as_any(&self) -> &AnyViewHandle {
+        self
     }
 }
 
 impl From<&dyn SidebarItemHandle> for AnyViewHandle {
     fn from(val: &dyn SidebarItemHandle) -> Self {
-        val.to_any()
+        val.as_any().clone()
     }
 }
 
@@ -192,7 +192,7 @@ impl View for Sidebar {
         if let Some(active_item) = self.active_item() {
             enum ResizeHandleTag {}
             let style = &cx.global::<Settings>().theme.workspace.sidebar;
-            ChildView::new(active_item.to_any(), cx)
+            ChildView::new(active_item.as_any(), cx)
                 .contained()
                 .with_style(style.container)
                 .with_resize_handle::<ResizeHandleTag, _>(
@@ -230,7 +230,7 @@ impl View for SidebarButtons {
         let tooltip_style = theme.tooltip.clone();
         let theme = &theme.workspace.status_bar.sidebar_buttons;
         let sidebar = self.sidebar.read(cx);
-        let item_style = theme.item;
+        let item_style = theme.item.clone();
         let badge_style = theme.badge;
         let active_ix = sidebar.active_item_ix;
         let is_open = sidebar.is_open;
@@ -254,7 +254,7 @@ impl View for SidebarButtons {
                         sidebar_side,
                         item_index: ix,
                     };
-                    MouseEventHandler::<Self>::new(ix, cx, move |state, cx| {
+                    MouseEventHandler::<Self>::new(ix, cx, |state, cx| {
                         let is_active = is_open && ix == active_ix;
                         let style = item_style.style_for(state, is_active);
                         Stack::new()

crates/workspace/src/status_bar.rs 🔗

@@ -14,7 +14,7 @@ pub trait StatusItemView: View {
 }
 
 trait StatusItemViewHandle {
-    fn to_any(&self) -> AnyViewHandle;
+    fn as_any(&self) -> &AnyViewHandle;
     fn set_active_pane_item(
         &self,
         active_pane_item: Option<&dyn ItemHandle>,
@@ -42,14 +42,14 @@ impl View for StatusBar {
         let theme = &cx.global::<Settings>().theme.workspace.status_bar;
         Flex::row()
             .with_children(self.left_items.iter().map(|i| {
-                ChildView::new(i.as_ref(), cx)
+                ChildView::new(i.as_any(), cx)
                     .aligned()
                     .contained()
                     .with_margin_right(theme.item_spacing)
                     .boxed()
             }))
             .with_children(self.right_items.iter().rev().map(|i| {
-                ChildView::new(i.as_ref(), cx)
+                ChildView::new(i.as_any(), cx)
                     .aligned()
                     .contained()
                     .with_margin_left(theme.item_spacing)
@@ -81,7 +81,7 @@ impl StatusBar {
     where
         T: 'static + StatusItemView,
     {
-        cx.reparent(&item);
+        cx.reparent(item.as_any());
         self.left_items.push(Box::new(item));
         cx.notify();
     }
@@ -90,7 +90,7 @@ impl StatusBar {
     where
         T: 'static + StatusItemView,
     {
-        cx.reparent(&item);
+        cx.reparent(item.as_any());
         self.right_items.push(Box::new(item));
         cx.notify();
     }
@@ -111,8 +111,8 @@ impl StatusBar {
 }
 
 impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
+    fn as_any(&self) -> &AnyViewHandle {
+        self
     }
 
     fn set_active_pane_item(
@@ -128,6 +128,6 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
 
 impl From<&dyn StatusItemViewHandle> for AnyViewHandle {
     fn from(val: &dyn StatusItemViewHandle) -> Self {
-        val.to_any()
+        val.as_any().clone()
     }
 }

crates/workspace/src/toolbar.rs 🔗

@@ -20,16 +20,19 @@ pub trait ToolbarItemView: View {
     ) -> ToolbarItemLocation {
         current_location
     }
+
+    fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut MutableAppContext) {}
 }
 
 trait ToolbarItemViewHandle {
     fn id(&self) -> usize;
-    fn to_any(&self) -> AnyViewHandle;
+    fn as_any(&self) -> &AnyViewHandle;
     fn set_active_pane_item(
         &self,
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut MutableAppContext,
     ) -> ToolbarItemLocation;
+    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext);
 }
 
 #[derive(Copy, Clone, Debug, PartialEq)]
@@ -42,6 +45,7 @@ pub enum ToolbarItemLocation {
 
 pub struct Toolbar {
     active_pane_item: Option<Box<dyn ItemHandle>>,
+    hidden: bool,
     pane: WeakViewHandle<Pane>,
     items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 }
@@ -67,7 +71,7 @@ impl View for Toolbar {
             match *position {
                 ToolbarItemLocation::Hidden => {}
                 ToolbarItemLocation::PrimaryLeft { flex } => {
-                    let left_item = ChildView::new(item.as_ref(), cx)
+                    let left_item = ChildView::new(item.as_any(), cx)
                         .aligned()
                         .contained()
                         .with_margin_right(spacing);
@@ -78,7 +82,7 @@ impl View for Toolbar {
                     }
                 }
                 ToolbarItemLocation::PrimaryRight { flex } => {
-                    let right_item = ChildView::new(item.as_ref(), cx)
+                    let right_item = ChildView::new(item.as_any(), cx)
                         .aligned()
                         .contained()
                         .with_margin_left(spacing)
@@ -91,7 +95,7 @@ impl View for Toolbar {
                 }
                 ToolbarItemLocation::Secondary => {
                     secondary_item = Some(
-                        ChildView::new(item.as_ref(), cx)
+                        ChildView::new(item.as_any(), cx)
                             .constrained()
                             .with_height(theme.height)
                             .boxed(),
@@ -211,6 +215,7 @@ impl Toolbar {
             active_pane_item: None,
             pane,
             items: Default::default(),
+            hidden: false,
         }
     }
 
@@ -243,6 +248,12 @@ impl Toolbar {
         cx: &mut ViewContext<Self>,
     ) {
         self.active_pane_item = pane_item.map(|item| item.boxed_clone());
+        self.hidden = self
+            .active_pane_item
+            .as_ref()
+            .map(|item| !item.show_toolbar(cx))
+            .unwrap_or(false);
+
         for (toolbar_item, current_location) in self.items.iter_mut() {
             let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
             if new_location != *current_location {
@@ -252,10 +263,20 @@ impl Toolbar {
         }
     }
 
+    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext) {
+        for (toolbar_item, _) in self.items.iter_mut() {
+            toolbar_item.pane_focus_update(pane_focused, cx);
+        }
+    }
+
     pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
         self.items
             .iter()
-            .find_map(|(item, _)| item.to_any().downcast())
+            .find_map(|(item, _)| item.as_any().clone().downcast())
+    }
+
+    pub fn hidden(&self) -> bool {
+        self.hidden
     }
 }
 
@@ -264,8 +285,8 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
         self.id()
     }
 
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
+    fn as_any(&self) -> &AnyViewHandle {
+        self
     }
 
     fn set_active_pane_item(
@@ -277,10 +298,14 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
             this.set_active_pane_item(active_pane_item, cx)
         })
     }
+
+    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
+    }
 }
 
 impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
     fn from(val: &dyn ToolbarItemViewHandle) -> Self {
-        val.to_any()
+        val.as_any().clone()
     }
 }

crates/workspace/src/workspace.rs 🔗

@@ -16,7 +16,7 @@ mod toolbar;
 
 pub use smallvec;
 
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
 use client::{
     proto::{self, PeerId},
@@ -34,20 +34,24 @@ use futures::{
 use gpui::{
     actions,
     elements::*,
-    geometry::vector::Vector2F,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     impl_actions, impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, WindowOptions},
-    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
-    MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
-    SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
+    Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext,
+    ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel,
+    RenderContext, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
+    WeakViewHandle, WindowBounds,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use language::LanguageRegistry;
 use std::{
     any::TypeId,
     borrow::Cow,
-    cmp,
+    cmp, env,
     future::Future,
     path::{Path, PathBuf},
     sync::Arc,
@@ -58,8 +62,9 @@ use crate::{
     notifications::simple_message_notification::{MessageNotification, OsOpen},
     persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
 };
+use lazy_static::lazy_static;
 use log::{error, warn};
-use notifications::NotificationHandle;
+use notifications::{NotificationHandle, NotifyResultExt};
 pub use pane::*;
 pub use pane_group::*;
 use persistence::{model::SerializedItem, DB};
@@ -79,6 +84,17 @@ use theme::{Theme, ThemeRegistry};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::ResultExt;
 
+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);
+}
+
 #[derive(Clone, PartialEq)]
 pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
@@ -101,7 +117,8 @@ actions!(
         NewTerminal,
         NewSearch,
         Feedback,
-        Restart
+        Restart,
+        Welcome
     ]
 );
 
@@ -148,6 +165,67 @@ pub struct OpenProjectEntryInPane {
     project_entry: ProjectEntryId,
 }
 
+pub struct Toast {
+    id: usize,
+    msg: Cow<'static, str>,
+    click: Option<(Cow<'static, str>, Box<dyn Action>)>,
+}
+
+impl Toast {
+    pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
+        Toast {
+            id,
+            msg: msg.into(),
+            click: None,
+        }
+    }
+
+    pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>(
+        id: usize,
+        msg: I1,
+        click_msg: I2,
+        action: impl Action,
+    ) -> Self {
+        Toast {
+            id,
+            msg: msg.into(),
+            click: Some((click_msg.into(), Box::new(action))),
+        }
+    }
+}
+
+impl PartialEq for Toast {
+    fn eq(&self, other: &Self) -> bool {
+        self.id == other.id
+            && self.msg == other.msg
+            && self.click.is_some() == other.click.is_some()
+    }
+}
+
+impl Clone for Toast {
+    fn clone(&self) -> Self {
+        Toast {
+            id: self.id,
+            msg: self.msg.to_owned(),
+            click: self
+                .click
+                .as_ref()
+                .map(|(msg, click)| (msg.to_owned(), click.boxed_clone())),
+        }
+    }
+}
+
+#[derive(Clone, PartialEq)]
+pub struct DismissToast {
+    id: usize,
+}
+
+impl DismissToast {
+    pub fn new(id: usize) -> Self {
+        DismissToast { id }
+    }
+}
+
 pub type WorkspaceId = i64;
 
 impl_internal_actions!(
@@ -161,6 +239,8 @@ impl_internal_actions!(
         SplitWithItem,
         SplitWithProjectEntry,
         OpenProjectEntryInPane,
+        Toast,
+        DismissToast
     ]
 );
 impl_actions!(workspace, [ActivatePane]);
@@ -170,21 +250,66 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     dock::init(cx);
     notifications::init(cx);
 
-    cx.add_global_action(open);
+    cx.add_global_action(|_: &Open, cx: &mut MutableAppContext| {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: true,
+            directories: true,
+            multiple: true,
+        });
+
+        cx.spawn(|mut cx| async move {
+            if let Some(paths) = paths.recv().await.flatten() {
+                cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
+            }
+        })
+        .detach();
+    });
+    cx.add_action(|_, _: &Open, cx: &mut ViewContext<Workspace>| {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: true,
+            directories: true,
+            multiple: true,
+        });
+
+        let handle = cx.handle().downgrade();
+        cx.spawn(|_, mut cx| async move {
+            if let Some(paths) = paths.recv().await.flatten() {
+                cx.update(|cx| {
+                    cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths })
+                })
+            }
+        })
+        .detach();
+    });
     cx.add_global_action({
         let app_state = Arc::downgrade(&app_state);
         move |action: &OpenPaths, cx: &mut MutableAppContext| {
             if let Some(app_state) = app_state.upgrade() {
-                open_paths(&action.paths, &app_state, cx).detach();
+                open_paths(&action.paths, &app_state, None, cx).detach();
             }
         }
     });
-    cx.add_global_action({
+    cx.add_async_action({
         let app_state = Arc::downgrade(&app_state);
-        move |_: &NewFile, cx: &mut MutableAppContext| {
-            if let Some(app_state) = app_state.upgrade() {
-                open_new(&app_state, cx).detach();
+        move |workspace, action: &OpenPaths, cx: &mut ViewContext<Workspace>| {
+            if !workspace.project().read(cx).is_local() {
+                cx.propagate_action();
+                return None;
             }
+
+            let app_state = app_state.upgrade()?;
+            let window_id = cx.window_id();
+            let action = action.clone();
+            let close = workspace.prepare_to_close(false, cx);
+
+            Some(cx.spawn_weak(|_, mut cx| async move {
+                let can_close = close.await?;
+                if can_close {
+                    cx.update(|cx| open_paths(&action.paths, &app_state, Some(window_id), cx))
+                        .await;
+                }
+                Ok(())
+            }))
         }
     });
 
@@ -192,7 +317,15 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         let app_state = Arc::downgrade(&app_state);
         move |_: &NewWindow, cx: &mut MutableAppContext| {
             if let Some(app_state) = app_state.upgrade() {
-                open_new(&app_state, cx).detach();
+                open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
+            }
+        }
+    });
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |_: &NewFile, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
             }
         }
     });
@@ -235,7 +368,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.add_action(Workspace::activate_pane_at_index);
 
     cx.add_action(Workspace::split_pane_with_item);
-    cx.add_action(Workspace::split_pane_with_project_entry);
+    cx.add_async_action(Workspace::split_pane_with_project_entry);
 
     cx.add_async_action(
         |workspace: &mut Workspace,
@@ -258,6 +391,49 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         },
     );
 
+    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");
+
+            cx.update(|cx| {
+                workspace.update(cx, |workspace, cx| {
+                    if matches!(err, Err(_)) {
+                        err.notify_err(workspace, cx);
+                    } else {
+                        workspace.show_notification(1, cx, |cx| {
+                            cx.add_view(|_| {
+                                MessageNotification::new_message(
+                                    "Successfully installed the `zed` binary",
+                                )
+                            })
+                        });
+                    }
+                })
+            })
+        })
+        .detach();
+    });
+
+    cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| {
+        workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
+        workspace.show_notification(alert.id, cx, |cx| {
+            cx.add_view(|_cx| match &alert.click {
+                Some((click_msg, action)) => MessageNotification::new_boxed_action(
+                    alert.msg.clone(),
+                    action.boxed_clone(),
+                    click_msg.clone(),
+                ),
+                None => MessageNotification::new_message(alert.msg.clone()),
+            })
+        })
+    });
+
+    cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| {
+        workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
+    });
+
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
@@ -288,7 +464,7 @@ type FollowableItemBuilders = HashMap<
     TypeId,
     (
         FollowableItemBuilder,
-        fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+        fn(&AnyViewHandle) -> Box<dyn FollowableItemHandle>,
     ),
 >;
 pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
@@ -302,7 +478,7 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
                             .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
                     })
                 },
-                |this| Box::new(this.downcast::<I>().unwrap()),
+                |this| Box::new(this.clone().downcast::<I>().unwrap()),
             ),
         );
     });
@@ -343,6 +519,7 @@ pub struct AppState {
         fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
     pub dock_default_item_factory: DockDefaultItemFactory,
+    pub background_actions: BackgroundActions,
 }
 
 impl AppState {
@@ -353,7 +530,7 @@ impl AppState {
 
         let fs = fs::FakeFs::new(cx.background().clone());
         let languages = Arc::new(LanguageRegistry::test());
-        let http_client = client::test::FakeHttpClient::with_404_response();
+        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 themes = ThemeRegistry::new((), cx.font_cache().clone());
@@ -365,7 +542,8 @@ impl AppState {
             user_store,
             initialize_workspace: |_, _, _| {},
             build_window_options: |_, _, _| Default::default(),
-            dock_default_item_factory: |_, _| unimplemented!(),
+            dock_default_item_factory: |_, _| None,
+            background_actions: || &[],
         })
     }
 }
@@ -453,6 +631,8 @@ pub struct Workspace {
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
+    background_actions: BackgroundActions,
+    _window_subscriptions: [Subscription; 3],
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<()>,
 }
@@ -482,12 +662,9 @@ impl Workspace {
         workspace_id: WorkspaceId,
         project: ModelHandle<Project>,
         dock_default_factory: DockDefaultItemFactory,
+        background_actions: BackgroundActions,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
-
-        cx.observe_window_activation(Self::on_window_activation_changed)
-            .detach();
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
         cx.subscribe(&project, move |this, _, event, cx| {
             match event {
@@ -510,13 +687,21 @@ impl Workspace {
                     cx.blur();
                 }
 
+                project::Event::Closed => {
+                    let window_id = cx.window_id();
+                    cx.remove_window(window_id);
+                }
+
                 _ => {}
             }
             cx.notify()
         })
         .detach();
 
-        let center_pane = cx.add_view(|cx| Pane::new(None, cx));
+        let weak_handle = cx.weak_handle();
+
+        let center_pane =
+            cx.add_view(|cx| Pane::new(weak_handle.id(), None, background_actions, cx));
         let pane_id = center_pane.id();
         cx.subscribe(&center_pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -524,7 +709,12 @@ impl Workspace {
         .detach();
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
-        let dock = Dock::new(dock_default_factory, cx);
+        let dock = Dock::new(
+            weak_handle.id(),
+            dock_default_factory,
+            background_actions,
+            cx,
+        );
         let dock_pane = dock.pane().clone();
 
         let fs = project.read(cx).fs().clone();
@@ -547,7 +737,6 @@ impl Workspace {
             }
         });
         let handle = cx.handle();
-        let weak_handle = cx.weak_handle();
 
         // All leader updates are enqueued and then processed in a single task, so
         // that each asynchronous operation can be run in order.
@@ -592,6 +781,28 @@ impl Workspace {
             active_call = Some((call, subscriptions));
         }
 
+        let subscriptions = [
+            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);
+            }),
+        ];
+
         let mut this = Workspace {
             modal: None,
             weak_self: weak_handle.clone(),
@@ -620,9 +831,11 @@ impl Workspace {
             window_edited: false,
             active_call,
             database_id: workspace_id,
+            background_actions,
             _observe_current_user,
             _apply_leader_updates,
             leader_updates_tx,
+            _window_subscriptions: subscriptions,
         };
         this.project_remote_id_changed(project.read(cx).remote_id(), cx);
         cx.defer(|this, cx| this.update_window_title(cx));
@@ -631,6 +844,10 @@ impl Workspace {
             cx.defer(move |_, cx| {
                 Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
             });
+        } else if project.read(cx).is_local() {
+            if cx.global::<Settings>().default_dock_anchor != DockAnchor::Expanded {
+                Dock::show(&mut this, false, cx);
+            }
         }
 
         this
@@ -639,6 +856,7 @@ impl Workspace {
     fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
+        requesting_window_id: Option<usize>,
         cx: &mut MutableAppContext,
     ) -> Task<(
         ViewHandle<Workspace>,
@@ -684,64 +902,75 @@ impl Workspace {
                 DB.next_id().await.unwrap_or(0)
             };
 
-            let (bounds, display) = serialized_workspace
-                .as_ref()
-                .and_then(|sw| sw.bounds.zip(sw.display))
-                .and_then(|(mut bounds, display)| {
-                    // 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();
+            let window_bounds_override =
+                ZED_WINDOW_POSITION
+                    .zip(*ZED_WINDOW_SIZE)
+                    .map(|(position, size)| {
+                        WindowBounds::Fixed(RectF::new(
+                            cx.platform().screens()[0].bounds().origin() + position,
+                            size,
+                        ))
+                    });
 
-            // Use the serialized workspace to construct the new window
-            let (_, workspace) = cx.add_window(
-                (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
-                |cx| {
+            let build_workspace =
+                |cx: &mut ViewContext<Workspace>,
+                 serialized_workspace: Option<SerializedWorkspace>| {
                     let mut workspace = Workspace::new(
                         serialized_workspace,
                         workspace_id,
                         project_handle,
                         app_state.dock_default_item_factory,
+                        app_state.background_actions,
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                    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);
+                    workspace
+                };
+
+            let workspace = if let Some(window_id) = requesting_window_id {
+                cx.update(|cx| {
+                    cx.replace_root_view(window_id, |cx| build_workspace(cx, serialized_workspace))
+                })
+            } else {
+                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;
+                                }
                             }
-                        }
 
-                        cx.background()
-                            .spawn(DB.set_window_bounds(workspace_id, bounds, display))
-                            .detach_and_log_err(cx);
-                    })
-                    .detach();
-                    workspace
-                },
-            );
+                            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| build_workspace(cx, serialized_workspace),
+                )
+                .1
+            };
 
             notify_if_database_failed(&workspace, &mut cx);
 
@@ -803,16 +1032,12 @@ impl Workspace {
         &self.project
     }
 
-    pub fn client(&self) -> &Arc<Client> {
+    pub fn client(&self) -> &Client {
         &self.client
     }
 
-    pub fn set_titlebar_item(
-        &mut self,
-        item: impl Into<AnyViewHandle>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.titlebar_item = Some(item.into());
+    pub fn set_titlebar_item(&mut self, item: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.titlebar_item = Some(item);
         cx.notify();
     }
 
@@ -837,7 +1062,7 @@ impl Workspace {
         if self.project.read(cx).is_local() {
             Task::Ready(Some(callback(self, cx)))
         } else {
-            let task = Self::new_local(Vec::new(), app_state.clone(), cx);
+            let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
             cx.spawn(|_vh, mut cx| async move {
                 let (workspace, _) = task.await;
                 workspace.update(&mut cx, callback)
@@ -929,7 +1154,10 @@ impl Workspace {
                     if answer == Some(1) {
                         return anyhow::Ok(false);
                     } else {
-                        active_call.update(&mut cx, |call, cx| call.hang_up(cx))?;
+                        active_call
+                            .update(&mut cx, |call, cx| call.hang_up(cx))
+                            .await
+                            .log_err();
                     }
                 }
             }
@@ -1123,7 +1351,7 @@ impl Workspace {
         } else {
             let modal = add_view(self, cx);
             cx.focus(&modal);
-            self.modal = Some(modal.into());
+            self.modal = Some(modal.into_any());
             None
         }
     }
@@ -1259,7 +1487,7 @@ impl Workspace {
             if active_item.is_focused(cx) {
                 cx.focus_self();
             } else {
-                cx.focus(active_item.to_any());
+                cx.focus(active_item.as_any());
             }
         } else {
             cx.focus_self();
@@ -1291,7 +1519,7 @@ impl Workspace {
             if active_item.is_focused(cx) {
                 cx.focus_self();
             } else {
-                cx.focus(active_item.to_any());
+                cx.focus(active_item.as_any());
             }
         }
 
@@ -1306,18 +1534,36 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(|cx| Pane::new(None, cx));
+        let pane =
+            cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx));
         let pane_id = pane.id();
         cx.subscribe(&pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
         })
         .detach();
         self.panes.push(pane.clone());
-        cx.focus(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) {
+                Pane::add_item(self, &center_pane, item, true, true, None, cx);
+                true
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    }
+
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
         let active_pane = self.active_pane().clone();
         Pane::add_item(self, &active_pane, item, true, true, None, cx);
@@ -1438,7 +1684,7 @@ impl Workspace {
     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);
+            cx.focus(&pane);
         } else {
             self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
         }
@@ -1449,7 +1695,7 @@ impl Workspace {
         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);
+            cx.focus(&next_pane);
         }
     }
 
@@ -1458,7 +1704,7 @@ impl Workspace {
         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);
+            cx.focus(&prev_pane);
         }
     }
 
@@ -1475,7 +1721,7 @@ impl Workspace {
             self.active_item_path_changed(cx);
 
             if &pane == self.dock_pane() {
-                Dock::show(self, cx);
+                Dock::show(self, true, cx);
             } else {
                 self.last_active_center_pane = Some(pane.downgrade());
                 if self.dock.is_anchored_at(DockAnchor::Expanded) {
@@ -1555,13 +1801,17 @@ impl Workspace {
         }
 
         let item = pane.read(cx).active_item()?;
-        let new_pane = self.add_pane(cx);
-        if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
-            Pane::add_item(self, &new_pane, clone, true, true, None, cx);
-        }
-        self.center.split(&pane, &new_pane, direction).unwrap();
+        let maybe_pane_handle =
+            if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
+                let new_pane = self.add_pane(cx);
+                Pane::add_item(self, &new_pane, clone, true, true, None, cx);
+                self.center.split(&pane, &new_pane, direction).unwrap();
+                Some(new_pane)
+            } else {
+                None
+            };
         cx.notify();
-        Some(new_pane)
+        maybe_pane_handle
     }
 
     pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
@@ -1617,7 +1867,7 @@ impl Workspace {
     fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.center.remove(&pane).unwrap() {
             self.panes.retain(|p| p != &pane);
-            cx.focus(self.panes.last().unwrap().clone());
+            cx.focus(self.panes.last().unwrap());
             self.unfollow(&pane, cx);
             self.last_leaders_by_pane.remove(&pane.downgrade());
             for removed_item in pane.read(cx).items() {
@@ -1794,24 +2044,15 @@ impl Workspace {
         None
     }
 
-    pub fn is_following(&self, peer_id: PeerId) -> bool {
+    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
         self.follower_states_by_leader.contains_key(&peer_id)
     }
 
-    pub fn is_followed(&self, peer_id: PeerId) -> bool {
+    pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
         self.leader_state.followers.contains(&peer_id)
     }
 
     fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
-        let project = &self.project.read(cx);
-        let mut worktree_root_names = String::new();
-        for (i, name) in project.worktree_root_names(cx).enumerate() {
-            if i > 0 {
-                worktree_root_names.push_str(", ");
-            }
-            worktree_root_names.push_str(name);
-        }
-
         // 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(cx.window_id());
@@ -1828,16 +2069,10 @@ impl Workspace {
             MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
                 Container::new(
                     Stack::new()
-                        .with_child(
-                            Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
-                                .aligned()
-                                .left()
-                                .boxed(),
-                        )
                         .with_children(
                             self.titlebar_item
                                 .as_ref()
-                                .map(|item| ChildView::new(item, cx).aligned().right().boxed()),
+                                .map(|item| ChildView::new(item, cx).boxed()),
                         )
                         .boxed(),
                 )
@@ -1924,7 +2159,7 @@ impl Workspace {
                 MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
                     let theme = &cx.global::<Settings>().theme;
                     Label::new(
-                        "Your connection to the remote project has been lost.".to_string(),
+                        "Your connection to the remote project has been lost.",
                         theme.workspace.disconnected_overlay.text.clone(),
                     )
                     .aligned()
@@ -1952,7 +2187,7 @@ impl Workspace {
             Some(
                 Flex::column()
                     .with_children(self.notifications.iter().map(|(_, _, notification)| {
-                        ChildView::new(notification.as_ref(), cx)
+                        ChildView::new(notification.as_any(), cx)
                             .contained()
                             .with_style(theme.notification)
                             .boxed()
@@ -2249,7 +2484,7 @@ impl Workspace {
             let active_item_was_focused = pane
                 .read(cx)
                 .active_item()
-                .map(|active_item| cx.is_child_focused(active_item.to_any()))
+                .map(|active_item| cx.is_child_focused(active_item.as_any()))
                 .unwrap_or_default();
 
             if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
@@ -2476,14 +2711,14 @@ impl Workspace {
                         cx.focus_self();
 
                         if let Some(active_pane) = active_pane {
-                            cx.focus(active_pane);
+                            cx.focus(&active_pane);
                         } else {
-                            cx.focus(workspace.panes.last().unwrap().clone());
+                            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)
+                            cx.focus(&old_center_handle)
                         } else {
                             cx.focus_self()
                         }
@@ -2499,7 +2734,12 @@ impl Workspace {
                     // the focus the dock generates start generating alternating
                     // focus due to the deferred execution each triggering each other
                     cx.after_window_update(move |workspace, cx| {
-                        Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
+                        Dock::set_dock_position(
+                            workspace,
+                            serialized_workspace.dock_position,
+                            true,
+                            cx,
+                        );
                     });
 
                     cx.notify();
@@ -2511,6 +2751,11 @@ impl Workspace {
         })
         .detach();
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        Self::new(None, 0, project, |_, _| None, || &[], cx)
+    }
 }
 
 fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
@@ -2522,7 +2767,7 @@ fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAp
                         indoc::indoc! {"
                             Failed to load any database file :(
                         "},
-                        OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
+                        OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
                         "Click to let us know about this error"
                     )
                 })
@@ -2544,7 +2789,7 @@ fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAp
                                 "},
                                 backup_path
                             ),
-                            OsOpen(backup_path.to_string()),
+                            OsOpen::new(backup_path.to_string()),
                             "Click to show old database in finder",
                         )
                     })
@@ -2693,11 +2938,7 @@ impl View for Workspace {
     }
 
     fn keymap_context(&self, _: &AppContext) -> KeymapContext {
-        let mut keymap = Self::default_keymap_context();
-        if self.active_pane() == self.dock_pane() {
-            keymap.set.insert("Dock".into());
-        }
-        keymap
+        Self::default_keymap_context()
     }
 }
 
@@ -2746,20 +2987,6 @@ impl std::fmt::Debug for OpenPaths {
     }
 }
 
-fn open(_: &Open, cx: &mut MutableAppContext) {
-    let mut paths = cx.prompt_for_paths(PathPromptOptions {
-        files: true,
-        directories: true,
-        multiple: true,
-    });
-    cx.spawn(|mut cx| async move {
-        if let Some(paths) = paths.recv().await.flatten() {
-            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
-        }
-    })
-    .detach();
-}
-
 pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
 
 pub fn activate_workspace_for_project(
@@ -2786,6 +3013,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
+    requesting_window_id: Option<usize>,
     cx: &mut MutableAppContext,
 ) -> Task<(
     ViewHandle<Workspace>,
@@ -2816,7 +3044,8 @@ pub fn open_paths(
                     .contains(&false);
 
             cx.update(|cx| {
-                let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
+                let task =
+                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
 
                 cx.spawn(|mut cx| async move {
                     let (workspace, items) = task.await;
@@ -2835,19 +3064,30 @@ pub fn open_paths(
     })
 }
 
-pub fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Task<()> {
-    let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
+pub fn open_new(
+    app_state: &Arc<AppState>,
+    cx: &mut MutableAppContext,
+    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, |_, cx| {
+        workspace.update(&mut cx, |workspace, cx| {
             if opened_paths.is_empty() {
-                cx.dispatch_action(NewFile);
+                init(workspace, 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 std::{cell::RefCell, rc::Rc};
@@ -2856,17 +3096,10 @@ mod tests {
 
     use super::*;
     use fs::FakeFs;
-    use gpui::{executor::Deterministic, TestAppContext, ViewContext};
+    use gpui::{executor::Deterministic, TestAppContext};
     use project::{Project, ProjectEntryId};
     use serde_json::json;
 
-    pub fn default_item_factory(
-        _workspace: &mut Workspace,
-        _cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        unimplemented!()
-    }
-
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();

crates/zed/BundleDocumentTypes.plist 🔗

@@ -6,7 +6,7 @@
         <key>CFBundleTypeRole</key>
         <string>Editor</string>
         <key>LSHandlerRank</key>
-        <string>Default</string>
+        <string>Alternate</string>
         <key>LSItemContentTypes</key>
         <array>
             <string>public.text</string>

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.75.0"
+version = "0.81.0"
 publish = false
 
 [lib]
@@ -28,7 +28,10 @@ command_palette = { path = "../command_palette" }
 context_menu = { path = "../context_menu" }
 client = { path = "../client" }
 clock = { path = "../clock" }
+copilot = { path = "../copilot" }
+copilot_button = { path = "../copilot_button" }
 diagnostics = { path = "../diagnostics" }
+db = { path = "../db" }
 editor = { path = "../editor" }
 feedback = { path = "../feedback" }
 file_finder = { path = "../file_finder" }
@@ -38,9 +41,12 @@ fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 go_to_line = { path = "../go_to_line" }
 gpui = { path = "../gpui" }
+install_cli = { path = "../install_cli" }
 journal = { path = "../journal" }
 language = { path = "../language" }
+language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }
+node_runtime = { path = "../node_runtime" }
 outline = { path = "../outline" }
 plugin_runtime = { path = "../plugin_runtime" }
 project = { path = "../project" }
@@ -58,6 +64,7 @@ theme_testbench = { path = "../theme_testbench" }
 util = { path = "../util" }
 vim = { path = "../vim" }
 workspace = { path = "../workspace" }
+welcome = { path = "../welcome" }
 anyhow = "1.0.38"
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-tar = "0.4.2"
@@ -78,13 +85,14 @@ libc = "0.2"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 num_cpus = "1.13.0"
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
+postage = { workspace = true }
 rand = "0.8.3"
 regex = "1.5"
 rsa = "0.4"
 rust-embed = { version = "6.3", features = ["include-exclude"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
 serde_path_to_error = "0.1.4"
 simplelog = "0.9"
 smallvec = { version = "1.6", features = ["union"] }
@@ -132,7 +140,7 @@ util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 
 env_logger = "0.9"
-serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_json = { workspace = true }
 unindent = "0.1.7"
 
 [package.metadata.bundle-dev]

crates/zed/src/languages.rs 🔗

@@ -1,13 +1,14 @@
 use anyhow::Context;
 pub use language::*;
+use node_runtime::NodeRuntime;
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
+use theme::ThemeRegistry;
 
 mod c;
 mod elixir;
 mod go;
 mod html;
-mod installation;
 mod json;
 mod language_plugin;
 mod lua;
@@ -31,17 +32,21 @@ mod yaml;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
-pub fn init(languages: Arc<LanguageRegistry>) {
+pub fn init(
+    languages: Arc<LanguageRegistry>,
+    themes: Arc<ThemeRegistry>,
+    node_runtime: Arc<NodeRuntime>,
+) {
     for (name, grammar, lsp_adapter) in [
         (
             "c",
             tree_sitter_c::language(),
-            Some(Box::new(c::CLspAdapter) as Box<dyn LspAdapter>),
+            Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
         ),
         (
             "cpp",
             tree_sitter_cpp::language(),
-            Some(Box::new(c::CLspAdapter)),
+            Some(Arc::new(c::CLspAdapter)),
         ),
         (
             "css",
@@ -51,17 +56,21 @@ pub fn init(languages: Arc<LanguageRegistry>) {
         (
             "elixir",
             tree_sitter_elixir::language(),
-            Some(Box::new(elixir::ElixirLspAdapter)),
+            Some(Arc::new(elixir::ElixirLspAdapter)),
         ),
         (
             "go",
             tree_sitter_go::language(),
-            Some(Box::new(go::GoLspAdapter)),
+            Some(Arc::new(go::GoLspAdapter)),
         ),
         (
             "json",
             tree_sitter_json::language(),
-            Some(Box::new(json::JsonLspAdapter)),
+            Some(Arc::new(json::JsonLspAdapter::new(
+                node_runtime.clone(),
+                languages.clone(),
+                themes.clone(),
+            ))),
         ),
         (
             "markdown",
@@ -71,12 +80,14 @@ pub fn init(languages: Arc<LanguageRegistry>) {
         (
             "python",
             tree_sitter_python::language(),
-            Some(Box::new(python::PythonLspAdapter)),
+            Some(Arc::new(python::PythonLspAdapter::new(
+                node_runtime.clone(),
+            ))),
         ),
         (
             "rust",
             tree_sitter_rust::language(),
-            Some(Box::new(rust::RustLspAdapter)),
+            Some(Arc::new(rust::RustLspAdapter)),
         ),
         (
             "toml",
@@ -86,32 +97,38 @@ pub fn init(languages: Arc<LanguageRegistry>) {
         (
             "tsx",
             tree_sitter_typescript::language_tsx(),
-            Some(Box::new(typescript::TypeScriptLspAdapter)),
+            Some(Arc::new(typescript::TypeScriptLspAdapter::new(
+                node_runtime.clone(),
+            ))),
         ),
         (
             "typescript",
             tree_sitter_typescript::language_typescript(),
-            Some(Box::new(typescript::TypeScriptLspAdapter)),
+            Some(Arc::new(typescript::TypeScriptLspAdapter::new(
+                node_runtime.clone(),
+            ))),
         ),
         (
             "javascript",
             tree_sitter_typescript::language_tsx(),
-            Some(Box::new(typescript::TypeScriptLspAdapter)),
+            Some(Arc::new(typescript::TypeScriptLspAdapter::new(
+                node_runtime.clone(),
+            ))),
         ),
         (
             "html",
             tree_sitter_html::language(),
-            Some(Box::new(html::HtmlLspAdapter)),
+            Some(Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))),
         ),
         (
             "ruby",
             tree_sitter_ruby::language(),
-            Some(Box::new(ruby::RubyLanguageServer)),
+            Some(Arc::new(ruby::RubyLanguageServer)),
         ),
         (
             "erb",
             tree_sitter_embedded_template::language(),
-            Some(Box::new(ruby::RubyLanguageServer)),
+            Some(Arc::new(ruby::RubyLanguageServer)),
         ),
         (
             "scheme",
@@ -126,12 +143,12 @@ pub fn init(languages: Arc<LanguageRegistry>) {
         (
             "lua",
             tree_sitter_lua::language(),
-            Some(Box::new(lua::LuaLspAdapter)),
+            Some(Arc::new(lua::LuaLspAdapter)),
         ),
         (
             "yaml",
             tree_sitter_yaml::language(),
-            Some(Box::new(yaml::YamlLspAdapter)),
+            Some(Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))),
         ),
     ] {
         languages.register(name, load_config(name), grammar, lsp_adapter, load_queries);
@@ -142,7 +159,7 @@ pub fn init(languages: Arc<LanguageRegistry>) {
 pub async fn language(
     name: &str,
     grammar: tree_sitter::Language,
-    lsp_adapter: Option<Box<dyn LspAdapter>>,
+    lsp_adapter: Option<Arc<dyn LspAdapter>>,
 ) -> Arc<Language> {
     Arc::new(
         Language::new(load_config(name), Some(grammar))

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

@@ -1,13 +1,16 @@
-use super::installation::{latest_github_release, GitHubLspBinaryVersion};
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::StreamExt;
 pub use language::*;
 use smol::fs::{self, File};
 use std::{any::Any, path::PathBuf, sync::Arc};
+use util::fs::remove_matching;
+use util::github::latest_github_release;
+use util::http::HttpClient;
 use util::ResultExt;
 
+use util::github::GitHubLspBinaryVersion;
+
 pub struct CLspAdapter;
 
 #[async_trait]
@@ -39,7 +42,7 @@ impl super::LspAdapter for CLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> 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));
@@ -69,22 +72,16 @@ impl super::LspAdapter for CLspAdapter {
                 Err(anyhow!("failed to unzip clangd archive"))?;
             }
 
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
+            remove_matching(&container_dir, |entry| entry != version_dir).await;
         }
 
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_clangd_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -97,7 +94,10 @@ impl super::LspAdapter for CLspAdapter {
             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(clangd_bin)
+                Ok(LanguageServerBinary {
+                    path: clangd_bin,
+                    arguments: vec![],
+                })
             } else {
                 Err(anyhow!(
                     "missing clangd binary in directory {:?}",

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

@@ -6,21 +6,7 @@ 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 },
-    { start = "'", end = "'", close = true, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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/zed/src/languages/cpp/config.toml 🔗

@@ -6,21 +6,7 @@ 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 },
-    { start = "'", end = "'", close = true, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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/zed/src/languages/css/config.toml 🔗

@@ -5,20 +5,6 @@ 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 },
-    { start = "'", end = "'", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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"] },
 ]

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

@@ -1,14 +1,17 @@
-use super::installation::{latest_github_release, GitHubLspBinaryVersion};
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::StreamExt;
 pub use language::*;
 use lsp::{CompletionItemKind, SymbolKind};
 use smol::fs::{self, File};
 use std::{any::Any, path::PathBuf, sync::Arc};
+use util::fs::remove_matching;
+use util::github::latest_github_release;
+use util::http::HttpClient;
 use util::ResultExt;
 
+use util::github::GitHubLspBinaryVersion;
+
 pub struct ElixirLspAdapter;
 
 #[async_trait]
@@ -40,7 +43,7 @@ impl LspAdapter for ElixirLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> 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));
@@ -76,35 +79,27 @@ impl LspAdapter for ElixirLspAdapter {
                 Err(anyhow!("failed to unzip clangd archive"))?;
             }
 
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            if let Ok(metadata) = fs::metadata(&entry_path).await {
-                                if metadata.is_file() {
-                                    fs::remove_file(&entry_path).await.log_err();
-                                } else {
-                                    fs::remove_dir_all(&entry_path).await.log_err();
-                                }
-                            }
-                        }
-                    }
-                }
-            }
+            remove_matching(&container_dir, |entry| entry != version_dir).await;
         }
 
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec![],
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, 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.ok_or_else(|| anyhow!("no cached binary"))
+            last.map(|path| LanguageServerBinary {
+                path,
+                arguments: vec![],
+            })
+            .ok_or_else(|| anyhow!("no cached binary"))
         })()
         .await
         .log_err()

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

@@ -6,20 +6,6 @@ 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 },
-    { start = "'", end = "'", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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"] },
 ]

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

@@ -1,15 +1,21 @@
-use super::installation::latest_github_release;
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::StreamExt;
 pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
 use smol::{fs, process};
+use std::ffi::{OsStr, OsString};
 use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc};
+use util::fs::remove_matching;
+use util::github::latest_github_release;
+use util::http::HttpClient;
 use util::ResultExt;
 
+fn server_binary_arguments() -> Vec<OsString> {
+    vec!["-mode=stdio".into()]
+}
+
 #[derive(Copy, Clone)]
 pub struct GoLspAdapter;
 
@@ -23,10 +29,6 @@ impl super::LspAdapter for GoLspAdapter {
         LanguageServerName("gopls".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["-mode=stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
@@ -47,7 +49,7 @@ impl super::LspAdapter for GoLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<Option<String>>().unwrap();
         let this = *self;
 
@@ -55,20 +57,15 @@ impl super::LspAdapter for GoLspAdapter {
             let binary_path = container_dir.join(&format!("gopls_{version}"));
             if let Ok(metadata) = fs::metadata(&binary_path).await {
                 if metadata.is_file() {
-                    if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                        while let Some(entry) = entries.next().await {
-                            if let Some(entry) = entry.log_err() {
-                                let entry_path = entry.path();
-                                if entry_path.as_path() != binary_path
-                                    && entry.file_name() != "gobin"
-                                {
-                                    fs::remove_file(&entry_path).await.log_err();
-                                }
-                            }
-                        }
-                    }
+                    remove_matching(&container_dir, |entry| {
+                        entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
+                    })
+                    .await;
 
-                    return Ok(binary_path.to_path_buf());
+                    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()).await {
@@ -102,10 +99,13 @@ impl super::LspAdapter for GoLspAdapter {
         let binary_path = container_dir.join(&format!("gopls_{version}"));
         fs::rename(&installed_binary_path, &binary_path).await?;
 
-        Ok(binary_path.to_path_buf())
+        Ok(LanguageServerBinary {
+            path: binary_path.to_path_buf(),
+            arguments: server_binary_arguments(),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_binary_path = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -122,7 +122,10 @@ impl super::LspAdapter for GoLspAdapter {
             }
 
             if let Some(path) = last_binary_path {
-                Ok(path)
+                Ok(LanguageServerBinary {
+                    path,
+                    arguments: server_binary_arguments(),
+                })
             } else {
                 Err(anyhow!("no cached binary"))
             }
@@ -314,7 +317,7 @@ mod tests {
         let language = language(
             "go",
             tree_sitter_go::language(),
-            Some(Box::new(GoLspAdapter)),
+            Some(Arc::new(GoLspAdapter)),
         )
         .await;
 

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

@@ -6,21 +6,7 @@ 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 },
-    { start = "'", end = "'", close = true, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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/zed/src/languages/html.rs 🔗

@@ -1,19 +1,31 @@
-use super::installation::{npm_install_packages, npm_package_latest_version};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::StreamExt;
-use language::{LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use node_runtime::NodeRuntime;
 use serde_json::json;
 use smol::fs;
+use std::ffi::OsString;
+use std::path::Path;
 use std::{any::Any, path::PathBuf, sync::Arc};
+use util::http::HttpClient;
 use util::ResultExt;
 
-pub struct HtmlLspAdapter;
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct HtmlLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 impl HtmlLspAdapter {
-    const BIN_PATH: &'static str =
+    const SERVER_PATH: &'static str =
         "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+
+    pub fn new(node: Arc<NodeRuntime>) -> Self {
+        HtmlLspAdapter { node }
+    }
 }
 
 #[async_trait]
@@ -22,15 +34,15 @@ impl LspAdapter for HtmlLspAdapter {
         LanguageServerName("vscode-html-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version("vscode-langservers-extracted").await?) as Box<_>)
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ) as Box<_>)
     }
 
     async fn fetch_server_binary(
@@ -38,37 +50,26 @@ impl LspAdapter for HtmlLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
-        let version_dir = container_dir.join(version.as_str());
-        fs::create_dir_all(&version_dir)
-            .await
-            .context("failed to create version directory")?;
-        let binary_path = version_dir.join(Self::BIN_PATH);
-
-        if fs::metadata(&binary_path).await.is_err() {
-            npm_install_packages(
-                [("vscode-langservers-extracted", version.as_str())],
-                &version_dir,
-            )
-            .await?;
+        let server_path = container_dir.join(Self::SERVER_PATH);
 
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    [("vscode-langservers-extracted", version.as_str())],
+                    &container_dir,
+                )
+                .await?;
         }
 
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -79,9 +80,12 @@ impl LspAdapter for HtmlLspAdapter {
                 }
             }
             let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
+            let server_path = last_version_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
+                Ok(LanguageServerBinary {
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
+                })
             } else {
                 Err(anyhow!(
                     "missing executable in directory {:?}",

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

@@ -1,26 +1,12 @@
 name = "HTML"
 path_suffixes = ["html"]
 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 },
-    { start = "!--", end = " --", close = true, newline = false },
-]
-
 block_comment = ["<!-- ", " -->"]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
 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"] },
 ]

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

@@ -1,111 +0,0 @@
-use anyhow::{anyhow, Context, Result};
-use client::http::HttpClient;
-
-use serde::Deserialize;
-use smol::io::AsyncReadExt;
-use std::{path::Path, sync::Arc};
-
-pub struct GitHubLspBinaryVersion {
-    pub name: String,
-    pub url: String,
-}
-
-#[derive(Deserialize)]
-#[serde(rename_all = "kebab-case")]
-struct NpmInfo {
-    #[serde(default)]
-    dist_tags: NpmInfoDistTags,
-    versions: Vec<String>,
-}
-
-#[derive(Deserialize, Default)]
-struct NpmInfoDistTags {
-    latest: Option<String>,
-}
-
-#[derive(Deserialize)]
-pub(crate) struct GithubRelease {
-    pub name: String,
-    pub assets: Vec<GithubReleaseAsset>,
-}
-
-#[derive(Deserialize)]
-pub(crate) struct GithubReleaseAsset {
-    pub name: String,
-    pub browser_download_url: String,
-}
-
-pub async fn npm_package_latest_version(name: &str) -> Result<String> {
-    let output = smol::process::Command::new("npm")
-        .args(["-fetch-retry-mintimeout", "2000"])
-        .args(["-fetch-retry-maxtimeout", "5000"])
-        .args(["-fetch-timeout", "5000"])
-        .args(["info", name, "--json"])
-        .output()
-        .await
-        .context("failed to run npm info")?;
-    if !output.status.success() {
-        Err(anyhow!(
-            "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
-            String::from_utf8_lossy(&output.stdout),
-            String::from_utf8_lossy(&output.stderr)
-        ))?;
-    }
-    let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
-    info.dist_tags
-        .latest
-        .or_else(|| info.versions.pop())
-        .ok_or_else(|| anyhow!("no version found for npm package {}", name))
-}
-
-pub async fn npm_install_packages(
-    packages: impl IntoIterator<Item = (&str, &str)>,
-    directory: &Path,
-) -> Result<()> {
-    let output = smol::process::Command::new("npm")
-        .args(["-fetch-retry-mintimeout", "2000"])
-        .args(["-fetch-retry-maxtimeout", "5000"])
-        .args(["-fetch-timeout", "5000"])
-        .arg("install")
-        .arg("--prefix")
-        .arg(directory)
-        .args(
-            packages
-                .into_iter()
-                .map(|(name, version)| format!("{name}@{version}")),
-        )
-        .output()
-        .await
-        .context("failed to run npm install")?;
-    if !output.status.success() {
-        Err(anyhow!(
-            "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
-            String::from_utf8_lossy(&output.stdout),
-            String::from_utf8_lossy(&output.stderr)
-        ))?;
-    }
-    Ok(())
-}
-
-pub(crate) async fn latest_github_release(
-    repo_name_with_owner: &str,
-    http: Arc<dyn HttpClient>,
-) -> Result<GithubRelease, anyhow::Error> {
-    let mut response = http
-        .get(
-            &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"),
-            Default::default(),
-            true,
-        )
-        .await
-        .context("error fetching latest release")?;
-    let mut body = Vec::new();
-    response
-        .body_mut()
-        .read_to_end(&mut body)
-        .await
-        .context("error reading latest release")?;
-    let release: GithubRelease =
-        serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?;
-    Ok(release)
-}

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

@@ -6,25 +6,11 @@ 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 },
-    { start = "\"", end = "\"", close = true, newline = false },
-    { start = "'", end = "'", close = true, newline = false },
-    { start = "`", end = "`", close = true, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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"] },
 ]
 
 [overrides.element]

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

@@ -43,8 +43,10 @@
 
 ; Special identifiers
 
-((identifier) @constructor
- (#match? @constructor "^[A-Z]"))
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+(type_identifier) @type
+(predefined_type) @type.builtin
 
 ([
   (identifier)
@@ -59,12 +61,15 @@
 (super) @variable.special
 
 [
-  (true)
-  (false)
   (null)
   (undefined)
 ] @constant.builtin
 
+[
+  (true)
+  (false)
+] @boolean
+
 (comment) @comment
 
 [
@@ -72,15 +77,11 @@
   (template_string)
 ] @string
 
-(regex) @string.special
+(regex) @string.regex
 (number) @number
 
 ; Tokens
 
-(template_substitution
-  "${" @punctuation.special
-  "}" @punctuation.special) @embedded
-
 [
   ";"
   "?."
@@ -189,13 +190,9 @@
   "yield"
 ] @keyword
 
-; Types
-
-(type_identifier) @type
-(predefined_type) @type.builtin
-
-((identifier) @type
- (#match? @type "^[A-Z]"))
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
 
 (type_arguments
   "<" @punctuation.bracket

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

@@ -1,17 +1,50 @@
-use super::installation::{latest_github_release, GitHubLspBinaryVersion};
 use anyhow::{anyhow, Result};
-use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
-use client::http::HttpClient;
 use collections::HashMap;
-use futures::{io::BufReader, StreamExt};
-use language::{LanguageServerName, LspAdapter};
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui::MutableAppContext;
+use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
+use node_runtime::NodeRuntime;
 use serde_json::json;
-use smol::fs::{self, File};
-use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
-use util::ResultExt;
+use settings::{keymap_file_json_schema, settings_file_json_schema};
+use smol::fs;
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use theme::ThemeRegistry;
+use util::http::HttpClient;
+use util::{paths, ResultExt, StaffMode};
 
-pub struct JsonLspAdapter;
+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<NodeRuntime>,
+    languages: Arc<LanguageRegistry>,
+    themes: Arc<ThemeRegistry>,
+}
+
+impl JsonLspAdapter {
+    pub fn new(
+        node: Arc<NodeRuntime>,
+        languages: Arc<LanguageRegistry>,
+        themes: Arc<ThemeRegistry>,
+    ) -> Self {
+        JsonLspAdapter {
+            node,
+            languages,
+            themes,
+        }
+    }
+}
 
 #[async_trait]
 impl LspAdapter for JsonLspAdapter {
@@ -19,78 +52,65 @@ impl LspAdapter for JsonLspAdapter {
         LanguageServerName("json-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("zed-industries/json-language-server", http).await?;
-        let asset_name = format!("json-language-server-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))?;
-        let version = GitHubLspBinaryVersion {
-            name: release.name,
-            url: asset.browser_download_url.clone(),
-        };
-        Ok(Box::new(version) as Box<_>)
+        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>,
-        http: Arc<dyn HttpClient>,
+        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
-        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
-        let destination_path = container_dir.join(format!(
-            "json-language-server-{}-{}",
-            version.name,
-            consts::ARCH
-        ));
-
-        if fs::metadata(&destination_path).await.is_err() {
-            let mut response = http
-                .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?;
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().unwrap();
+        let server_path = container_dir.join(SERVER_PATH);
 
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != destination_path {
-                            fs::remove_file(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    [("vscode-json-languageserver", version.as_str())],
+                    &container_dir,
+                )
+                .await?;
         }
 
-        Ok(destination_path)
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
-            let mut last = None;
+            let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;
             while let Some(entry) = entries.next().await {
-                last = Some(entry?.path());
+                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: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
+                })
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
             }
-            last.ok_or_else(|| anyhow!("no cached binary"))
         })()
         .await
         .log_err()
@@ -102,7 +122,45 @@ impl LspAdapter for JsonLspAdapter {
         }))
     }
 
+    fn workspace_configuration(
+        &self,
+        cx: &mut MutableAppContext,
+    ) -> Option<BoxFuture<'static, serde_json::Value>> {
+        let action_names = cx.all_action_names().collect::<Vec<_>>();
+        let theme_names = self
+            .themes
+            .list(**cx.default_global::<StaffMode>())
+            .map(|meta| meta.name)
+            .collect();
+        let language_names = self.languages.language_names();
+        Some(
+            future::ready(serde_json::json!({
+                "json": {
+                    "format": {
+                        "enable": true,
+                    },
+                    "schemas": [
+                        {
+                            "fileMatch": [schema_file_match(&paths::SETTINGS)],
+                            "schema": settings_file_json_schema(theme_names, &language_names),
+                        },
+                        {
+                            "fileMatch": [schema_file_match(&paths::KEYMAP)],
+                            "schema": keymap_file_json_schema(&action_names),
+                        }
+                    ]
+                }
+            }))
+            .boxed(),
+        )
+    }
+
     async fn language_ids(&self) -> HashMap<String, String> {
         [("JSON".into(), "jsonc".into())].into_iter().collect()
     }
 }
+
+fn schema_file_match(path: &Path) -> &Path {
+    path.strip_prefix(path.parent().unwrap().parent().unwrap())
+        .unwrap()
+}

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

@@ -5,11 +5,5 @@ autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },
     { start = "[", end = "]", close = true, newline = true },
-    { start = "\"", end = "\"", close = true, newline = false },
-]
-
-[overrides.string]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]

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

@@ -1,12 +1,12 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use collections::HashMap;
 use futures::lock::Mutex;
 use gpui::executor::Background;
-use language::{LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
 use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
 use std::{any::Any, path::PathBuf, sync::Arc};
+use util::http::HttpClient;
 use util::ResultExt;
 
 #[allow(dead_code)]
@@ -32,10 +32,9 @@ pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
 
 pub struct PluginLspAdapter {
     name: WasiFn<(), String>,
-    server_args: WasiFn<(), Vec<String>>,
     fetch_latest_server_version: WasiFn<(), Option<String>>,
-    fetch_server_binary: WasiFn<(PathBuf, String), Result<PathBuf, String>>,
-    cached_server_binary: WasiFn<PathBuf, Option<PathBuf>>,
+    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>,
@@ -47,7 +46,6 @@ impl PluginLspAdapter {
     pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
         Ok(Self {
             name: plugin.function("name")?,
-            server_args: plugin.function("server_args")?,
             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")?,
@@ -72,15 +70,6 @@ impl LspAdapter for PluginLspAdapter {
         LanguageServerName(name.into())
     }
 
-    async fn server_args<'a>(&'a self) -> Vec<String> {
-        self.runtime
-            .lock()
-            .await
-            .call(&self.server_args, ())
-            .await
-            .unwrap()
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
@@ -105,7 +94,7 @@ impl LspAdapter for PluginLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = *version.downcast::<String>().unwrap();
         let runtime = self.runtime.clone();
         let function = self.fetch_server_binary;
@@ -113,7 +102,7 @@ impl LspAdapter for PluginLspAdapter {
             .spawn(async move {
                 let mut runtime = runtime.lock().await;
                 let handle = runtime.attach_path(&container_dir)?;
-                let result: Result<PathBuf, String> =
+                let result: Result<LanguageServerBinary, String> =
                     runtime.call(&function, (container_dir, version)).await?;
                 runtime.remove_resource(handle)?;
                 result.map_err(|e| anyhow!("{}", e))
@@ -121,7 +110,7 @@ impl LspAdapter for PluginLspAdapter {
             .await
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         let runtime = self.runtime.clone();
         let function = self.cached_server_binary;
 
@@ -129,7 +118,8 @@ impl LspAdapter for PluginLspAdapter {
             .spawn(async move {
                 let mut runtime = runtime.lock().await;
                 let handle = runtime.attach_path(&container_dir).ok()?;
-                let result: Option<PathBuf> = runtime.call(&function, container_dir).await.ok()?;
+                let result: Option<LanguageServerBinary> =
+                    runtime.call(&function, container_dir).await.ok()?;
                 runtime.remove_resource(handle).ok()?;
                 result
             })

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

@@ -1,33 +1,31 @@
-use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
-
 use anyhow::{anyhow, bail, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::{io::BufReader, StreamExt};
-use language::LanguageServerName;
+use language::{LanguageServerBinary, LanguageServerName};
 use smol::fs;
-use util::{async_iife, ResultExt};
+use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
+use util::{async_iife, github::latest_github_release, http::HttpClient, ResultExt};
 
-use super::installation::{latest_github_release, GitHubLspBinaryVersion};
+use util::github::GitHubLspBinaryVersion;
 
 #[derive(Copy, Clone)]
 pub struct LuaLspAdapter;
 
+fn server_binary_arguments() -> Vec<OsString> {
+    vec![
+        "--logpath=~/lua-language-server.log".into(),
+        "--loglevel=trace".into(),
+    ]
+}
+
 #[async_trait]
 impl super::LspAdapter for LuaLspAdapter {
     async fn name(&self) -> LanguageServerName {
         LanguageServerName("lua-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec![
-            "--logpath=~/lua-language-server.log".into(),
-            "--loglevel=trace".into(),
-        ]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
@@ -57,7 +55,7 @@ impl super::LspAdapter for LuaLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 
         let binary_path = container_dir.join("bin/lua-language-server");
@@ -77,10 +75,13 @@ impl super::LspAdapter for LuaLspAdapter {
             <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
         )
         .await?;
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: server_binary_arguments(),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         async_iife!({
             let mut last_binary_path = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -97,7 +98,10 @@ impl super::LspAdapter for LuaLspAdapter {
             }
 
             if let Some(path) = last_binary_path {
-                Ok(path)
+                Ok(LanguageServerBinary {
+                    path,
+                    arguments: server_binary_arguments(),
+                })
             } else {
                 Err(anyhow!("no cached binary"))
             }

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

@@ -3,13 +3,7 @@ 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 },
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
-
-[overrides.string]
-brackets = [
-{ start = "{", end = "}", close = true, newline = true },
-{ start = "[", end = "]", close = true, newline = true },
-]

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

@@ -137,7 +137,7 @@
 ;; Constants
 
 ((identifier) @constant
- (#lua-match? @constant "^[A-Z][A-Z_0-9]*$"))
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
 
 (vararg_expression) @constant
 
@@ -164,11 +164,17 @@
 
 (parameters (identifier) @parameter)
 
-(function_call name: (identifier) @function.call)
-(function_declaration name: (identifier) @function)
+(function_call
+  name: [
+    (identifier) @function
+    (dot_index_expression field: (identifier) @function)
+  ])
 
-(function_call name: (dot_index_expression field: (identifier) @function.call))
-(function_declaration name: (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)
 

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

@@ -1,17 +1,32 @@
-use super::installation::{npm_install_packages, npm_package_latest_version};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::StreamExt;
-use language::{LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use node_runtime::NodeRuntime;
 use smol::fs;
-use std::{any::Any, path::PathBuf, sync::Arc};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::http::HttpClient;
 use util::ResultExt;
 
-pub struct PythonLspAdapter;
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct PythonLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 impl PythonLspAdapter {
-    const BIN_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+    const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+
+    pub fn new(node: Arc<NodeRuntime>) -> Self {
+        PythonLspAdapter { node }
+    }
 }
 
 #[async_trait]
@@ -20,15 +35,11 @@ impl LspAdapter for PythonLspAdapter {
         LanguageServerName("pyright".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version("pyright").await?) as Box<_>)
+        Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
     }
 
     async fn fetch_server_binary(
@@ -36,33 +47,23 @@ impl LspAdapter for PythonLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
-        let version_dir = container_dir.join(version.as_str());
-        fs::create_dir_all(&version_dir)
-            .await
-            .context("failed to create version directory")?;
-        let binary_path = version_dir.join(Self::BIN_PATH);
-
-        if fs::metadata(&binary_path).await.is_err() {
-            npm_install_packages([("pyright", version.as_str())], &version_dir).await?;
-
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
+        let server_path = container_dir.join(Self::SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages([("pyright", version.as_str())], &container_dir)
+                .await?;
         }
 
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -73,9 +74,12 @@ impl LspAdapter for PythonLspAdapter {
                 }
             }
             let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
+            let server_path = last_version_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
+                Ok(LanguageServerBinary {
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
+                })
             } else {
                 Err(anyhow!(
                     "missing executable in directory {:?}",

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

@@ -2,28 +2,14 @@ name = "Python"
 path_suffixes = ["py", "pyi"]
 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 },
-  { start = "'", end = "'", close = false, newline = false },
-]
-
-auto_indent_using_last_non_empty_line = false
-increase_indent_pattern = ":\\s*$"
-decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"
-
-[overrides.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 = false, newline = false, not_in = ["string"] },
 ]
 
-[overrides.string]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
+auto_indent_using_last_non_empty_line = false
+increase_indent_pattern = ":\\s*$"
+decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"

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

@@ -3,7 +3,7 @@
 [(string)
  (here_string)
  (byte_string)] @string
-(regex) @string.special
+(regex) @string.regex
 (escape_sequence) @escape
 
 [(comment)
@@ -19,7 +19,7 @@
 (quote . (symbol)) @constant
 
 (extension) @keyword
-(lang_name) @variable.builtin
+(lang_name) @variable.special
 
 ((symbol) @operator
  (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))

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

@@ -1,8 +1,8 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
-use language::{LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
 use std::{any::Any, path::PathBuf, sync::Arc};
+use util::http::HttpClient;
 
 pub struct RubyLanguageServer;
 
@@ -12,10 +12,6 @@ impl LspAdapter for RubyLanguageServer {
         LanguageServerName("solargraph".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
@@ -28,12 +24,15 @@ impl LspAdapter for RubyLanguageServer {
         _version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         _container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         Err(anyhow!("solargraph must be installed manually"))
     }
 
-    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<PathBuf> {
-        Some("solargraph".into())
+    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "solargraph".into(),
+            arguments: vec!["stdio".into()],
+        })
     }
 
     async fn label_for_completion(

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

@@ -6,20 +6,6 @@ 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 },
-  { start = "'", end = "'", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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"] },
 ]

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

@@ -1,14 +1,15 @@
-use super::installation::{latest_github_release, GitHubLspBinaryVersion};
 use anyhow::{anyhow, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::{io::BufReader, StreamExt};
 pub use language::*;
 use lazy_static::lazy_static;
 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;
+use util::github::{latest_github_release, GitHubLspBinaryVersion};
+use util::http::HttpClient;
 use util::ResultExt;
 
 pub struct RustLspAdapter;
@@ -42,7 +43,7 @@ impl LspAdapter for RustLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 
@@ -60,29 +61,26 @@ impl LspAdapter for RustLspAdapter {
             )
             .await?;
 
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != destination_path {
-                            fs::remove_file(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
+            remove_matching(&container_dir, |entry| entry != destination_path).await;
         }
 
-        Ok(destination_path)
+        Ok(LanguageServerBinary {
+            path: destination_path,
+            arguments: Default::default(),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, 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.ok_or_else(|| anyhow!("no cached binary"))
+            anyhow::Ok(LanguageServerBinary {
+                path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+                arguments: Default::default(),
+            })
         })()
         .await
         .log_err()
@@ -306,7 +304,7 @@ mod tests {
         let language = language(
             "rust",
             tree_sitter_rust::language(),
-            Some(Box::new(RustLspAdapter)),
+            Some(Arc::new(RustLspAdapter)),
         )
         .await;
         let grammar = language.grammar().unwrap();
@@ -392,7 +390,7 @@ mod tests {
         let language = language(
             "rust",
             tree_sitter_rust::language(),
-            Some(Box::new(RustLspAdapter)),
+            Some(Arc::new(RustLspAdapter)),
         )
         .await;
         let grammar = language.grammar().unwrap();

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

@@ -6,24 +6,7 @@ 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 },
-    { start = "\"", end = "\"", close = true, newline = false },
-    { start = "'", end = "'", close = false, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
-]
-
-[overrides.comment]
-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 },
-]
-
-[overrides.string]
-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 },
+    { 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"] },
 ]

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

@@ -5,17 +5,5 @@ autoclose_before = "])"
 brackets = [
     { start = "[", end = "]", close = true, newline = false },
     { start = "(", end = ")", close = true, newline = false },
-    { start = "\"", end = "\"", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-]
-
-[overrides.string]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
 ]

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

@@ -5,18 +5,6 @@ autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },
     { start = "[", end = "]", close = true, newline = true },
-    { start = "\"", end = "\"", close = true, newline = false },
-    { start = "'", end = "'", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-]
-
-[overrides.string]
-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/zed/src/languages/tsx/config.toml 🔗

@@ -6,18 +6,13 @@ 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 },
-    { start = "\"", end = "\"", close = true, newline = false },
-    { start = "'", end = "'", close = true, newline = false },
-    { start = "`", end = "`", close = true, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
+    { 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"] },
 ]
 
 [overrides.element]
 line_comment = { remove = true }
 block_comment = ["{/* ", " */}"]
-
-[overrides.string]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-]

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

@@ -1,19 +1,40 @@
-use super::installation::{npm_install_packages, npm_package_latest_version};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
 use futures::StreamExt;
-use language::{LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use lsp::CodeActionKind;
+use node_runtime::NodeRuntime;
 use serde_json::json;
 use smol::fs;
-use std::{any::Any, path::PathBuf, sync::Arc};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::http::HttpClient;
 use util::ResultExt;
 
-pub struct TypeScriptLspAdapter;
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![
+        server_path.into(),
+        "--stdio".into(),
+        "--tsserver-path".into(),
+        "node_modules/typescript/lib".into(),
+    ]
+}
+
+pub struct TypeScriptLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 impl TypeScriptLspAdapter {
-    const OLD_BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
-    const NEW_BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
+    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<NodeRuntime>) -> Self {
+        TypeScriptLspAdapter { node }
+    }
 }
 
 struct Versions {
@@ -27,20 +48,16 @@ impl LspAdapter for TypeScriptLspAdapter {
         LanguageServerName("typescript-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        ["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
-            .into_iter()
-            .map(str::to_string)
-            .collect()
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(Versions {
-            typescript_version: npm_package_latest_version("typescript").await?,
-            server_version: npm_package_latest_version("typescript-language-server").await?,
+            typescript_version: self.node.npm_package_latest_version("typescript").await?,
+            server_version: self
+                .node
+                .npm_package_latest_version("typescript-language-server")
+                .await?,
         }) as Box<_>)
     }
 
@@ -49,46 +66,32 @@ impl LspAdapter for TypeScriptLspAdapter {
         versions: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let versions = versions.downcast::<Versions>().unwrap();
-        let version_dir = container_dir.join(&format!(
-            "typescript-{}:server-{}",
-            versions.typescript_version, versions.server_version
-        ));
-        fs::create_dir_all(&version_dir)
-            .await
-            .context("failed to create version directory")?;
-        let binary_path = version_dir.join(Self::NEW_BIN_PATH);
-
-        if fs::metadata(&binary_path).await.is_err() {
-            npm_install_packages(
-                [
-                    ("typescript", versions.typescript_version.as_str()),
-                    (
-                        "typescript-language-server",
-                        versions.server_version.as_str(),
-                    ),
-                ],
-                &version_dir,
-            )
-            .await?;
-
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
+        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
+
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages(
+                    [
+                        ("typescript", versions.typescript_version.as_str()),
+                        (
+                            "typescript-language-server",
+                            versions.server_version.as_str(),
+                        ),
+                    ],
+                    &container_dir,
+                )
+                .await?;
         }
 
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -99,12 +102,18 @@ impl LspAdapter for TypeScriptLspAdapter {
                 }
             }
             let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let old_bin_path = last_version_dir.join(Self::OLD_BIN_PATH);
-            let new_bin_path = last_version_dir.join(Self::NEW_BIN_PATH);
-            if new_bin_path.exists() {
-                Ok(new_bin_path)
-            } else if old_bin_path.exists() {
-                Ok(old_bin_path)
+            let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH);
+            let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH);
+            if new_server_path.exists() {
+                Ok(LanguageServerBinary {
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&new_server_path),
+                })
+            } else if old_server_path.exists() {
+                Ok(LanguageServerBinary {
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&old_server_path),
+                })
             } else {
                 Err(anyhow!(
                     "missing executable in directory {:?}",
@@ -116,6 +125,15 @@ impl LspAdapter for TypeScriptLspAdapter {
         .log_err()
     }
 
+    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: &lsp::CompletionItem,

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

@@ -6,23 +6,9 @@ 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 },
-    { start = "\"", end = "\"", close = true, newline = false },
-    { start = "'", end = "'", close = true, newline = false },
-    { start = "`", end = "`", close = true, newline = false },
-    { start = "/*", end = " */", close = true, newline = false },
-]
-
-[overrides.comment]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-]
-
-[overrides.string]
-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"] },
 ]

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

@@ -46,6 +46,11 @@
 ((identifier) @constructor
  (#match? @constructor "^[A-Z]"))
 
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+(type_identifier) @type
+(predefined_type) @type.builtin
+
 ([
   (identifier)
   (shorthand_property_identifier)
@@ -59,12 +64,15 @@
 (super) @variable.special
 
 [
-  (true)
-  (false)
   (null)
   (undefined)
 ] @constant.builtin
 
+[
+  (true)
+  (false)
+] @boolean
+
 (comment) @comment
 
 [
@@ -72,15 +80,11 @@
   (template_string)
 ] @string
 
-(regex) @string.special
+(regex) @string.regex
 (number) @number
 
 ; Tokens
 
-(template_substitution
-  "${" @punctuation.special
-  "}" @punctuation.special) @embedded
-
 [
   ";"
   "?."
@@ -190,13 +194,9 @@
   "yield"
 ] @keyword
 
-; Types
-
-(type_identifier) @type
-(predefined_type) @type.builtin
-
-((identifier) @type
- (#match? @type "^[A-Z]"))
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
 
 (type_arguments
   "<" @punctuation.bracket

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

@@ -1,20 +1,36 @@
-use std::{any::Any, path::PathBuf, sync::Arc};
-
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use client::http::HttpClient;
-use futures::StreamExt;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui::MutableAppContext;
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use node_runtime::NodeRuntime;
+use serde_json::Value;
+use settings::Settings;
 use smol::fs;
-
-use language::{LanguageServerName, LspAdapter};
+use std::{
+    any::Any,
+    ffi::OsString,
+    future,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::http::HttpClient;
 use util::ResultExt;
 
-use super::installation::{npm_install_packages, npm_package_latest_version};
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
 
-pub struct YamlLspAdapter;
+pub struct YamlLspAdapter {
+    node: Arc<NodeRuntime>,
+}
 
 impl YamlLspAdapter {
-    const BIN_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+    const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+
+    pub fn new(node: Arc<NodeRuntime>) -> Self {
+        YamlLspAdapter { node }
+    }
 }
 
 #[async_trait]
@@ -23,15 +39,15 @@ impl LspAdapter for YamlLspAdapter {
         LanguageServerName("yaml-language-server".into())
     }
 
-    async fn server_args(&self) -> Vec<String> {
-        vec!["--stdio".into()]
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: Arc<dyn HttpClient>,
     ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version("yaml-language-server").await?) as Box<_>)
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("yaml-language-server")
+                .await?,
+        ) as Box<_>)
     }
 
     async fn fetch_server_binary(
@@ -39,34 +55,23 @@ impl LspAdapter for YamlLspAdapter {
         version: Box<dyn 'static + Send + Any>,
         _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
-    ) -> Result<PathBuf> {
+    ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
-        let version_dir = container_dir.join(version.as_str());
-        fs::create_dir_all(&version_dir)
-            .await
-            .context("failed to create version directory")?;
-        let binary_path = version_dir.join(Self::BIN_PATH);
+        let server_path = container_dir.join(Self::SERVER_PATH);
 
-        if fs::metadata(&binary_path).await.is_err() {
-            npm_install_packages([("yaml-language-server", version.as_str())], &version_dir)
+        if fs::metadata(&server_path).await.is_err() {
+            self.node
+                .npm_install_packages([("yaml-language-server", version.as_str())], &container_dir)
                 .await?;
-
-            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                while let Some(entry) = entries.next().await {
-                    if let Some(entry) = entry.log_err() {
-                        let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
-                        }
-                    }
-                }
-            }
         }
 
-        Ok(binary_path)
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            arguments: server_binary_arguments(&server_path),
+        })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;
@@ -77,9 +82,12 @@ impl LspAdapter for YamlLspAdapter {
                 }
             }
             let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
+            let server_path = last_version_dir.join(Self::SERVER_PATH);
+            if server_path.exists() {
+                Ok(LanguageServerBinary {
+                    path: self.node.binary_path().await?,
+                    arguments: server_binary_arguments(&server_path),
+                })
             } else {
                 Err(anyhow!(
                     "missing executable in directory {:?}",
@@ -90,4 +98,19 @@ impl LspAdapter for YamlLspAdapter {
         .await
         .log_err()
     }
+
+    fn workspace_configuration(
+        &self,
+        cx: &mut MutableAppContext,
+    ) -> Option<BoxFuture<'static, Value>> {
+        let settings = cx.global::<Settings>();
+        Some(
+            future::ready(serde_json::json!({
+                "[yaml]": {
+                    "editor.tabSize": settings.tab_size(Some("YAML"))
+                }
+            }))
+            .boxed(),
+        )
+    }
 }

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

@@ -5,13 +5,7 @@ autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },
     { start = "[", end = "]", close = true, newline = true },
-    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
 
 increase_indent_pattern = ":\\s*[|>]?\\s*$"
-
-[overrides.string]
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-]

crates/zed/src/main.rs 🔗

@@ -8,20 +8,17 @@ use cli::{
     ipc::{self, IpcSender},
     CliRequest, CliResponse, IpcHandshake,
 };
-use client::{
-    self,
-    http::{self, HttpClient},
-    UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
-};
-
+use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use db::kvp::KEY_VALUE_STORE;
 use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
+use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
 use isahc::{config::Configurable, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
+use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use project::Fs;
 use serde_json::json;
@@ -31,20 +28,25 @@ use settings::{
 };
 use simplelog::ConfigBuilder;
 use smol::process::Command;
-use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration};
-use std::{fs::OpenOptions, os::unix::prelude::OsStrExt};
+use std::{
+    env, ffi::OsStr, fs::OpenOptions, io::Write as _, os::unix::prelude::OsStrExt, panic,
+    path::PathBuf, sync::Arc, thread, time::Duration,
+};
 use terminal_view::{get_working_directory, TerminalView};
+use util::http::{self, HttpClient};
+use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
-use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile};
+use settings::watched_json::WatchedJsonFile;
 use theme::ThemeRegistry;
 #[cfg(debug_assertions)]
 use util::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{
-    self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace,
+    self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
+    OpenPaths, Workspace,
 };
-use zed::{self, build_window_options, initialize_workspace, languages, menus};
+use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
 
 fn main() {
     let http = http::client();
@@ -78,7 +80,7 @@ fn main() {
     };
 
     let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
-    let (open_paths_tx, open_paths_rx) = mpsc::unbounded();
+    let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded();
     app.on_open_urls(move |urls, _| {
         if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
             if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
@@ -101,7 +103,8 @@ fn main() {
                 .map_err(|_| anyhow!("no listener for open urls requests"))
                 .log_err();
         }
-    });
+    })
+    .on_reopen(move |cx| cx.dispatch_global_action(NewFile));
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
@@ -118,18 +121,27 @@ fn main() {
             fs.clone(),
         ));
 
-        watch_settings_file(default_settings, settings_file_content, themes.clone(), cx);
-        upload_previous_panics(http.clone(), cx);
+        settings::watch_files(
+            default_settings,
+            settings_file_content,
+            themes.clone(),
+            keymap_file,
+            cx,
+        );
+
+        if !stdout_is_a_pty() {
+            upload_previous_panics(http.clone(), cx);
+        }
 
         let client = client::Client::new(http.clone(), cx);
         let mut languages = LanguageRegistry::new(login_shell_env_loaded);
         languages.set_executor(cx.background().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
-        languages::init(languages.clone());
-        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
+        let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
 
-        watch_keymap_file(keymap_file, cx);
+        languages::init(languages.clone(), themes.clone(), node_runtime.clone());
+        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
 
         cx.set_global(client.clone());
 
@@ -149,6 +161,7 @@ fn main() {
         terminal_view::init(cx);
         theme_testbench::init(cx);
         recent_projects::init(cx);
+        copilot::init(client.clone(), node_runtime, cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
             .detach();
@@ -176,27 +189,27 @@ fn main() {
             build_window_options,
             initialize_workspace,
             dock_default_item_factory,
+            background_actions,
         });
         auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
 
         workspace::init(app_state.clone(), cx);
 
         journal::init(app_state.clone(), cx);
+        language_selector::init(app_state.clone(), cx);
         theme_selector::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
         collab_ui::init(app_state.clone(), cx);
         feedback::init(app_state.clone(), cx);
+        welcome::init(cx);
 
         cx.set_menus(menus::menus());
 
-        cx.spawn(|cx| handle_open_paths(open_paths_rx, app_state.clone(), cx))
-            .detach();
-
         if stdout_is_a_pty() {
             cx.platform().activate(true);
             let paths = collect_path_args();
             if paths.is_empty() {
-                cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
+                cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
                     .detach()
             } else {
                 cx.dispatch_global_action(OpenPaths { paths });
@@ -205,13 +218,34 @@ fn main() {
             if let Ok(Some(connection)) = cli_connections_rx.try_next() {
                 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
                     .detach();
+            } else if let Ok(Some(paths)) = open_paths_rx.try_next() {
+                cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
+                    .detach();
             } else {
-                cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
-                    .detach()
+                cx.spawn({
+                    let app_state = app_state.clone();
+                    |cx| async move { restore_or_create_workspace(&app_state, cx).await }
+                })
+                .detach()
             }
-            cx.spawn(|cx| async move {
-                while let Some(connection) = cli_connections_rx.next().await {
-                    handle_cli_connection(connection, app_state.clone(), cx.clone()).await;
+
+            cx.spawn(|cx| {
+                let app_state = app_state.clone();
+                async move {
+                    while let Some(connection) = cli_connections_rx.next().await {
+                        handle_cli_connection(connection, app_state.clone(), cx.clone()).await;
+                    }
+                }
+            })
+            .detach();
+
+            cx.spawn(|mut cx| {
+                let app_state = app_state.clone();
+                async move {
+                    while let Some(paths) = open_paths_rx.next().await {
+                        cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
+                            .detach();
+                    }
                 }
             })
             .detach();
@@ -231,13 +265,15 @@ fn main() {
     });
 }
 
-async fn restore_or_create_workspace(mut cx: AsyncAppContext) {
+async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
     if let Some(location) = workspace::last_opened_workspace_paths().await {
         cx.update(|cx| {
             cx.dispatch_global_action(OpenPaths {
                 paths: location.paths().as_ref().clone(),
             })
         });
+    } 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| {
             cx.dispatch_global_action(NewFile);
@@ -314,18 +350,22 @@ fn init_panic_hook(app_version: String) {
             ),
         };
 
-        let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
-        std::fs::write(
-            paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)),
-            &message,
-        )
-        .context("error writing panic to disk")
-        .log_err();
-
         if is_pty {
             eprintln!("{}", message);
-        } else {
-            log::error!(target: "panic", "{}", message);
+            return;
+        }
+
+        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", app_version, 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 {
+            write!(&mut panic_file, "{}", message).log_err();
+            panic_file.flush().log_err();
         }
     }));
 }
@@ -399,7 +439,7 @@ async fn load_login_shell_environment() -> Result<()> {
         "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")])
+        .args(["-lic", &format!("echo {marker} && /usr/bin/env -0")])
         .output()
         .await
         .context("failed to spawn login shell to source login environment variables")?;
@@ -411,7 +451,7 @@ async fn load_login_shell_environment() -> Result<()> {
 
     if let Some(env_output_start) = stdout.find(marker) {
         let env_output = &stdout[env_output_start + marker.len()..];
-        for line in env_output.lines() {
+        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..];
@@ -521,17 +561,6 @@ fn load_config_files(
     rx
 }
 
-async fn handle_open_paths(
-    mut rx: mpsc::UnboundedReceiver<Vec<PathBuf>>,
-    app_state: Arc<AppState>,
-    mut cx: AsyncAppContext,
-) {
-    while let Some(paths) = rx.next().await {
-        cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
-            .detach();
-    }
-}
-
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
@@ -578,7 +607,7 @@ async fn handle_cli_connection(
                     paths
                 };
                 let (workspace, items) = cx
-                    .update(|cx| workspace::open_paths(&paths, &app_state, cx))
+                    .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
                     .await;
 
                 let mut errored = false;
@@ -679,3 +708,13 @@ pub fn dock_default_item_factory(
 
     Some(Box::new(terminal_view))
 }
+
+pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
+    &[
+        ("Go to file", &file_finder::Toggle),
+        ("Open command palette", &command_palette::Toggle),
+        ("Focus the dock", &FocusDock),
+        ("Open recent projects", &recent_projects::OpenRecent),
+        ("Change your settings", &OpenSettings),
+    ]
+}

crates/zed/src/menus.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Menu, MenuItem};
+use gpui::{Menu, MenuItem, OsAction};
 
 #[cfg(target_os = "macos")]
 pub fn menus() -> Vec<Menu<'static>> {
@@ -6,363 +6,161 @@ pub fn menus() -> Vec<Menu<'static>> {
         Menu {
             name: "Zed",
             items: vec![
-                MenuItem::Action {
-                    name: "About Zed…",
-                    action: Box::new(super::About),
-                },
-                MenuItem::Action {
-                    name: "Check for Updates",
-                    action: Box::new(auto_update::Check),
-                },
-                MenuItem::Separator,
-                MenuItem::Submenu(Menu {
+                MenuItem::action("About Zed…", super::About),
+                MenuItem::action("Check for Updates", auto_update::Check),
+                MenuItem::separator(),
+                MenuItem::submenu(Menu {
                     name: "Preferences",
                     items: vec![
-                        MenuItem::Action {
-                            name: "Open Settings",
-                            action: Box::new(super::OpenSettings),
-                        },
-                        MenuItem::Action {
-                            name: "Open Key Bindings",
-                            action: Box::new(super::OpenKeymap),
-                        },
-                        MenuItem::Action {
-                            name: "Open Default Settings",
-                            action: Box::new(super::OpenDefaultSettings),
-                        },
-                        MenuItem::Action {
-                            name: "Open Default Key Bindings",
-                            action: Box::new(super::OpenDefaultKeymap),
-                        },
-                        MenuItem::Action {
-                            name: "Select Theme",
-                            action: Box::new(theme_selector::Toggle),
-                        },
+                        MenuItem::action("Open Settings", super::OpenSettings),
+                        MenuItem::action("Open Key Bindings", super::OpenKeymap),
+                        MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
+                        MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
+                        MenuItem::action("Select Theme", theme_selector::Toggle),
                     ],
                 }),
-                MenuItem::Action {
-                    name: "Install CLI",
-                    action: Box::new(super::InstallCommandLineInterface),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Hide Zed",
-                    action: Box::new(super::Hide),
-                },
-                MenuItem::Action {
-                    name: "Hide Others",
-                    action: Box::new(super::HideOthers),
-                },
-                MenuItem::Action {
-                    name: "Show All",
-                    action: Box::new(super::ShowAll),
-                },
-                MenuItem::Action {
-                    name: "Quit",
-                    action: Box::new(super::Quit),
-                },
+                MenuItem::action("Install CLI", install_cli::Install),
+                MenuItem::separator(),
+                MenuItem::action("Hide Zed", super::Hide),
+                MenuItem::action("Hide Others", super::HideOthers),
+                MenuItem::action("Show All", super::ShowAll),
+                MenuItem::action("Quit", super::Quit),
             ],
         },
         Menu {
             name: "File",
             items: vec![
-                MenuItem::Action {
-                    name: "New",
-                    action: Box::new(workspace::NewFile),
-                },
-                MenuItem::Action {
-                    name: "New Window",
-                    action: Box::new(workspace::NewWindow),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Open…",
-                    action: Box::new(workspace::Open),
-                },
-                MenuItem::Action {
-                    name: "Open Recent...",
-                    action: Box::new(recent_projects::OpenRecent),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Add Folder to Project…",
-                    action: Box::new(workspace::AddFolderToProject),
-                },
-                MenuItem::Action {
-                    name: "Save",
-                    action: Box::new(workspace::Save),
-                },
-                MenuItem::Action {
-                    name: "Save As…",
-                    action: Box::new(workspace::SaveAs),
-                },
-                MenuItem::Action {
-                    name: "Save All",
-                    action: Box::new(workspace::SaveAll),
-                },
-                MenuItem::Action {
-                    name: "Close Editor",
-                    action: Box::new(workspace::CloseActiveItem),
-                },
-                MenuItem::Action {
-                    name: "Close Window",
-                    action: Box::new(workspace::CloseWindow),
-                },
+                MenuItem::action("New", workspace::NewFile),
+                MenuItem::action("New Window", workspace::NewWindow),
+                MenuItem::separator(),
+                MenuItem::action("Open…", workspace::Open),
+                MenuItem::action("Open Recent...", recent_projects::OpenRecent),
+                MenuItem::separator(),
+                MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
+                MenuItem::action("Save", workspace::Save),
+                MenuItem::action("Save As…", workspace::SaveAs),
+                MenuItem::action("Save All", workspace::SaveAll),
+                MenuItem::action("Close Editor", workspace::CloseActiveItem),
+                MenuItem::action("Close Window", workspace::CloseWindow),
             ],
         },
         Menu {
             name: "Edit",
             items: vec![
-                MenuItem::Action {
-                    name: "Undo",
-                    action: Box::new(editor::Undo),
-                },
-                MenuItem::Action {
-                    name: "Redo",
-                    action: Box::new(editor::Redo),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Cut",
-                    action: Box::new(editor::Cut),
-                },
-                MenuItem::Action {
-                    name: "Copy",
-                    action: Box::new(editor::Copy),
-                },
-                MenuItem::Action {
-                    name: "Paste",
-                    action: Box::new(editor::Paste),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Find",
-                    action: Box::new(search::buffer_search::Deploy { focus: true }),
-                },
-                MenuItem::Action {
-                    name: "Find In Project",
-                    action: Box::new(workspace::NewSearch),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Toggle Line Comment",
-                    action: Box::new(editor::ToggleComments::default()),
-                },
-                MenuItem::Action {
-                    name: "Emoji & Symbols",
-                    action: Box::new(editor::ShowCharacterPalette),
-                },
+                MenuItem::os_action("Undo", editor::Undo, OsAction::Undo),
+                MenuItem::os_action("Redo", editor::Redo, OsAction::Redo),
+                MenuItem::separator(),
+                MenuItem::os_action("Cut", editor::Cut, OsAction::Cut),
+                MenuItem::os_action("Copy", editor::Copy, OsAction::Copy),
+                MenuItem::os_action("Paste", editor::Paste, OsAction::Paste),
+                MenuItem::separator(),
+                MenuItem::action("Find", search::buffer_search::Deploy { focus: true }),
+                MenuItem::action("Find In Project", workspace::NewSearch),
+                MenuItem::separator(),
+                MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()),
+                MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette),
             ],
         },
         Menu {
             name: "Selection",
             items: vec![
-                MenuItem::Action {
-                    name: "Select All",
-                    action: Box::new(editor::SelectAll),
-                },
-                MenuItem::Action {
-                    name: "Expand Selection",
-                    action: Box::new(editor::SelectLargerSyntaxNode),
-                },
-                MenuItem::Action {
-                    name: "Shrink Selection",
-                    action: Box::new(editor::SelectSmallerSyntaxNode),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Add Cursor Above",
-                    action: Box::new(editor::AddSelectionAbove),
-                },
-                MenuItem::Action {
-                    name: "Add Cursor Below",
-                    action: Box::new(editor::AddSelectionBelow),
-                },
-                MenuItem::Action {
-                    name: "Select Next Occurrence",
-                    action: Box::new(editor::SelectNext {
+                MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll),
+                MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode),
+                MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode),
+                MenuItem::separator(),
+                MenuItem::action("Add Cursor Above", editor::AddSelectionAbove),
+                MenuItem::action("Add Cursor Below", editor::AddSelectionBelow),
+                MenuItem::action(
+                    "Select Next Occurrence",
+                    editor::SelectNext {
                         replace_newest: false,
-                    }),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Move Line Up",
-                    action: Box::new(editor::MoveLineUp),
-                },
-                MenuItem::Action {
-                    name: "Move Line Down",
-                    action: Box::new(editor::MoveLineDown),
-                },
-                MenuItem::Action {
-                    name: "Duplicate Selection",
-                    action: Box::new(editor::DuplicateLine),
-                },
+                    },
+                ),
+                MenuItem::separator(),
+                MenuItem::action("Move Line Up", editor::MoveLineUp),
+                MenuItem::action("Move Line Down", editor::MoveLineDown),
+                MenuItem::action("Duplicate Selection", editor::DuplicateLine),
             ],
         },
         Menu {
             name: "View",
             items: vec![
-                MenuItem::Action {
-                    name: "Zoom In",
-                    action: Box::new(super::IncreaseBufferFontSize),
-                },
-                MenuItem::Action {
-                    name: "Zoom Out",
-                    action: Box::new(super::DecreaseBufferFontSize),
-                },
-                MenuItem::Action {
-                    name: "Reset Zoom",
-                    action: Box::new(super::ResetBufferFontSize),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Toggle Left Sidebar",
-                    action: Box::new(workspace::ToggleLeftSidebar),
-                },
-                MenuItem::Submenu(Menu {
+                MenuItem::action("Zoom In", super::IncreaseBufferFontSize),
+                MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
+                MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
+                MenuItem::separator(),
+                MenuItem::action("Toggle Left Sidebar", workspace::ToggleLeftSidebar),
+                MenuItem::submenu(Menu {
                     name: "Editor Layout",
                     items: vec![
-                        MenuItem::Action {
-                            name: "Split Up",
-                            action: Box::new(workspace::SplitUp),
-                        },
-                        MenuItem::Action {
-                            name: "Split Down",
-                            action: Box::new(workspace::SplitDown),
-                        },
-                        MenuItem::Action {
-                            name: "Split Left",
-                            action: Box::new(workspace::SplitLeft),
-                        },
-                        MenuItem::Action {
-                            name: "Split Right",
-                            action: Box::new(workspace::SplitRight),
-                        },
+                        MenuItem::action("Split Up", workspace::SplitUp),
+                        MenuItem::action("Split Down", workspace::SplitDown),
+                        MenuItem::action("Split Left", workspace::SplitLeft),
+                        MenuItem::action("Split Right", workspace::SplitRight),
                     ],
                 }),
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Project Panel",
-                    action: Box::new(project_panel::ToggleFocus),
-                },
-                MenuItem::Action {
-                    name: "Command Palette",
-                    action: Box::new(command_palette::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Diagnostics",
-                    action: Box::new(diagnostics::Deploy),
-                },
-                MenuItem::Separator,
+                MenuItem::separator(),
+                MenuItem::action("Project Panel", project_panel::ToggleFocus),
+                MenuItem::action("Command Palette", command_palette::Toggle),
+                MenuItem::action("Diagnostics", diagnostics::Deploy),
+                MenuItem::separator(),
             ],
         },
         Menu {
             name: "Go",
             items: vec![
-                MenuItem::Action {
-                    name: "Back",
-                    action: Box::new(workspace::GoBack { pane: None }),
-                },
-                MenuItem::Action {
-                    name: "Forward",
-                    action: Box::new(workspace::GoForward { pane: None }),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Go to File",
-                    action: Box::new(file_finder::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Go to Symbol in Project",
-                    action: Box::new(project_symbols::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Go to Symbol in Editor",
-                    action: Box::new(outline::Toggle),
-                },
-                MenuItem::Action {
-                    name: "Go to Definition",
-                    action: Box::new(editor::GoToDefinition),
-                },
-                MenuItem::Action {
-                    name: "Go to Type Definition",
-                    action: Box::new(editor::GoToTypeDefinition),
-                },
-                MenuItem::Action {
-                    name: "Find All References",
-                    action: Box::new(editor::FindAllReferences),
-                },
-                MenuItem::Action {
-                    name: "Go to Line/Column",
-                    action: Box::new(go_to_line::Toggle),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Next Problem",
-                    action: Box::new(editor::GoToDiagnostic),
-                },
-                MenuItem::Action {
-                    name: "Previous Problem",
-                    action: Box::new(editor::GoToPrevDiagnostic),
-                },
+                MenuItem::action("Back", workspace::GoBack { pane: None }),
+                MenuItem::action("Forward", workspace::GoForward { pane: None }),
+                MenuItem::separator(),
+                MenuItem::action("Go to File", file_finder::Toggle),
+                MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
+                MenuItem::action("Go to Symbol in Editor", outline::Toggle),
+                MenuItem::action("Go to Definition", editor::GoToDefinition),
+                MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition),
+                MenuItem::action("Find All References", editor::FindAllReferences),
+                MenuItem::action("Go to Line/Column", go_to_line::Toggle),
+                MenuItem::separator(),
+                MenuItem::action("Next Problem", editor::GoToDiagnostic),
+                MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic),
             ],
         },
         Menu {
             name: "Window",
             items: vec![
-                MenuItem::Action {
-                    name: "Minimize",
-                    action: Box::new(super::Minimize),
-                },
-                MenuItem::Action {
-                    name: "Zoom",
-                    action: Box::new(super::Zoom),
-                },
-                MenuItem::Separator,
+                MenuItem::action("Minimize", super::Minimize),
+                MenuItem::action("Zoom", super::Zoom),
+                MenuItem::separator(),
             ],
         },
         Menu {
             name: "Help",
             items: vec![
-                MenuItem::Action {
-                    name: "Command Palette",
-                    action: Box::new(command_palette::Toggle),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "View Telemetry Log",
-                    action: Box::new(crate::OpenTelemetryLog),
-                },
-                MenuItem::Action {
-                    name: "View Dependency Licenses",
-                    action: Box::new(crate::OpenLicenses),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Copy System Specs Into Clipboard",
-                    action: Box::new(feedback::CopySystemSpecsIntoClipboard),
-                },
-                MenuItem::Action {
-                    name: "File Bug Report",
-                    action: Box::new(feedback::FileBugReport),
-                },
-                MenuItem::Action {
-                    name: "Request Feature",
-                    action: Box::new(feedback::RequestFeature),
-                },
-                MenuItem::Separator,
-                MenuItem::Action {
-                    name: "Documentation",
-                    action: Box::new(crate::OpenBrowser {
+                MenuItem::action("Command Palette", command_palette::Toggle),
+                MenuItem::separator(),
+                MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
+                MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
+                MenuItem::action("Show Welcome", workspace::Welcome),
+                MenuItem::separator(),
+                MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback),
+                MenuItem::action(
+                    "Copy System Specs Into Clipboard",
+                    feedback::CopySystemSpecsIntoClipboard,
+                ),
+                MenuItem::action("File Bug Report", feedback::FileBugReport),
+                MenuItem::action("Request Feature", feedback::RequestFeature),
+                MenuItem::separator(),
+                MenuItem::action(
+                    "Documentation",
+                    crate::OpenBrowser {
                         url: "https://zed.dev/docs".into(),
-                    }),
-                },
-                MenuItem::Action {
-                    name: "Zed Twitter",
-                    action: Box::new(crate::OpenBrowser {
+                    },
+                ),
+                MenuItem::action(
+                    "Zed Twitter",
+                    crate::OpenBrowser {
                         url: "https://twitter.com/zeddotdev".into(),
-                    }),
-                },
+                    },
+                ),
             ],
         },
     ]

crates/zed/src/zed.rs 🔗

@@ -2,11 +2,11 @@ pub mod languages;
 pub mod menus;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
-use anyhow::{anyhow, Context, Result};
+use anyhow::Context;
 use assets::Assets;
 use breadcrumbs::Breadcrumbs;
 pub use client;
-use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu};
+use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
 use collections::VecDeque;
 pub use editor;
 use editor::{Editor, MultiBuffer};
@@ -17,25 +17,22 @@ use feedback::{
 use futures::StreamExt;
 use gpui::{
     actions,
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
+    geometry::vector::vec2f,
     impl_actions,
     platform::{WindowBounds, WindowOptions},
-    AssetSource, AsyncAppContext, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
+    AssetSource, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
 };
 use language::Rope;
-use lazy_static::lazy_static;
 pub use lsp;
 pub use project;
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
 use serde::Deserialize;
 use serde_json::to_string_pretty;
-use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
+use settings::Settings;
 use std::{borrow::Cow, env, path::Path, str, sync::Arc};
-use util::{channel::ReleaseChannel, paths, ResultExt, StaffMode};
+use terminal_view::terminal_button::{self, TerminalButton};
+use util::{channel::ReleaseChannel, paths, ResultExt};
 use uuid::Uuid;
 pub use workspace;
 use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace};
@@ -69,25 +66,14 @@ actions!(
         IncreaseBufferFontSize,
         DecreaseBufferFontSize,
         ResetBufferFontSize,
-        InstallCommandLineInterface,
         ResetDatabase,
     ]
 );
 
 const MIN_FONT_SIZE: f32 = 6.0;
 
-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 fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
+    terminal_button::init(cx);
     cx.add_action(about);
     cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
         cx.platform().hide();
@@ -114,9 +100,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
         },
     );
     cx.add_action(
-        |workspace: &mut Workspace,
-         _: &ToggleCollaborationMenu,
-         cx: &mut ViewContext<Workspace>| {
+        |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
             if let Some(item) = workspace
                 .titlebar_item()
                 .and_then(|item| item.downcast::<CollabTitlebarItem>())
@@ -157,9 +141,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             cx.refresh_windows();
         });
     });
-    cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
-        cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
-            .detach_and_log_err(cx);
+    cx.add_global_action(move |_: &install_cli::Install, cx| {
+        cx.spawn(|cx| async move {
+            install_cli::install_cli(&cx)
+                .await
+                .context("error creating CLI symlink")
+        })
+        .detach_and_log_err(cx);
     });
     cx.add_action({
         let app_state = app_state.clone();
@@ -184,9 +172,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     });
     cx.add_action({
         let app_state = app_state.clone();
-        move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
+        move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
-                workspace,
                 app_state.clone(),
                 "licenses.md",
                 "Open Source License Attribution",
@@ -209,9 +196,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     });
     cx.add_action({
         let app_state = app_state.clone();
-        move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
+        move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
-                workspace,
                 app_state.clone(),
                 "keymaps/default.json",
                 "Default Key Bindings",
@@ -222,11 +208,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     });
     cx.add_action({
         let app_state = app_state.clone();
-        move |workspace: &mut Workspace,
-              _: &OpenDefaultSettings,
-              cx: &mut ViewContext<Workspace>| {
+        move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
-                workspace,
                 app_state.clone(),
                 "settings/default.json",
                 "Default Settings",
@@ -235,32 +218,41 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             );
         }
     });
-    cx.add_action(
-        |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+    cx.add_action({
+        let app_state = app_state.clone();
+        move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+            let app_state = app_state.clone();
+            let markdown = app_state.languages.language_for_name("JSON");
             let content = to_string_pretty(&cx.debug_elements()).unwrap();
-            let project = workspace.project().clone();
-            let json_language = project
-                .read(cx)
-                .languages()
-                .language_for_name("JSON")
-                .unwrap();
-            if project.read(cx).is_remote() {
-                cx.propagate_action();
-            } else if let Some(buffer) = project
-                .update(cx, |project, cx| {
-                    project.create_buffer(&content, Some(json_language), cx)
-                })
-                .log_err()
-            {
-                workspace.add_item(
-                    Box::new(
-                        cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
-                    ),
-                    cx,
-                );
-            }
-        },
-    );
+            cx.spawn(|workspace, mut cx| async move {
+                let markdown = markdown.await.log_err();
+                workspace
+                    .update(&mut cx, |workspace, cx| {
+                        workspace.with_local_workspace(&app_state, cx, move |workspace, cx| {
+                            let project = workspace.project().clone();
+
+                            let buffer = project
+                                .update(cx, |project, cx| {
+                                    project.create_buffer(&content, markdown, cx)
+                                })
+                                .expect("creating buffers on a local workspace always succeeds");
+                            let buffer = cx.add_model(|cx| {
+                                MultiBuffer::singleton(buffer, cx)
+                                    .with_title("Debug Elements".into())
+                            });
+                            workspace.add_item(
+                                Box::new(cx.add_view(|cx| {
+                                    Editor::for_multibuffer(buffer, Some(project.clone()), cx)
+                                })),
+                                cx,
+                            );
+                        })
+                    })
+                    .await;
+            })
+            .detach();
+        }
+    });
     cx.add_action(
         |workspace: &mut Workspace,
          _: &project_panel::ToggleFocus,
@@ -268,8 +260,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
         },
     );
-
     activity_indicator::init(cx);
+    copilot_button::init(cx);
     call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
     settings::KeymapFileContent::load_defaults(cx);
 }
@@ -305,37 +297,9 @@ pub fn initialize_workspace(
     cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
     cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
 
-    let theme_names = app_state
-        .themes
-        .list(**cx.default_global::<StaffMode>())
-        .map(|meta| meta.name)
-        .collect();
-    let language_names = app_state.languages.language_names();
-
-    workspace.project().update(cx, |project, cx| {
-        let action_names = cx.all_action_names().collect::<Vec<_>>();
-        project.set_language_server_settings(serde_json::json!({
-            "json": {
-                "format": {
-                    "enable": true,
-                },
-                "schemas": [
-                    {
-                        "fileMatch": [schema_file_match(&paths::SETTINGS)],
-                        "schema": settings_file_json_schema(theme_names, &language_names),
-                    },
-                    {
-                        "fileMatch": [schema_file_match(&paths::KEYMAP)],
-                        "schema": keymap_file_json_schema(&action_names),
-                    }
-                ]
-            }
-        }));
-    });
-
     let collab_titlebar_item =
         cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
-    workspace.set_titlebar_item(collab_titlebar_item, cx);
+    workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
 
     let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
     workspace.left_sidebar().update(cx, |sidebar, cx| {
@@ -347,18 +311,24 @@ pub fn initialize_workspace(
         )
     });
 
+    let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
+    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
     let diagnostic_summary =
         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
     let activity_indicator =
         activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
-    let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+    let active_buffer_language = cx.add_view(|_| language_selector::ActiveBufferLanguage::new());
     let feedback_button =
-        cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton {});
+        cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new());
+    let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
         status_bar.add_left_item(activity_indicator, cx);
-        status_bar.add_right_item(cursor_position, cx);
+        status_bar.add_right_item(toggle_terminal, cx);
         status_bar.add_right_item(feedback_button, cx);
+        status_bar.add_right_item(copilot, cx);
+        status_bar.add_right_item(active_buffer_language, cx);
+        status_bar.add_right_item(cursor_position, cx);
     });
 
     auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
@@ -379,14 +349,7 @@ pub fn build_window_options(
     display: Option<Uuid>,
     platform: &dyn Platform,
 ) -> WindowOptions<'static> {
-    let bounds = bounds
-        .or_else(|| {
-            ZED_WINDOW_POSITION
-                .zip(*ZED_WINDOW_SIZE)
-                .map(|(position, size)| WindowBounds::Fixed(RectF::new(position, size)))
-        })
-        .unwrap_or(WindowBounds::Maximized);
-
+    let bounds = bounds.unwrap_or(WindowBounds::Maximized);
     let screen = display.and_then(|display| platform.screen_by_id(display));
 
     WindowOptions {
@@ -405,7 +368,47 @@ pub fn build_window_options(
 }
 
 fn restart(_: &Restart, cx: &mut gpui::MutableAppContext) {
-    cx.platform().restart();
+    let mut workspaces = cx
+        .window_ids()
+        .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
+        .collect::<Vec<_>>();
+
+    // If multiple windows have unsaved changes, and need a save prompt,
+    // prompt in the active window before switching to a different window.
+    workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
+
+    let should_confirm = cx.global::<Settings>().confirm_quit;
+    cx.spawn(|mut cx| async move {
+        if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
+            let answer = cx
+                .prompt(
+                    workspace.window_id(),
+                    PromptLevel::Info,
+                    "Are you sure you want to restart?",
+                    &["Restart", "Cancel"],
+                )
+                .next()
+                .await;
+            if answer != Some(0) {
+                return Ok(());
+            }
+        }
+
+        // If the user cancels any save prompt, then keep the app open.
+        for workspace in workspaces {
+            if !workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.prepare_to_close(true, cx)
+                })
+                .await?
+            {
+                return Ok(());
+            }
+        }
+        cx.platform().restart();
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
 }
 
 fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
@@ -462,54 +465,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
     );
 }
 
-async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
-    let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
-    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("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"))
-    }
-}
-
 fn open_config_file(
     path: &'static Path,
     app_state: Arc<AppState>,
@@ -603,8 +558,13 @@ fn open_telemetry_log_file(
     workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
         cx.spawn_weak(|workspace, mut cx| async move {
             let workspace = workspace.upgrade(&cx)?;
-            let path = app_state.client.telemetry_log_file_path()?;
-            let log = app_state.fs.load(&path).await.log_err()?;
+
+            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
+                let path = app_state.client.telemetry_log_file_path()?;
+                app_state.fs.load(&path).await.log_err()
+            }
+
+            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
 
             const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
             let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
@@ -612,6 +572,7 @@ fn open_telemetry_log_file(
                 start_offset += newline_offset + 1;
             }
             let log_suffix = &log[start_offset..];
+            let json = app_state.languages.language_for_name("JSON").await.log_err();
 
             workspace.update(&mut cx, |workspace, cx| {
                 let project = workspace.project().clone();
@@ -619,7 +580,7 @@ fn open_telemetry_log_file(
                     .update(cx, |project, cx| project.create_buffer("", None, cx))
                     .expect("creating buffers on a local workspace always succeeds");
                 buffer.update(cx, |buffer, cx| {
-                    buffer.set_language(app_state.languages.language_for_name("JSON"), cx);
+                    buffer.set_language(json, cx);
                     buffer.edit(
                         [(
                             0..0,
@@ -652,47 +613,42 @@ fn open_telemetry_log_file(
 }
 
 fn open_bundled_file(
-    workspace: &mut Workspace,
     app_state: Arc<AppState>,
     asset_path: &'static str,
     title: &'static str,
     language: &'static str,
     cx: &mut ViewContext<Workspace>,
 ) {
-    workspace
-        .with_local_workspace(&app_state, cx, |workspace, cx| {
-            let project = workspace.project().clone();
-            let buffer = project.update(cx, |project, cx| {
-                let text = Assets::get(asset_path)
-                    .map(|f| f.data)
-                    .unwrap_or_else(|| Cow::Borrowed(b"File not found"));
-                let text = str::from_utf8(text.as_ref()).unwrap();
-                project
-                    .create_buffer(text, project.languages().language_for_name(language), cx)
-                    .expect("creating buffers on a local workspace always succeeds")
-            });
-            let buffer =
-                cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
-            workspace.add_item(
-                Box::new(
-                    cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx)),
-                ),
-                cx,
-            );
-        })
-        .detach();
-}
-
-fn schema_file_match(path: &Path) -> &Path {
-    path.strip_prefix(path.parent().unwrap().parent().unwrap())
-        .unwrap()
-}
-
-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))
+    let language = app_state.languages.language_for_name(language);
+    cx.spawn(|workspace, mut cx| async move {
+        let language = language.await.log_err();
+        workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
+                    let project = workspace.project();
+                    let buffer = project.update(cx, |project, cx| {
+                        let text = Assets::get(asset_path)
+                            .map(|f| f.data)
+                            .unwrap_or_else(|| Cow::Borrowed(b"File not found"));
+                        let text = str::from_utf8(text.as_ref()).unwrap();
+                        project
+                            .create_buffer(text, language, cx)
+                            .expect("creating buffers on a local workspace always succeeds")
+                    });
+                    let buffer = cx.add_model(|cx| {
+                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
+                    });
+                    workspace.add_item(
+                        Box::new(cx.add_view(|cx| {
+                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
+                        })),
+                        cx,
+                    );
+                })
+            })
+            .await;
+    })
+    .detach();
 }
 
 #[cfg(test)]
@@ -703,6 +659,8 @@ mod tests {
     use gpui::{
         executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle,
     };
+    use language::LanguageRegistry;
+    use node_runtime::NodeRuntime;
     use project::{Project, ProjectPath};
     use serde_json::json;
     use std::{
@@ -710,6 +668,7 @@ mod tests {
         path::{Path, PathBuf},
     };
     use theme::ThemeRegistry;
+    use util::http::FakeHttpClient;
     use workspace::{
         item::{Item, ItemHandle},
         open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
@@ -736,6 +695,10 @@ mod tests {
                         "ca": null,
                         "cb": null,
                     },
+                    "d": {
+                        "da": null,
+                        "db": null,
+                    },
                 }),
             )
             .await;
@@ -744,13 +707,14 @@ mod tests {
             open_paths(
                 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
                 &app_state,
+                None,
                 cx,
             )
         })
         .await;
         assert_eq!(cx.window_ids().len(), 1);
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await;
         assert_eq!(cx.window_ids().len(), 1);
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
@@ -764,11 +728,37 @@ mod tests {
             open_paths(
                 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
                 &app_state,
+                None,
+                cx,
+            )
+        })
+        .await;
+        assert_eq!(cx.window_ids().len(), 2);
+
+        // Replace existing windows
+        let window_id = cx.window_ids()[0];
+        cx.update(|cx| {
+            open_paths(
+                &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+                &app_state,
+                Some(window_id),
                 cx,
             )
         })
         .await;
         assert_eq!(cx.window_ids().len(), 2);
+        let workspace_1 = cx.root_view::<Workspace>(window_id).unwrap();
+        workspace_1.read_with(cx, |workspace, cx| {
+            assert_eq!(
+                workspace
+                    .worktrees(cx)
+                    .map(|w| w.read(cx).abs_path())
+                    .collect::<Vec<_>>(),
+                &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+            );
+            assert!(workspace.left_sidebar().read(cx).is_open());
+            assert!(workspace.active_pane().is_focused(cx));
+        });
     }
 
     #[gpui::test]
@@ -780,7 +770,7 @@ mod tests {
             .insert_tree("/root", json!({"a": "hey"}))
             .await;
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await;
         assert_eq!(cx.window_ids().len(), 1);
 
@@ -818,7 +808,7 @@ mod tests {
         assert!(!cx.is_window_edited(workspace.window_id()));
 
         // Opening the buffer again doesn't impact the window's edited state.
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await;
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
@@ -848,7 +838,8 @@ mod tests {
     #[gpui::test]
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
         let app_state = init(cx);
-        cx.update(|cx| open_new(&app_state, cx)).await;
+        cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
+            .await;
 
         let window_id = *cx.window_ids().first().unwrap();
         let workspace = cx.root_view::<Workspace>(window_id).unwrap();
@@ -893,9 +884,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1014,9 +1003,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1033,8 +1020,8 @@ mod tests {
                     .read(cx)
                     .active_item()
                     .unwrap()
-                    .to_any()
-                    .downcast::<Editor>()
+                    .as_any()
+                    .downcast_ref::<Editor>()
                     .unwrap()
                     .read(cx)
                     .title(cx),
@@ -1069,8 +1056,8 @@ mod tests {
                     .read(cx)
                     .active_item()
                     .unwrap()
-                    .to_any()
-                    .downcast::<Editor>()
+                    .as_any()
+                    .downcast_ref::<Editor>()
                     .unwrap()
                     .read(cx)
                     .title(cx),
@@ -1105,8 +1092,8 @@ mod tests {
                     .read(cx)
                     .active_item()
                     .unwrap()
-                    .to_any()
-                    .downcast::<Editor>()
+                    .as_any()
+                    .downcast_ref::<Editor>()
                     .unwrap()
                     .read(cx)
                     .title(cx),
@@ -1155,8 +1142,8 @@ mod tests {
                     .read(cx)
                     .active_item()
                     .unwrap()
-                    .to_any()
-                    .downcast::<Editor>()
+                    .as_any()
+                    .downcast_ref::<Editor>()
                     .unwrap()
                     .read(cx)
                     .title(cx),
@@ -1175,9 +1162,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1219,9 +1204,7 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
@@ -1310,9 +1293,7 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Create a new untitled buffer
         cx.dispatch_action(window_id, NewFile);
@@ -1365,9 +1346,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1441,15 +1420,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1713,15 +1684,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -1886,6 +1849,21 @@ mod tests {
         assert!(has_default_theme);
     }
 
+    #[gpui::test]
+    fn test_bundled_languages(cx: &mut MutableAppContext) {
+        let mut languages = LanguageRegistry::test();
+        languages.set_executor(cx.background().clone());
+        let languages = Arc::new(languages);
+        let themes = ThemeRegistry::new((), cx.font_cache().clone());
+        let http = FakeHttpClient::with_404_response();
+        let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
+        languages::init(languages.clone(), themes, node_runtime);
+        for name in languages.language_names() {
+            languages.language_for_name(&name);
+        }
+        cx.foreground().run_until_parked();
+    }
+
     fn init(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.foreground().forbid_parking();
         cx.update(|cx| {

plugins/Cargo.lock 🔗

@@ -23,6 +23,7 @@ version = "0.1.0"
 dependencies = [
  "plugin",
  "serde",
+ "serde_derive",
  "serde_json",
 ]
 
@@ -33,6 +34,7 @@ dependencies = [
  "bincode",
  "plugin_macros",
  "serde",
+ "serde_derive",
 ]
 
 [[package]]
@@ -43,6 +45,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
+ "serde_derive",
  "syn",
 ]
 

plugins/json_language/Cargo.toml 🔗

@@ -5,7 +5,8 @@ edition = "2021"
 
 [dependencies]
 plugin = { path = "../../crates/plugin" }
-serde = { version = "1.0", features = ["derive"] }
+serde = { version = "1.0", features = ["derive", "rc"] }
+serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = "1.0"
 
 [lib]

plugins/json_language/src/lib.rs 🔗

@@ -6,7 +6,7 @@ use serde::Deserialize;
 #[import]
 fn command(string: &str) -> Option<Vec<u8>>;
 
-const BIN_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
 
 #[export]
 pub fn name() -> &'static str {
@@ -38,7 +38,7 @@ pub fn fetch_server_binary(container_dir: PathBuf, version: String) -> Result<Pa
     let version_dir = container_dir.join(version.as_str());
     fs::create_dir_all(&version_dir)
         .map_err(|_| "failed to create version directory".to_string())?;
-    let binary_path = version_dir.join(BIN_PATH);
+    let binary_path = version_dir.join(SERVER_PATH);
 
     if fs::metadata(&binary_path).is_err() {
         let output = command(&format!(
@@ -76,9 +76,9 @@ pub fn cached_server_binary(container_dir: PathBuf) -> Option<PathBuf> {
     }
 
     let last_version_dir = last_version_dir?;
-    let bin_path = last_version_dir.join(BIN_PATH);
-    if bin_path.exists() {
-        Some(bin_path)
+    let server_path = last_version_dir.join(SERVER_PATH);
+    if server_path.exists() {
+        Some(server_path)
     } else {
         println!("no binary found");
         None

script/bundle 🔗

@@ -2,6 +2,24 @@
 
 set -e
 
+build_flag="--release"
+target_dir="release"
+open_result=false
+
+# If -o option is specified, the folder of the resulting dmg will be opened in finder
+# If -d is specified, Zed will be compiled in debug mode and the application's path printed
+# If -od or -do is specified Zed will be bundled in debug and the application will be run.
+while getopts 'od' flag
+do
+    case "${flag}" in
+        o) open_result=true;;
+        d)
+            build_flag="";
+            target_dir="debug"
+            ;;
+    esac
+done
+
 export ZED_BUNDLE=true
 export MACOSX_DEPLOYMENT_TARGET=10.15.7
 
@@ -12,13 +30,13 @@ rustup target add wasm32-wasi
 export CXXFLAGS="-stdlib=libc++"
 
 echo "Compiling zed binary for aarch64-apple-darwin"
-cargo build --release --package zed --target aarch64-apple-darwin
+cargo build ${build_flag} --package zed --target aarch64-apple-darwin
 echo "Compiling zed binary for x86_64-apple-darwin"
-cargo build --release --package zed --target x86_64-apple-darwin
+cargo build ${build_flag} --package zed --target x86_64-apple-darwin
 echo "Compiling cli binary for aarch64-apple-darwin"
-cargo build --release --package cli --target aarch64-apple-darwin
+cargo build ${build_flag} --package cli --target aarch64-apple-darwin
 echo "Compiling cli binary for x86_64-apple-darwin"
-cargo build --release --package cli --target x86_64-apple-darwin
+cargo build ${build_flag} --package cli --target x86_64-apple-darwin
 
 echo "Creating application bundle"
 pushd crates/zed
@@ -28,7 +46,10 @@ sed \
     -i .backup \
     "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \
     Cargo.toml
-app_path=$(cargo bundle --release --target x86_64-apple-darwin | xargs)
+app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin | xargs)
+
+echo app_path
+
 mv Cargo.toml.backup Cargo.toml
 popd
 echo "Bundled ${app_path}"
@@ -36,18 +57,18 @@ echo "Bundled ${app_path}"
 echo "Creating fat binaries"
 lipo \
     -create \
-    target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/Zed \
+    target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \
     -output \
     "${app_path}/Contents/MacOS/zed"
 lipo \
     -create \
-    target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/cli \
+    target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
     -output \
     "${app_path}/Contents/MacOS/cli"
 
 echo "Copying WebRTC.framework into the frameworks folder"
 mkdir "${app_path}/Contents/Frameworks"
-cp -R target/x86_64-apple-darwin/release/WebRTC.framework "${app_path}/Contents/Frameworks/"
+cp -R target/x86_64-apple-darwin/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
 
 mv "${app_path}/Contents/Info.plist" "${app_path}/Contents/WithoutDocumentTypes.plist"
 awk \
@@ -73,7 +94,17 @@ else
     codesign --force --deep --sign - "${app_path}" -v
 fi
 
-dmg_target_directory="target/release"
+if [ "$target_dir" = "debug" ]; then
+    if [ "$open_result" = true ]; then
+        open "$app_path"
+    else
+        echo "Created application bundle:"
+        echo "$app_path"
+    fi
+    exit 0
+fi
+
+dmg_target_directory="target/${target_dir}"
 dmg_source_directory="${dmg_target_directory}/dmg"
 dmg_file_path="${dmg_target_directory}/Zed.dmg"
 
@@ -88,16 +119,16 @@ hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZ
 # This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now.
 rm ${dmg_source_directory}/Applications
 
+echo "Adding license agreement to DMG"
+npm install --global dmg-license minimist
+dmg-license script/terms/terms-of-use.json "${dmg_file_path}"
+
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Notarizing DMG with Apple"
     npm install -g notarize-cli
-    npx notarize-cli --file ${dmg_file_path} --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD"
+    npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD"
 fi
 
-# If -o option is specified, open the $dmg_target_directory directory in Finder to reveal the DMG
-while getopts o flag
-do
-    case "${flag}" in
-        o) open $dmg_target_directory;;
-    esac
-done
+if [ "$open_result" = true ]; then
+    open $dmg_target_directory
+fi

script/generate-licenses 🔗

@@ -10,7 +10,7 @@ echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE
 
 echo "Generating theme licenses"
 cd styles
-npm ci
+npm --silent ci
 npm run --silent build-licenses >> $OUTPUT_FILE
 cd ..
 

script/start-local-collaboration 🔗

@@ -31,7 +31,7 @@ scale_factor=1
 if [[ $resolution_line =~ Retina ]]; then scale_factor=2; fi
 width=$(expr ${screen_size[0]} / 2 / $scale_factor)
 height=${screen_size[1] / $scale_factor}
-y=$(expr $height / 2)
+y=0
 
 position_1=0,${y}
 position_2=${width},${y}

script/terms/terms-of-use.rtf 🔗

@@ -0,0 +1,1568 @@
+{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff37\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi31507\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}

+{\f2\fbidi \fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;}{\f3\fbidi \froman\fcharset2\fprq2{\*\panose 05050102010706020507}Symbol;}{\f10\fbidi \fnil\fcharset2\fprq2{\*\panose 05000000000000000000}Wingdings;}

+{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}{\f37\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\f42\fbidi \froman\fcharset0\fprq2{\*\panose 00000500000000020000}Times;}

+{\f43\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030203}Lato;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}

+{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}

+{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}

+{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}

+{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f44\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f45\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}

+{\f47\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f48\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f49\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f50\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}

+{\f51\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f52\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f54\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\f55\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;}

+{\f57\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\f58\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\f59\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f60\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);}

+{\f61\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\f62\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\f64\fbidi \fmodern\fcharset238\fprq1 Courier New CE;}{\f65\fbidi \fmodern\fcharset204\fprq1 Courier New Cyr;}

+{\f67\fbidi \fmodern\fcharset161\fprq1 Courier New Greek;}{\f68\fbidi \fmodern\fcharset162\fprq1 Courier New Tur;}{\f69\fbidi \fmodern\fcharset177\fprq1 Courier New (Hebrew);}{\f70\fbidi \fmodern\fcharset178\fprq1 Courier New (Arabic);}

+{\f71\fbidi \fmodern\fcharset186\fprq1 Courier New Baltic;}{\f72\fbidi \fmodern\fcharset163\fprq1 Courier New (Vietnamese);}{\f414\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f415\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}

+{\f417\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f418\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f419\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\f420\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}

+{\f421\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f422\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\f464\fbidi \froman\fcharset238\fprq2 Times CE;}{\f465\fbidi \froman\fcharset204\fprq2 Times Cyr;}

+{\f467\fbidi \froman\fcharset161\fprq2 Times Greek;}{\f468\fbidi \froman\fcharset162\fprq2 Times Tur;}{\f469\fbidi \froman\fcharset177\fprq2 Times (Hebrew);}{\f470\fbidi \froman\fcharset178\fprq2 Times (Arabic);}

+{\f471\fbidi \froman\fcharset186\fprq2 Times Baltic;}{\f472\fbidi \froman\fcharset163\fprq2 Times (Vietnamese);}{\f474\fbidi \fswiss\fcharset238\fprq2 Lato CE;}{\f475\fbidi \fswiss\fcharset204\fprq2 Lato Cyr;}

+{\f477\fbidi \fswiss\fcharset161\fprq2 Lato Greek;}{\f478\fbidi \fswiss\fcharset162\fprq2 Lato Tur;}{\f481\fbidi \fswiss\fcharset186\fprq2 Lato Baltic;}{\f482\fbidi \fswiss\fcharset163\fprq2 Lato (Vietnamese);}

+{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}

+{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}

+{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}

+{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}

+{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}

+{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;}

+{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}

+{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}

+{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}

+{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}

+{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}

+{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}

+{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}

+{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}

+{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}

+{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}

+{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}

+{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}

+{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}

+{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}

+{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}

+{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;

+\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;\caccentone\ctint255\cshade191\red54\green95\blue145;

+\caccentone\ctint255\cshade255\red79\green129\blue189;\caccentone\ctint255\cshade127\red36\green63\blue96;\ctextone\ctint191\cshade255\red64\green64\blue64;\chyperlink\ctint255\cshade255\red0\green0\blue255;

+\cfollowedhyperlink\ctint255\cshade255\red128\green0\blue128;\ctexttwo\ctint255\cshade191\red23\green54\blue93;\ctextone\ctint255\cshade255\red0\green0\blue0;\ctextone\ctint127\cshade255\red128\green128\blue128;

+\caccenttwo\ctint255\cshade255\red192\green80\blue77;\red61\green61\blue61;\red96\green94\blue92;\red225\green223\blue221;\red29\green28\blue29;\red248\green248\blue248;\red34\green34\blue34;}{\*\defchp \fs22\loch\af31506\hich\af31506\dbch\af31505 }

+{\*\defpap \ql \li0\ri0\sa200\sl276\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 

+\af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 \styrsid15886968 Normal;}{

+\s1\ql \li0\ri0\sb480\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af31503\afs28\alang1025 \ltrch\fcs0 

+\b\fs28\cf19\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink63 \sqformat \spriority9 \styrsid15139026 heading 1,h1;}{

+\s2\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af31503\afs26\alang1025 \ltrch\fcs0 

+\b\fs26\cf20\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink64 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 2,h2;}{

+\s3\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel2\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af31503\afs24\alang1025 \ltrch\fcs0 

+\b\fs22\cf20\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink65 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 3,h3;}{

+\s4\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel3\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\ai\af31503\afs24\alang1025 \ltrch\fcs0 

+\b\i\fs22\cf20\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink66 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 4,h4;}{

+\s5\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel4\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31503\afs24\alang1025 \ltrch\fcs0 

+\fs22\cf21\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink67 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 5,h5;}{

+\s6\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel5\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ai\af31503\afs24\alang1025 \ltrch\fcs0 

+\i\fs22\cf21\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink68 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 6,h6;}{

+\s7\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel6\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ai\af31503\afs24\alang1025 \ltrch\fcs0 

+\i\fs22\cf22\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink69 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 7,h7;}{

+\s8\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel7\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31503\afs20\alang1025 \ltrch\fcs0 

+\fs20\cf20\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink70 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 8,h8;}{

+\s9\ql \li0\ri0\sb200\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel8\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ai\af31503\afs20\alang1025 \ltrch\fcs0 

+\i\fs20\cf22\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink71 \sunhideused \sqformat \spriority9 \styrsid15139026 heading 9,h9;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 

+Default Paragraph Font;}{\*\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv 

+\ql \li0\ri0\sa200\sl276\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 

+\snext11 \ssemihidden \sunhideused Normal Table;}{\s15\qj \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext15 \slink16 \sunhideused \styrsid12276285 footnote text;}{\*\cs16 \additive \rtlch\fcs1 \af0\afs20 \ltrch\fcs0 \fs20 \sbasedon10 \slink15 \slocked \styrsid12276285 Footnote Text Char;}{\*\cs17 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 

+\super \sbasedon10 \sunhideused \styrsid8213195 footnote reference;}{\s18\ql \li720\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin720\itap0\contextualspace \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext18 \slink26 \sqformat \spriority34 \styrsid15139026 List Paragraph;}{\*\cs19 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf23 \sbasedon10 \sunhideused \styrsid8213195 Hyperlink;}{

+\s20\ql \li0\ri0\widctlpar\tqc\tx4320\tqr\tx8640\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext20 \slink21 \spriority0 \styrsid8085629 header;}{\*\cs21 \additive \rtlch\fcs1 \af0\afs20 \ltrch\fcs0 \f1\fs20 \sbasedon10 \slink20 \slocked \spriority0 \styrsid8213195 Header Char;}{\s22\ql \li0\ri0\widctlpar

+\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext22 \slink23 \styrsid8085629 footer;}{\*\cs23 

+\additive \rtlch\fcs1 \af0\afs20 \ltrch\fcs0 \f1\fs20 \sbasedon10 \slink22 \slocked \styrsid8213195 Footer Char;}{\s24\ql \li0\ri0\sl-200\slmult0\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 

+\ltrch\fcs0 \f1\fs14\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext24 \spriority0 \styrsid11146237 MacPac Trailer;}{\s25\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 

+\af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext25 \slink27 \spriority0 \styrsid8213195 Legal2 Cont 1;}{\*\cs26 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 

+\sbasedon10 \slink18 \slocked \spriority34 \styrsid8213195 List Paragraph Char;}{\*\cs27 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink25 \slocked \spriority0 \styrsid8213195 Legal2 Cont 1 Char;}{

+\s28\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon25 \snext28 \slink29 \spriority0 \styrsid8213195 Legal2 Cont 2;}{\*\cs29 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink28 \slocked \spriority0 \styrsid8213195 Legal2 Cont 2 Char;}{

+\s30\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon28 \snext30 \slink31 \spriority0 \styrsid8213195 Legal2 Cont 3;}{\*\cs31 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink30 \slocked \spriority0 \styrsid8213195 Legal2 Cont 3 Char;}{

+\s32\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon30 \snext32 \slink33 \spriority0 \styrsid8213195 Legal2 Cont 4;}{\*\cs33 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink32 \slocked \spriority0 \styrsid8213195 Legal2 Cont 4 Char;}{

+\s34\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon32 \snext34 \slink35 \spriority0 \styrsid8213195 Legal2 Cont 5;}{\*\cs35 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink34 \slocked \spriority0 \styrsid8213195 Legal2 Cont 5 Char;}{

+\s36\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon34 \snext36 \slink37 \spriority0 \styrsid8213195 Legal2 Cont 6;}{\*\cs37 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink36 \slocked \spriority0 \styrsid8213195 Legal2 Cont 6 Char;}{

+\s38\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon36 \snext38 \slink39 \spriority0 \styrsid8213195 Legal2 Cont 7;}{\*\cs39 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink38 \slocked \spriority0 \styrsid8213195 Legal2 Cont 7 Char;}{

+\s40\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon38 \snext40 \slink41 \spriority0 \styrsid8213195 Legal2 Cont 8;}{\*\cs41 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink40 \slocked \spriority0 \styrsid8213195 Legal2 Cont 8 Char;}{

+\s42\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon40 \snext42 \slink43 \spriority0 \styrsid8213195 Legal2 Cont 9;}{\*\cs43 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink42 \slocked \spriority0 \styrsid8213195 Legal2 Cont 9 Char;}{

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext25 \slink45 \spriority0 \styrsid8213195 Legal2_L1;}{\*\cs45 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink44 \slocked \spriority0 \styrsid8213195 Legal2_L1 Char;}{\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon44 \snext28 \slink47 \spriority0 \styrsid8213195 Legal2_L2;}{\*\cs47 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink46 \slocked \spriority0 \styrsid8213195 Legal2_L2 Char;}{

+\s48\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl2\outlinelevel2\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon46 \snext30 \slink49 \spriority0 \styrsid8213195 Legal2_L3;}{\*\cs49 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink48 \slocked \spriority0 \styrsid8213195 Legal2_L3 Char;}{

+\s50\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl3\outlinelevel3\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon48 \snext32 \slink51 \spriority0 \styrsid8213195 Legal2_L4;}{\*\cs51 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink50 \slocked \spriority0 \styrsid8213195 Legal2_L4 Char;}{

+\s52\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl4\outlinelevel4\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon50 \snext34 \slink53 \spriority0 \styrsid8213195 Legal2_L5;}{\*\cs53 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink52 \slocked \spriority0 \styrsid8213195 Legal2_L5 Char;}{

+\s54\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl5\outlinelevel5\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon52 \snext36 \slink55 \spriority0 \styrsid8213195 Legal2_L6;}{\*\cs55 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink54 \slocked \spriority0 \styrsid8213195 Legal2_L6 Char;}{

+\s56\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl6\outlinelevel6\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon54 \snext38 \slink57 \spriority0 \styrsid8213195 Legal2_L7;}{\*\cs57 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink56 \slocked \spriority0 \styrsid8213195 Legal2_L7 Char;}{

+\s58\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl7\outlinelevel7\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon56 \snext40 \slink59 \spriority0 \styrsid8213195 Legal2_L8;}{\*\cs59 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink58 \slocked \spriority0 \styrsid8213195 Legal2_L8 Char;}{

+\s60\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl8\outlinelevel8\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon58 \snext42 \slink61 \spriority0 \styrsid8213195 Legal2_L9;}{\*\cs61 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon26 \slink60 \slocked \spriority0 \styrsid8213195 Legal2_L9 Char;}{

+\s62\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af37\afs18\alang1025 \ltrch\fcs0 \b\f37\fs18\cf20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext0 \ssemihidden \sunhideused \sqformat \spriority35 \styrsid15139026 caption;}{\*\cs63 \additive \rtlch\fcs1 \ab\af31503\afs28 \ltrch\fcs0 \b\fs28\cf19\loch\f31502\hich\af31502\dbch\af31501 

+\sbasedon10 \slink1 \slocked \spriority9 \styrsid15139026 Heading 1 Char,h1 Char;}{\*\cs64 \additive \rtlch\fcs1 \ab\af31503\afs26 \ltrch\fcs0 \b\fs26\cf20\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink2 \slocked \spriority9 \styrsid15139026 

+Heading 2 Char,h2 Char;}{\*\cs65 \additive \rtlch\fcs1 \ab\af31503 \ltrch\fcs0 \b\cf20\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink3 \slocked \spriority9 \styrsid15139026 Heading 3 Char,h3 Char;}{\*\cs66 \additive \rtlch\fcs1 \ab\ai\af31503 

+\ltrch\fcs0 \b\i\cf20\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink4 \slocked \spriority9 \styrsid15139026 Heading 4 Char,h4 Char;}{\*\cs67 \additive \rtlch\fcs1 \af31503 \ltrch\fcs0 \cf21\loch\f31502\hich\af31502\dbch\af31501 

+\sbasedon10 \slink5 \slocked \spriority9 \styrsid15139026 Heading 5 Char,h5 Char;}{\*\cs68 \additive \rtlch\fcs1 \ai\af31503 \ltrch\fcs0 \i\cf21\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink6 \slocked \spriority9 \styrsid15139026 

+Heading 6 Char,h6 Char;}{\*\cs69 \additive \rtlch\fcs1 \ai\af31503 \ltrch\fcs0 \i\cf22\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink7 \slocked \spriority9 \styrsid15139026 Heading 7 Char,h7 Char;}{\*\cs70 \additive \rtlch\fcs1 \af31503\afs20 

+\ltrch\fcs0 \fs20\cf20\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink8 \slocked \spriority9 \styrsid15139026 Heading 8 Char,h8 Char;}{\*\cs71 \additive \rtlch\fcs1 \ai\af31503\afs20 \ltrch\fcs0 \i\fs20\cf22\loch\f31502\hich\af31502\dbch\af31501 

+\sbasedon10 \slink9 \slocked \spriority9 \styrsid15139026 Heading 9 Char,h9 Char;}{\s72\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext72 \slink73 \spriority0 \styrsid8085629 Body Text;}{\*\cs73 \additive \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \f37\fs20 \sbasedon10 \slink72 \slocked \spriority0 \styrsid8085629 

+Body Text Char;}{\s74\ql \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext74 \sunhideused \styrsid8213195 Normal (Web);}{\s75\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs16\alang1025 \ltrch\fcs0 

+\f1\fs16\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext75 \slink76 \ssemihidden \sunhideused \styrsid8213195 Document Map;}{\*\cs76 \additive \rtlch\fcs1 \af1\afs16 \ltrch\fcs0 \f1\fs16 

+\sbasedon10 \slink75 \slocked \ssemihidden \styrsid8213195 Document Map Char;}{\s77\qc \li0\ri0\sa240\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs20\alang1025 \ltrch\fcs0 

+\b\scaps\fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \spriority0 \styrsid8213195 Centered;}{\s78\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs20\alang1025 

+\ltrch\fcs0 \b\scaps\fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext78 \spriority0 \styrsid8213195 Instruction;}{\*\cs79 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf24 \sbasedon10 \ssemihidden \sunhideused \styrsid8213195 

+FollowedHyperlink;}{\*\cs80 \additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b \sbasedon10 \sqformat \spriority22 \styrsid15139026 Strong;}{\s81\ql \li0\ri0\widctlpar

+\tx916\tx1832\tx2748\tx3664\tx4580\tx5496\tx6412\tx7328\tx8244\tx9160\tx10076\tx10992\tx11908\tx12824\tx13740\tx14656\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af2\afs20\alang1025 \ltrch\fcs0 

+\f2\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext81 \slink82 \ssemihidden \sunhideused \styrsid8213195 HTML Preformatted;}{\*\cs82 \additive \rtlch\fcs1 \af2\afs20 \ltrch\fcs0 \f2\fs20 

+\sbasedon10 \slink81 \slocked \ssemihidden \styrsid8213195 HTML Preformatted Char;}{\s83\ql \li0\ri0\sb480\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af31503\afs28\alang1025 \ltrch\fcs0 

+\b\fs28\cf19\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon1 \snext0 \ssemihidden \sunhideused \sqformat \spriority39 \styrsid15139026 TOC Heading;}{

+\s84\qj \li0\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon44 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 1;}{\s85\ql \li220\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin220\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 2;}{\s86\ql \li440\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin440\itap0 \rtlch\fcs1 

+\af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 3;}{

+\s87\ql \li660\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin660\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 4;}{\s88\ql \li880\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin880\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 5;}{\s89\ql \li1100\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin1100\itap0 \rtlch\fcs1 

+\af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 6;}{

+\s90\ql \li1320\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin1320\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 7;}{\s91\ql \li1540\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin1540\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 8;}{\s92\ql \li1760\ri0\sa100\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin1760\itap0 \rtlch\fcs1 

+\af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \sautoupd \sunhideused \spriority39 \styrsid14369087 toc 9;}{

+\s93\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs16\alang1025 \ltrch\fcs0 \f1\fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext93 \slink94 \ssemihidden \sunhideused \styrsid6561208 Balloon Text;}{\*\cs94 \additive \rtlch\fcs1 \af1\afs16 \ltrch\fcs0 \f1\fs16 \sbasedon10 \slink93 \slocked \ssemihidden \styrsid6561208 Balloon Text Char;}{\*\ts95\tsrowd\trbrdrt

+\brdrs\brdrw10 \trbrdrl\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrh\brdrs\brdrw10 \trbrdrv\brdrs\brdrw10 

+\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv 

+\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs20\alang1025 \ltrch\fcs0 \fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon11 \snext95 \spriority59 \styrsid10293665 Table Grid;}{\*

+\cs96 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \sbasedon10 \spriority0 \styrsid10293665 ss_leftalign;}{\*\cs97 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \sbasedon10 \spriority0 \styrsid10293665 ss_bf;}{\*\cs98 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 

+\sbasedon10 \spriority0 \styrsid10293665 ss_it;}{\*\cs99 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \sbasedon10 \spriority0 \styrsid10293665 pnote;}{\s100\qc \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 

+\ab\af1\afs24\alang1025 \ltrch\fcs0 \b\scaps\fs22\cf6\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext100 \spriority0 \styrsid8081097 DraftStamp;}{\*\cs101 \additive \rtlch\fcs1 \af0\afs16 \ltrch\fcs0 \fs16 

+\sbasedon10 \ssemihidden \sunhideused \styrsid4142941 annotation reference;}{\s102\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext93 \slink103 \styrsid10380577 annotation text;}{\*\cs103 \additive \rtlch\fcs1 \af0\afs20 \ltrch\fcs0 \fs20 \sbasedon10 \slink102 \slocked \styrsid10380577 Comment Text Char;}{

+\s104\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af37\afs20\alang1025 \ltrch\fcs0 \b\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon102 \snext102 \slink105 \ssemihidden \sunhideused \styrsid4142941 annotation subject;}{\*\cs105 \additive \rtlch\fcs1 \ab\af0\afs20 \ltrch\fcs0 \b\fs20 \sbasedon103 \slink104 \slocked \ssemihidden \styrsid4142941 Comment Subject Char;}{

+\s106\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 

+\snext106 \shidden \ssemihidden \styrsid4142941 Revision;}{\*\cs107 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \sbasedon10 \spriority0 \styrsid5007233 sh_1801012974;}{\s108\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 

+\rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon102 \snext108 \spriority0 \styrsid11100321 comment;}{

+\s109\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext109 \slink110 \sunhideused \styrsid11420059 endnote text;}{\*\cs110 \additive \rtlch\fcs1 \af0\afs20 \ltrch\fcs0 \fs20 \sbasedon10 \slink109 \slocked \styrsid11420059 Endnote Text Char;}{\*\cs111 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 

+\super \sbasedon10 \ssemihidden \sunhideused \spriority0 \styrsid11420059 endnote reference;}{\*\cs112 \additive \rtlch\fcs1 \ab\af0\afs22 \ltrch\fcs0 \b\f42\fs22\cf1 \sbasedon10 \spriority0 \styrsid11889139 qs_num_paranum_1;}{

+\s113\ql \li0\ri0\sa300\widctlpar\brdrb\brdrs\brdrw20\brsp80\brdrcf20 \wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\contextualspace \rtlch\fcs1 \af31503\afs52\alang1025 \ltrch\fcs0 

+\fs52\expnd1\expndtw5\cf25\lang1033\langfe1033\kerning28\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink114 \sqformat \spriority10 \styrsid15139026 Title;}{\*\cs114 \additive \rtlch\fcs1 \af31503\afs52 

+\ltrch\fcs0 \fs52\expnd1\expndtw5\cf25\kerning28\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink113 \slocked \spriority10 \styrsid15139026 Title Char;}{

+\s115\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ilvl1\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ai\af31503\afs24\alang1025 \ltrch\fcs0 

+\i\fs24\expnd3\expndtw15\cf20\lang1033\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink116 \sqformat \spriority11 \styrsid15139026 Subtitle;}{\*\cs116 \additive \rtlch\fcs1 \ai\af31503\afs24 

+\ltrch\fcs0 \i\fs24\expnd3\expndtw15\cf20\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink115 \slocked \spriority11 \styrsid15139026 Subtitle Char;}{\*\cs117 \additive \rtlch\fcs1 \ai\af0 \ltrch\fcs0 \i 

+\sbasedon10 \sqformat \spriority20 \styrsid15139026 Emphasis;}{\s118\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 

+\fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext118 \sqformat \spriority1 \styrsid15139026 No Spacing;}{\s119\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 

+\rtlch\fcs1 \ai\af37\afs24\alang1025 \ltrch\fcs0 \i\f37\fs22\cf26\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink120 \sqformat \spriority29 \styrsid15139026 Quote;}{\*\cs120 \additive \rtlch\fcs1 \ai\af0 \ltrch\fcs0 \i\cf26 

+\sbasedon10 \slink119 \slocked \spriority29 \styrsid15139026 Quote Char;}{\s121\ql \li936\ri936\sb200\sa280\widctlpar\brdrb\brdrs\brdrw10\brsp80\brdrcf20 \wrapdefault\aspalpha\aspnum\faauto\adjustright\rin936\lin936\itap0 \rtlch\fcs1 

+\ab\ai\af37\afs24\alang1025 \ltrch\fcs0 \b\i\f37\fs22\cf20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink122 \sqformat \spriority30 \styrsid15139026 Intense Quote;}{\*\cs122 \additive \rtlch\fcs1 \ab\ai\af0 \ltrch\fcs0 

+\b\i\cf20 \sbasedon10 \slink121 \slocked \spriority30 \styrsid15139026 Intense Quote Char;}{\*\cs123 \additive \rtlch\fcs1 \ai\af0 \ltrch\fcs0 \i\cf27 \sbasedon10 \sqformat \spriority19 \styrsid15139026 Subtle Emphasis;}{\*\cs124 \additive \rtlch\fcs1 

+\ab\ai\af0 \ltrch\fcs0 \b\i\cf20 \sbasedon10 \sqformat \spriority21 \styrsid15139026 Intense Emphasis;}{\*\cs125 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \scaps\ul\cf28 \sbasedon10 \sqformat \spriority31 \styrsid15139026 Subtle Reference;}{\*\cs126 

+\additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b\scaps\ul\expnd1\expndtw5\cf28 \sbasedon10 \sqformat \spriority32 \styrsid15139026 Intense Reference;}{\*\cs127 \additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b\scaps\expnd1\expndtw5 

+\sbasedon10 \sqformat \spriority33 \styrsid15139026 Book Title;}{\s128\ql \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\b\caps\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \spriority0 \styrsid8085629 Confidential Phrase;}{\s129\qc \li0\ri0\sa480\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 

+\af37\afs24\alang1025 \ltrch\fcs0 \b\caps\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext72 \spriority0 \styrsid8085629 Document Title;}{

+\s130\qj \fi-360\li720\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls2\adjustright\rin0\lin720\itap0 \rtlch\fcs1 \af37\afs22\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon72 \snext130 \sqformat \spriority0 \styrsid8085629 Bullets;}{\*\cs131 \additive \rtlch\fcs1 \ab\af0 \ltrch\fcs0 \b\cf29 \sbasedon10 \spriority0 \styrsid4603784 co_searchterm2;}{\*\cs132 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \cf15 

+\sbasedon10 \ssemihidden \styrsid6960092 Placeholder Text;}{\*\cs133 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \cf30\chshdng0\chcfpat0\chcbpat31 \sbasedon10 \ssemihidden \sunhideused \styrsid16074695 Unresolved Mention;}{

+\s134\ql \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs22\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 

+\sbasedon0 \snext134 \spriority0 \styrsid15365944 text-indent-1;}{\s135\ql \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs22\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext135 \spriority0 \styrsid15365944 text-indent-2;}{\*\cs136 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \sbasedon10 \spriority0 \styrsid15365944 level-num;}{

+\s137\qc \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \b\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext137 \spriority0 \styrsid1463839 

+Footnote;}}{\*\listtable{\list\listtemplateid-202610920{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li720\lin720 }

+{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0

+\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative

+\levelspace0\levelindent0{\leveltext\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext

+\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3929 _;}{\levelnumbers;}

+\f10\fbias0\hres0\chhres0 \fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 

+\fi-360\li5040\lin5040 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23

+\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li6480\lin6480 }{\listname ;}\listid101150606}{\list\listtemplateid-711560524

+{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat0\levelspace0\levelindent0{\leveltext\'01\'95;}{\levelnumbers;}\f1\fs24\fbias0\hres0\chhres0 \s130\fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0

+\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0

+\levelindent0{\leveltext\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext

+\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01o;}{\levelnumbers;}

+\f2\fbias0\hres0\chhres0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 

+\fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li5040\lin5040 }{\listlevel\levelnfc23

+\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0

+\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li6480\lin6480 }{\listname ;}\listid194393157}{\list\listtemplateid814771694\listhybrid{\listlevel\levelnfc23\levelnfcn23

+\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0

+\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0

+\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0

+{\leveltext\leveltemplateid67698689\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext

+\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693

+\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698689

+\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li5040\lin5040 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers

+;}\f2\fbias0\hres0\chhres0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 _;}{\levelnumbers;}

+\f10\fbias0\hres0\chhres0 \fi-360\li6480\lin6480 }{\listname ;}\listid489449319}{\list\listtemplateid608867100\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext

+\leveltemplateid67698689\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 \fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691

+\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 _;}{\levelnumbers

+;}\f10\fbias0\hres0\chhres0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 _;}{\levelnumbers;}

+\f3\fbias0\hres0\chhres0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 

+\fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 

+\fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 _;}{\levelnumbers;}\f3\fbias0\hres0\chhres0 

+\fi-360\li5040\lin5040 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0\hres0\chhres0 \fi-360\li5760\lin5760 }

+{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 _;}{\levelnumbers;}\f10\fbias0\hres0\chhres0 \fi-360\li6480\lin6480 }{\listname 

+;}\listid614751655}{\list\listtemplateid-180045048{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\i0\strike0\scaps0\caps\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s44}{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'03\'00.\'01;}{\levelnumbers

+\'01\'03;}\rtlch\fcs1 \af37 \ltrch\fcs0 \b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s46}{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext

+\'03(\'02);}{\levelnumbers\'02;}\rtlch\fcs1 \af37 \ltrch\fcs0 \b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s48}{\listlevel\levelnfc2\levelnfcn2\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0

+\levelindent0{\leveltext\'03(\'03);}{\levelnumbers\'02;}\rtlch\fcs1 \af37 \ltrch\fcs0 \b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s50}{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0

+\levelstartat1\levelspace0\levelindent0{\leveltext\'03(\'04);}{\levelnumbers\'02;}\rtlch\fcs1 \af37 \ltrch\fcs0 \b0\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s52}{\listlevel\levelnfc4\levelnfcn4\leveljc0

+\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'05.;}{\levelnumbers\'01;}\rtlch\fcs1 \af37 \ltrch\fcs0 \b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s54}{\listlevel\levelnfc2

+\levelnfcn2\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'06.;}{\levelnumbers\'01;}\rtlch\fcs1 \af37 \ltrch\fcs0 \b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s56}

+{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'03(\'07);}{\levelnumbers\'02;}\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s58}{\listlevel\levelnfc2\levelnfcn2\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'03(\'08);}{\levelnumbers\'02;}

+\rtlch\fcs1 \af37 \ltrch\fcs0 \b\i0\strike0\caps0\v0\f37\fs22\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \s60}{\listname zzmpLegal2||Legal2|2|1|1|1|0|9||1|0|1||1|0|4||1|0|0||1|0|0||1|0|0||1|0|0||1|0|0||1|0|0||;}\listid900554160}}

+{\*\listoverridetable{\listoverride\listid900554160\listoverridecount0\ls1}{\listoverride\listid194393157\listoverridecount0\ls2}{\listoverride\listid900554160\listoverridecount9{\lfolevel\listoverrideformat{\listlevel\levelnfc0\levelnfcn0\leveljc0

+\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'00.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \b\i0\strike0\outl0\shad0\embo0\impr0\scaps0\caps\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 

+\fi0\li0\jclisttab\tx720\lin0 }}{\lfolevel\listoverrideformat{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'03\'00.\'01;}{\levelnumbers\'01\'03;}\rtlch\fcs1 \af0 \ltrch\fcs0 

+\b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi720\li0\jclisttab\tx1440\lin0 }}{\lfolevel\listoverrideformat{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1

+\levelspace0\levelindent0{\leveltext\'03(\'02);}{\levelnumbers\'02;}\rtlch\fcs1 \af0 \ltrch\fcs0 \b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi1440\li0\jclisttab\tx2160\lin0 }}{\lfolevel

+\listoverrideformat{\listlevel\levelnfc2\levelnfcn2\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'03(\'03);}{\levelnumbers\'02;}\rtlch\fcs1 \af0 \ltrch\fcs0 

+\b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi2160\li0\jclisttab\tx2880\lin0 }}{\lfolevel\listoverrideformat{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1

+\levelspace0\levelindent0{\leveltext\'03(\'04);}{\levelnumbers\'02;}\rtlch\fcs1 \af0 \ltrch\fcs0 \b0\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi2880\li0\jclisttab\tx3600\lin0 }}{\lfolevel

+\listoverrideformat{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'02\'05.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 

+\b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi3600\li0\jclisttab\tx4320\lin0 }}{\lfolevel\listoverrideformat{\listlevel\levelnfc2\levelnfcn2\leveljc0\leveljcn0\levelfollow0\levelstartat1

+\levelspace0\levelindent0{\leveltext\'02\'06.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi4320\li0\jclisttab\tx5040\lin0 }}{\lfolevel

+\listoverrideformat{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\'03(\'07);}{\levelnumbers\'02;}\rtlch\fcs1 \af0 \ltrch\fcs0 

+\b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi720\li0\jclisttab\tx1440\lin0 }}{\lfolevel\listoverrideformat{\listlevel\levelnfc2\levelnfcn2\leveljc0\leveljcn0\levelfollow0\levelstartat1

+\levelspace0\levelindent0{\leveltext\'03(\'08);}{\levelnumbers\'02;}\rtlch\fcs1 \af0 \ltrch\fcs0 \b\i0\strike0\outl0\shad0\embo0\impr0\caps0\v0\ulnone\cf0\nosupersub\animtext0\striked0\fbias0\hres0\chhres0 \fi1440\li0\jclisttab\tx2160\lin0 }}\ls3}

+{\listoverride\listid101150606\listoverridecount0\ls4}{\listoverride\listid614751655\listoverridecount0\ls5}{\listoverride\listid900554160\listoverridecount0\ls6}{\listoverride\listid900554160\listoverridecount9{\lfolevel\listoverridestartat\levelstartat1

+}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel

+\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}\ls7}{\listoverride\listid900554160\listoverridecount9{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat

+\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}{\lfolevel

+\listoverridestartat\levelstartat1}{\lfolevel\listoverridestartat\levelstartat1}\ls8}{\listoverride\listid489449319\listoverridecount0\ls9}{\listoverride\listid900554160\listoverridecount0\ls10}}{\*\revtbl {Unknown;}}{\*\pgptbl {\pgp\ipgp24\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0

+\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp22\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp11\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp27\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0

+\sb0\sa0}{\pgp\ipgp30\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp10\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp7\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp

+\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp5\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp4\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp6\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp3\itap0\li0

+\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp29\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp23\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp2\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}

+{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid4700\rsid7710\rsid13026\rsid22678\rsid26922\rsid31596\rsid144401\rsid146804\rsid160254\rsid160870\rsid161868\rsid213856\rsid214880\rsid216424\rsid219498\rsid223431

+\rsid266473\rsid266646\rsid280886\rsid290974\rsid331100\rsid335295\rsid342254\rsid344383\rsid355887\rsid396895\rsid411218\rsid420721\rsid422344\rsid425130\rsid462650\rsid470586\rsid491002\rsid527050\rsid528095\rsid556273\rsid603483\rsid603598\rsid603994

+\rsid604071\rsid609158\rsid615286\rsid617490\rsid617612\rsid665585\rsid671147\rsid676335\rsid680207\rsid686176\rsid686739\rsid722758\rsid722783\rsid725295\rsid743984\rsid871559\rsid878315\rsid944773\rsid944803\rsid984291\rsid987135\rsid987523\rsid987784

+\rsid988460\rsid999466\rsid1013857\rsid1050293\rsid1052705\rsid1065260\rsid1066021\rsid1069338\rsid1072687\rsid1139394\rsid1146143\rsid1189734\rsid1198993\rsid1201705\rsid1204980\rsid1209747\rsid1247912\rsid1254179\rsid1269739\rsid1310930\rsid1317403

+\rsid1322206\rsid1326568\rsid1333245\rsid1337732\rsid1378149\rsid1378296\rsid1407615\rsid1443088\rsid1450648\rsid1453444\rsid1455585\rsid1460143\rsid1463839\rsid1465131\rsid1518049\rsid1522592\rsid1526502\rsid1530575\rsid1577921\rsid1580337\rsid1640834

+\rsid1650239\rsid1656736\rsid1664216\rsid1710880\rsid1730702\rsid1732870\rsid1735266\rsid1786539\rsid1798460\rsid1800369\rsid1836458\rsid1836681\rsid1838898\rsid1847059\rsid1851000\rsid1860721\rsid1863418\rsid1864250\rsid1906275\rsid1920791\rsid1930861

+\rsid1979321\rsid1986300\rsid1989332\rsid1997888\rsid2047388\rsid2050041\rsid2098702\rsid2104621\rsid2106276\rsid2107230\rsid2109577\rsid2168709\rsid2169198\rsid2179087\rsid2236056\rsid2236908\rsid2237941\rsid2247522\rsid2248501\rsid2362270\rsid2367597

+\rsid2384351\rsid2427524\rsid2493522\rsid2493752\rsid2504876\rsid2519599\rsid2574312\rsid2584919\rsid2626737\rsid2627335\rsid2634953\rsid2637059\rsid2639438\rsid2648422\rsid2650620\rsid2651054\rsid2695645\rsid2699426\rsid2708720\rsid2715028\rsid2755678

+\rsid2756086\rsid2772758\rsid2819432\rsid2820915\rsid2822229\rsid2823788\rsid2827188\rsid2883744\rsid2894295\rsid2896802\rsid2912033\rsid2953919\rsid2971893\rsid2974373\rsid2980796\rsid3017397\rsid3017574\rsid3021615\rsid3021887\rsid3030269\rsid3040103

+\rsid3040596\rsid3045134\rsid3080648\rsid3091376\rsid3092136\rsid3095194\rsid3096713\rsid3099018\rsid3103324\rsid3107918\rsid3111772\rsid3168967\rsid3169748\rsid3175273\rsid3211942\rsid3213231\rsid3243479\rsid3286560\rsid3288021\rsid3300023\rsid3302595

+\rsid3345086\rsid3345752\rsid3351871\rsid3352675\rsid3352836\rsid3365686\rsid3365979\rsid3407993\rsid3435018\rsid3435047\rsid3440386\rsid3483903\rsid3505176\rsid3542935\rsid3543342\rsid3543465\rsid3547008\rsid3607023\rsid3625343\rsid3636573\rsid3670983

+\rsid3690616\rsid3693022\rsid3694660\rsid3694949\rsid3700095\rsid3701070\rsid3741066\rsid3742238\rsid3745495\rsid3752937\rsid3765003\rsid3767768\rsid3824222\rsid3867425\rsid3882330\rsid3888490\rsid3956203\rsid3963276\rsid4006682\rsid4011999\rsid4012565

+\rsid4014611\rsid4025281\rsid4065051\rsid4077919\rsid4078359\rsid4082471\rsid4084845\rsid4092197\rsid4132386\rsid4133945\rsid4140885\rsid4142941\rsid4150273\rsid4156408\rsid4160409\rsid4195482\rsid4196857\rsid4215344\rsid4217299\rsid4262549\rsid4266147

+\rsid4270964\rsid4274088\rsid4280288\rsid4282611\rsid4290583\rsid4333461\rsid4339968\rsid4340289\rsid4346442\rsid4352808\rsid4356118\rsid4413457\rsid4414305\rsid4420262\rsid4457609\rsid4484431\rsid4484683\rsid4541092\rsid4542147\rsid4545280\rsid4589520

+\rsid4600426\rsid4603351\rsid4603784\rsid4609068\rsid4609945\rsid4619315\rsid4673694\rsid4676806\rsid4731154\rsid4731751\rsid4734497\rsid4738878\rsid4743391\rsid4785860\rsid4800161\rsid4800162\rsid4863591\rsid4864043\rsid4870494\rsid4876858\rsid4877566

+\rsid4920153\rsid4923659\rsid4926582\rsid4926990\rsid4935154\rsid4947787\rsid5007233\rsid5010714\rsid5061700\rsid5068005\rsid5070719\rsid5070732\rsid5078346\rsid5078969\rsid5114342\rsid5178447\rsid5183255\rsid5187565\rsid5204709\rsid5207651\rsid5255460

+\rsid5261771\rsid5270890\rsid5272351\rsid5319962\rsid5332872\rsid5337249\rsid5341031\rsid5379992\rsid5389012\rsid5395338\rsid5404721\rsid5405715\rsid5440398\rsid5441245\rsid5444188\rsid5451781\rsid5469606\rsid5507101\rsid5510358\rsid5531747\rsid5533752

+\rsid5572565\rsid5582699\rsid5600017\rsid5601434\rsid5640103\rsid5644008\rsid5647448\rsid5652340\rsid5658740\rsid5662771\rsid5715708\rsid5723501\rsid5725259\rsid5774141\rsid5778695\rsid5781465\rsid5789911\rsid5837626\rsid5925368\rsid5929724\rsid5929952

+\rsid5990047\rsid5993963\rsid6056755\rsid6058702\rsid6100989\rsid6117879\rsid6120033\rsid6164268\rsid6173429\rsid6181258\rsid6190288\rsid6226702\rsid6229466\rsid6243481\rsid6249201\rsid6250141\rsid6255032\rsid6294792\rsid6304534\rsid6308196\rsid6317605

+\rsid6320661\rsid6320703\rsid6320826\rsid6357286\rsid6370791\rsid6371791\rsid6374365\rsid6387213\rsid6425425\rsid6439598\rsid6446352\rsid6450578\rsid6451512\rsid6495544\rsid6506846\rsid6510640\rsid6511065\rsid6512509\rsid6515459\rsid6520563\rsid6561208

+\rsid6567459\rsid6578873\rsid6584622\rsid6628176\rsid6629303\rsid6637033\rsid6706504\rsid6709869\rsid6711823\rsid6714884\rsid6716486\rsid6759571\rsid6766409\rsid6767617\rsid6774828\rsid6816168\rsid6816717\rsid6819474\rsid6820493\rsid6828972\rsid6838117

+\rsid6908092\rsid6913897\rsid6913922\rsid6949068\rsid6951037\rsid6960092\rsid6975429\rsid7029897\rsid7035110\rsid7045062\rsid7092798\rsid7092828\rsid7093731\rsid7097635\rsid7100725\rsid7110561\rsid7144527\rsid7147623\rsid7150967\rsid7154850\rsid7161773

+\rsid7175672\rsid7209080\rsid7218337\rsid7222915\rsid7223468\rsid7279275\rsid7295818\rsid7295890\rsid7350664\rsid7358637\rsid7362897\rsid7366451\rsid7370680\rsid7426851\rsid7483072\rsid7485254\rsid7486184\rsid7497581\rsid7539808\rsid7540750\rsid7545100

+\rsid7556610\rsid7557037\rsid7566966\rsid7611460\rsid7619174\rsid7619833\rsid7623192\rsid7623285\rsid7672098\rsid7672571\rsid7683966\rsid7684140\rsid7689668\rsid7692384\rsid7743086\rsid7744394\rsid7763794\rsid7765599\rsid7800947\rsid7802219\rsid7807245

+\rsid7825957\rsid7828744\rsid7830150\rsid7873369\rsid7876246\rsid7883253\rsid7884697\rsid7885971\rsid7895530\rsid7895698\rsid7929988\rsid7930367\rsid7934437\rsid7935602\rsid7939392\rsid7942526\rsid7944588\rsid7954437\rsid7955824\rsid7960262\rsid8001297

+\rsid8004789\rsid8008725\rsid8008995\rsid8012889\rsid8027675\rsid8027933\rsid8062525\rsid8068478\rsid8071221\rsid8072313\rsid8081097\rsid8084093\rsid8085629\rsid8092105\rsid8129132\rsid8196705\rsid8197638\rsid8203354\rsid8213195\rsid8213743\rsid8215638

+\rsid8223382\rsid8282657\rsid8288394\rsid8339455\rsid8340504\rsid8345311\rsid8390183\rsid8401561\rsid8403900\rsid8405481\rsid8407714\rsid8408482\rsid8412964\rsid8413462\rsid8414257\rsid8522236\rsid8537998\rsid8585511\rsid8589610\rsid8590836\rsid8591664

+\rsid8593431\rsid8606050\rsid8607742\rsid8616671\rsid8651334\rsid8657946\rsid8659799\rsid8681450\rsid8728211\rsid8734528\rsid8739875\rsid8741457\rsid8782191\rsid8788468\rsid8848778\rsid8854539\rsid8855642\rsid8916697\rsid8926884\rsid8927026\rsid8928991

+\rsid8930135\rsid8942126\rsid8982555\rsid8986704\rsid8989729\rsid9004891\rsid9005251\rsid9007319\rsid9010967\rsid9048421\rsid9049967\rsid9060906\rsid9110852\rsid9139236\rsid9176369\rsid9182146\rsid9204236\rsid9204461\rsid9205522\rsid9260795\rsid9262777

+\rsid9311007\rsid9319257\rsid9330787\rsid9334348\rsid9390822\rsid9395004\rsid9444976\rsid9456234\rsid9461934\rsid9466783\rsid9503511\rsid9507072\rsid9508028\rsid9526330\rsid9529644\rsid9533627\rsid9534378\rsid9583119\rsid9585415\rsid9586278\rsid9592100

+\rsid9597453\rsid9638262\rsid9644005\rsid9653345\rsid9653435\rsid9657924\rsid9712703\rsid9718501\rsid9771555\rsid9782570\rsid9793380\rsid9797448\rsid9851272\rsid9856866\rsid9861706\rsid9896965\rsid9897909\rsid9899775\rsid9901219\rsid9918851\rsid9924602

+\rsid9962423\rsid9986896\rsid10027110\rsid10053900\rsid10099784\rsid10102541\rsid10112705\rsid10114085\rsid10163059\rsid10165496\rsid10172776\rsid10172954\rsid10184249\rsid10237341\rsid10249255\rsid10293257\rsid10293665\rsid10296051\rsid10303388

+\rsid10319145\rsid10362263\rsid10370074\rsid10377623\rsid10380577\rsid10380755\rsid10424211\rsid10444835\rsid10446014\rsid10492007\rsid10505110\rsid10515756\rsid10517420\rsid10556168\rsid10557728\rsid10562088\rsid10567495\rsid10583768\rsid10625955

+\rsid10638090\rsid10708221\rsid10714704\rsid10714808\rsid10774561\rsid10779496\rsid10822360\rsid10828694\rsid10843309\rsid10844849\rsid10845426\rsid10897450\rsid10899443\rsid10899859\rsid10907923\rsid10956746\rsid10958215\rsid11012184\rsid11024253

+\rsid11028562\rsid11029367\rsid11031202\rsid11035602\rsid11037873\rsid11079709\rsid11088067\rsid11088543\rsid11100321\rsid11145994\rsid11146237\rsid11146656\rsid11146795\rsid11147959\rsid11148739\rsid11149104\rsid11150999\rsid11151589\rsid11167911

+\rsid11215482\rsid11220303\rsid11220458\rsid11221571\rsid11232493\rsid11235531\rsid11272372\rsid11273450\rsid11281187\rsid11284423\rsid11285316\rsid11296140\rsid11354466\rsid11355041\rsid11360041\rsid11364928\rsid11367358\rsid11412511\rsid11415733

+\rsid11420059\rsid11426157\rsid11469999\rsid11472409\rsid11476684\rsid11476892\rsid11479639\rsid11487439\rsid11493189\rsid11546711\rsid11557568\rsid11558545\rsid11562216\rsid11616764\rsid11622679\rsid11628649\rsid11629537\rsid11629610\rsid11631110

+\rsid11666011\rsid11667198\rsid11679499\rsid11687548\rsid11691784\rsid11739067\rsid11747682\rsid11759368\rsid11797826\rsid11801578\rsid11821352\rsid11825925\rsid11827193\rsid11829032\rsid11864119\rsid11868388\rsid11873193\rsid11876500\rsid11879540

+\rsid11889139\rsid11934200\rsid11936601\rsid11944493\rsid11950059\rsid11958442\rsid12002191\rsid12010400\rsid12010940\rsid12012798\rsid12022945\rsid12059584\rsid12060762\rsid12069410\rsid12073624\rsid12076974\rsid12082140\rsid12129192\rsid12136496

+\rsid12144589\rsid12145401\rsid12197862\rsid12206834\rsid12221038\rsid12221900\rsid12222103\rsid12257121\rsid12276285\rsid12276716\rsid12339099\rsid12412209\rsid12455992\rsid12466478\rsid12474964\rsid12522077\rsid12524682\rsid12529628\rsid12539567

+\rsid12540805\rsid12592614\rsid12593095\rsid12598978\rsid12606391\rsid12612791\rsid12613858\rsid12719849\rsid12720549\rsid12729868\rsid12781640\rsid12797613\rsid12802263\rsid12805427\rsid12805604\rsid12811591\rsid12858946\rsid12863911\rsid12912053

+\rsid12913684\rsid12914113\rsid12923456\rsid12983389\rsid13007511\rsid13044204\rsid13048655\rsid13058527\rsid13058882\rsid13107873\rsid13120874\rsid13134864\rsid13175079\rsid13177336\rsid13185453\rsid13186617\rsid13187444\rsid13196337\rsid13199081

+\rsid13245694\rsid13249622\rsid13269136\rsid13269468\rsid13309701\rsid13322378\rsid13389850\rsid13395722\rsid13444342\rsid13462099\rsid13503161\rsid13508172\rsid13509273\rsid13528286\rsid13567990\rsid13570702\rsid13581395\rsid13581562\rsid13585341

+\rsid13701201\rsid13701500\rsid13769069\rsid13769756\rsid13785433\rsid13792726\rsid13894359\rsid13895437\rsid13900591\rsid13909511\rsid13921068\rsid13966695\rsid13977299\rsid13982496\rsid14042512\rsid14046248\rsid14050083\rsid14052115\rsid14092598

+\rsid14106345\rsid14111382\rsid14113114\rsid14117888\rsid14159362\rsid14160265\rsid14161022\rsid14167332\rsid14222438\rsid14237629\rsid14242777\rsid14245556\rsid14295472\rsid14298402\rsid14315930\rsid14353607\rsid14360051\rsid14362879\rsid14367410

+\rsid14369087\rsid14370330\rsid14373527\rsid14373807\rsid14374083\rsid14385092\rsid14418445\rsid14427196\rsid14433703\rsid14489749\rsid14495628\rsid14504906\rsid14550853\rsid14552939\rsid14554005\rsid14558440\rsid14564720\rsid14569275\rsid14572688

+\rsid14572989\rsid14573824\rsid14615169\rsid14619447\rsid14701726\rsid14702099\rsid14704718\rsid14752235\rsid14755910\rsid14764209\rsid14773146\rsid14775245\rsid14815156\rsid14820077\rsid14831576\rsid14838117\rsid14840731\rsid14881156\rsid14888977

+\rsid14897298\rsid14900802\rsid14908866\rsid14946113\rsid14950867\rsid14952547\rsid15012725\rsid15032626\rsid15034971\rsid15074130\rsid15076982\rsid15101920\rsid15139026\rsid15144848\rsid15146583\rsid15155825\rsid15157344\rsid15159932\rsid15165931

+\rsid15170745\rsid15210725\rsid15211466\rsid15212000\rsid15212001\rsid15212587\rsid15220941\rsid15231950\rsid15273517\rsid15299570\rsid15338058\rsid15341627\rsid15347106\rsid15359189\rsid15365944\rsid15412083\rsid15430709\rsid15431975\rsid15467125

+\rsid15476886\rsid15539886\rsid15546803\rsid15551826\rsid15552699\rsid15616662\rsid15620187\rsid15621618\rsid15671939\rsid15693442\rsid15695468\rsid15738380\rsid15754409\rsid15758072\rsid15801130\rsid15804476\rsid15805862\rsid15882623\rsid15886796

+\rsid15886968\rsid15892384\rsid15927262\rsid15936580\rsid15946588\rsid16011718\rsid16015553\rsid16017562\rsid16058409\rsid16059943\rsid16074695\rsid16079006\rsid16082697\rsid16083738\rsid16084135\rsid16086525\rsid16148635\rsid16200213\rsid16213806

+\rsid16218050\rsid16256358\rsid16264736\rsid16265334\rsid16266006\rsid16273354\rsid16276299\rsid16337211\rsid16392126\rsid16395734\rsid16399261\rsid16405298\rsid16405825\rsid16456735\rsid16458454\rsid16458690\rsid16517015\rsid16522228\rsid16582948

+\rsid16586081\rsid16587272\rsid16601740\rsid16603438\rsid16603563\rsid16606794\rsid16612089\rsid16649249\rsid16665764\rsid16672008\rsid16676865\rsid16720743\rsid16727379\rsid16736865\rsid16739084\rsid16743747}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0

+\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author Nathan Sobo}{\operator Nathan Sobo}{\creatim\yr2023\mo3\dy10\hr17\min10}{\revtim\yr2023\mo3\dy10\hr17\min10}{\printim\mo1\dy1}{\version2}{\edmins1}

+{\nofpages17}{\nofwords9564}{\nofchars54519}{\nofcharsws63956}{\vern147}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}{\xmlns2 urn-legalmacpac-data/10}}

+\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect 

+\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting0\donotembedsysfont1\relyonvml1\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors1\noxlattoyen

+\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace110\dgvspace180\dghorigin1440\dgvorigin1440\dghshow2\dgvshow1

+\jexpand\viewkind1\viewscale234\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct

+\asianbrkrule\rsidroot8213195\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0

+{\*\wgrffmtfilter 2810}\nofeaturethrottle1\ilfomacatclnup4{\*\ftnsep \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278 \chftnsep 

+\par 

+\par }}{\*\ftnsepc \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid16273354 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278 \chftnsep 

+\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278 

+\par }}{\*\ftncn \ltrpar \pard\plain \ltrpar\s22\ql \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4870494 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278\charrsid4870494 

+\par }}{\*\aftnsep \ltrpar \pard\plain \ltrpar\s22\ql \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid8085629 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278\charrsid8085629 

+\par }}{\*\aftnsepc \ltrpar \pard\plain \ltrpar\s22\ql \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid8085629 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278\charrsid8085629 

+\par }}{\*\aftncn \ltrpar \pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid9586278 

+\par }}\ltrpar \sectd \ltrsect\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\headerl \ltrpar \pard\plain \ltrpar\s20\ql \li0\ri0\widctlpar

+\tqc\tx4320\tqr\tx8640\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }}{\headerr \ltrpar \pard\plain \ltrpar\s20\ql \li0\ri0\widctlpar\tqc\tx4320\tqr\tx8640\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }}{\footerl \ltrpar \pard\plain \ltrpar\s22\ql \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7362897  

+\par }}{\footerr \ltrpar \pard\plain \ltrpar\s22\qc \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid984291 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7362897  PAGE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid7362897 \\* MERGEFORMAT}}{\fldrslt {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid7362897 75}}}\sectd \ltrsect\linex0\endnhere\sectdefaultcl\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7362897 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }\pard \ltrpar\s22\ql \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 {\rtlch\fcs1 \af37 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid8203354 

+{\shp{\*\shpinst\shpleft0\shptop0\shpright4032\shpbottom403\shpfhdr1\shpbxmargin\shpbxignore\shpbypara\shpbyignore\shpwr3\shpwrk0\shpfblwtxt1\shpz0\shplid1030{\sp{\sn shapeType}{\sv 202}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}

+{\sp{\sn lTxid}{\sv 65536}}{\sp{\sn dxTextLeft}{\sv 0}}{\sp{\sn dyTextTop}{\sv 0}}{\sp{\sn dxTextRight}{\sv 0}}{\sp{\sn dyTextBottom}{\sv 0}}{\sp{\sn fRotateText}{\sv 0}}

+{\sp{\sn fFitShapeToText}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}{\sp{\sn fNoFillHitTest}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Text Box 2}}{\sp{\sn posrelh}{\sv 0}}

+{\sp{\sn dhgt}{\sv 251659264}}{\sp{\sn fEditedWrap}{\sv 0}}{\sp{\sn fBehindDocument}{\sv 1}}{\sp{\sn fIsButton}{\sv 0}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}{\shptxt \ltrpar \pard\plain \ltrpar\s24\ql \li0\ri0\sl-200\slmult0

+\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \f1\fs14\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid11146237 275625662 v5

+\par 

+\par }}}{\shprslt{\*\do\dobxmargin\dobypara\dodhgt0\dptxbx\dptxlrtb{\dptxbxtext\ltrpar \pard\plain \ltrpar\s24\ql \li0\ri0\sl-200\slmult0\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 

+\f1\fs14\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid11146237 275625662 v5

+\par 

+\par }}\dpx0\dpy0\dpxsize4032\dpysize403\dpfillfgcr255\dpfillfgcg255\dpfillfgcb255\dpfillbgcr255\dpfillbgcg255\dpfillbgcb255\dpfillpat0\dplinehollow}}}}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }}{\headerf \ltrpar \pard\plain \ltrpar\s20\ql \li0\ri0\widctlpar\tqc\tx4320\tqr\tx8640\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }}{\footerf \ltrpar \pard\plain \ltrpar\s22\qc \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid984291 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7362897  PAGE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid7362897 \\* MERGEFORMAT}}{\fldrslt {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid7362897 47}}}\sectd \ltrsect\linex0\endnhere\sectdefaultcl\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7362897 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }\pard \ltrpar\s22\ql \li0\ri0\widctlpar\tqc\tx4680\tqr\tx9360\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 {\rtlch\fcs1 \af37 \ltrch\fcs0 \lang1024\langfe1024\noproof\insrsid8203354 

+{\shp{\*\shpinst\shpleft0\shptop0\shpright4032\shpbottom403\shpfhdr0\shpbxmargin\shpbxignore\shpbypara\shpbyignore\shpwr3\shpwrk0\shpfblwtxt1\shpz1\shplid1029{\sp{\sn shapeType}{\sv 202}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}

+{\sp{\sn fLockRotation}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 0}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockVerticies}{\sv 0}}{\sp{\sn fLockText}{\sv 0}}{\sp{\sn fLockAdjustHandles}{\sv 0}}

+{\sp{\sn fLockAgainstGrouping}{\sv 0}}{\sp{\sn lTxid}{\sv 131072}}{\sp{\sn dxTextLeft}{\sv 0}}{\sp{\sn dyTextTop}{\sv 0}}{\sp{\sn dxTextRight}{\sv 0}}{\sp{\sn dyTextBottom}{\sv 0}}{\sp{\sn fRotateText}{\sv 0}}{\sp{\sn fFitShapeToText}{\sv 0}}

+{\sp{\sn fFilled}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn fLockShapeType}{\sv 0}}{\sp{\sn wzName}{\sv Text Box 1}}{\sp{\sn posrelh}{\sv 0}}{\sp{\sn metroBlob}{\sv {\*\svb 

+504b030414000600080000002100b6833892fe000000e1010000130000005b436f6e74656e745f54797065735d2e786d6c9491414ec3301045f748dcc1f216254ebb400825e982b44b40a81c60644f128b646c794c686f8f93b61b449158da33ffbf27bbdc1cc6414c18d83aaae42a2fa440d2ce58ea2af9bedf650f527004

+323038c24a1e91e5a6bebd29f7478f2c529ab8927d8cfe5129d63d8ec0b9f34869d2ba30424cc7d0290ffa033a54eba2b857da51448a599c3b645d36d8c2e710c5f690ae4f26010796e2e9b438b32a09de0f56434ca66a22f383929d09794a2e3bdc5bcf774943aa5f09f3e43ae09c7b494f13ac41f10a213ec398349409ac

+70ed1aa7f3bf3b66c99133d7b65663de04de2ea98bd3b56ee3be28e0f4dff226c5de70bab4abe583ea6f000000ffff0300504b03041400060008000000210038fd21ffd6000000940100000b0000005f72656c732f2e72656c73a490c16ac3300c86ef83bd83d17d719ac318a34e2fa3d06be91ec0d88a631a5b4632d9faf6

+3383c1327adb51bfd0f7897f7ff84c8b5a91255236b0eb7a50981df9988381f7cbf1e90594549bbd5d28a3811b0a1cc6c787fd19175bdb91ccb1886a942c06e65acbabd6e2664c563a2a98db66224eb6b691832ed65d6d403df4fdb3e6df0c18374c75f206f8e40750975b69e63fec141d93d0543b4749d33445778faa3d7d

+e433ae8d62396035e059be43c6b56bcf81beefddfdd31bd89639ba23db846fe4b67e1ca8653f7abde972fc020000ffff0300504b0304140006000800000021007a9ae899d8010000980300000e0000006472732f65326f446f632e786d6cac53db6ed430107d47e21f2cbfb3c9066d05d166abd2aa08a940a5c207388e9d58

+241e33f66eb27c3d6327d9727943bc589319fbcc396726fbeb69e8d949a137602bbedde49c292ba131b6adf8d72ff7afde70e683b08de8c1aa8a9f95e7d787972ff6a32b55011df48d424620d697a3ab7817822bb3cccb4e0dc26fc0294b450d3888409fd8660d8a91d0873e2bf2fc2a1b011b872095f794bd9b8bfc90f0b5

+56327cd6daabc0fa8a13b7904e4c671dcfecb017658bc275462e34c43fb01884b1d4f4027527826047347f410d462278d0612361c8406b2355d2406ab6f91f6a9e3ae154d242e67877b1c9ff3f58f9e9f4e41e9185e91d4c34c024c2bb0790df3cb370db09dbaa1b44183b251a6abc8d9665a3f3e5f2345aed4b1f41eaf123

+343464710c9080268d437485743242a7019c2fa6ab293049c9627795bf2ea824a956ec766ff35d6a21caf5b5431fde2b18580c2a8e34d4842e4e0f3e4436a25cafc46616ee4ddfa7c1f6f6b7045d8c99c43e129ea987a99e98691669514c0dcd99e420cceb42eb4d4107f883b39156a5e2fefb51a0e2acff60c992b8576b80

+6b50af81b0929e563c703687b761debfa343d376843c9b6ee1866cd326297a66b1d0a5f127a1cbaac6fdfaf53bdd7afea10e3f010000ffff0300504b0304140006000800000021006147e3f4dd000000090100000f0000006472732f646f776e7265762e786d6c4c8fcd4ec3301084ef48bc83b548dca84d4115a471aa8a9f

+1312220d078e4ebc4dacc6eb10bb6d787b965eca65b4abd1cece97af26df8b038ed105d2703b5320909a601db51a3eabd79b07103119b2a60f841a7e30c2aab8bcc84d66c3914a3c6c522b388462663474290d9994b1e9d09b380b03127bdb307a93781d5b694773e470dfcbb9520be98d23fed099019f3a6c769bbdd7b0fe

+a2f2c57dbfd71fe5b67455f5a8e86db1d3fafa6a7a5eb2ac9720124ee97c017f0cdc1f0a2e56873dd9287a0d4c934ecadebdba9b83a84f03c82297ff098a5f000000ffff0300504b01022d0014000600080000002100b6833892fe000000e10100001300000000000000000000000000000000005b436f6e74656e745f5479

+7065735d2e786d6c504b01022d001400060008000000210038fd21ffd6000000940100000b000000000000000000000000002f0100005f72656c732f2e72656c73504b01022d00140006000800000021007a9ae899d8010000980300000e000000000000000000000000002e0200006472732f65326f446f632e786d6c504b

+01022d00140006000800000021006147e3f4dd000000090100000f00000000000000000000000000320400006472732f646f776e7265762e786d6c504b05060000000004000400f30000003c0500000000

+}}}{\sp{\sn dhgt}{\sv 251661312}}{\sp{\sn fLayoutInCell}{\sv 1}}{\sp{\sn fAllowOverlap}{\sv 1}}{\sp{\sn fBehindDocument}{\sv 1}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}{\shptxt \ltrpar \pard\plain \ltrpar\s24\ql \li0\ri0\sl-200\slmult0

+\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \f1\fs14\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid11146237 275625662 v5

+\par 

+\par }}}{\shprslt{\*\do\dobxmargin\dobypara\dodhgt1\dptxbx\dptxlrtb{\dptxbxtext\ltrpar \pard\plain \ltrpar\s24\ql \li0\ri0\sl-200\slmult0\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 

+\f1\fs14\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \insrsid11146237 275625662 v5

+\par 

+\par }}\dpx0\dpy0\dpxsize4032\dpysize403\dpfillfgcr255\dpfillfgcg255\dpfillfgcb255\dpfillbgcr255\dpfillbgcg255\dpfillbgcb255\dpfillpat0\dplinehollow}}}}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11146237 

+\par }}{\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}

+{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8

+\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar

+\s137\qc \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid1463839 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \b\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 {\*\bkmkstart _Hlk108160434}Terms of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11629537  Agreement}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+\par }\pard\plain \ltrpar\qc \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid7358637 \rtlch\fcs1 \af37\afs24\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\insrsid8085629\charrsid7358637 Last Updated:}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid11631110  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid15805862 3/7/2023}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8085629 

+\par }\pard\plain \ltrpar\s72\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid9986896 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af0 \ltrch\fcs0 \cf1\insrsid7710\charrsid12404255 Welcome and thank you for your interest in}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11281187  Zed Industries, Inc. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1465131 (\'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 

+\b\insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1465131 \'94}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 , \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid6370791 we}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 \'94 or \'93}{

+\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid6370791 us}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 \'94}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1465131 )}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896\charrsid11156953 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896\charrsid11156953 Th}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16458690 e}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896\charrsid11156953 s}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16458690 e}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710  Terms of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896\charrsid11156953  Agreement}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 (\'93}{\rtlch\fcs1 \ab\af37 

+\ltrch\fcs0 \b\insrsid6370791 Terms of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 \'94)}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 , together with any applicable Supplemental Terms (as defined in Section}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid12540805  }{\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12540805  REF _Ref107558864 \\w \\h }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12540805 {\*\datafield 

+08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100300037003500350038003800360034000000}}}{\fldrslt {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7807245 1.3}}}\sectd \ltrsect

+\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12540805 )}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429  (collectively, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5395338 

+with the Terms of Use, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 the \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid6975429 Agreement}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 \'94)}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9986896\charrsid11156953  describe the terms and conditions that apply to your use of}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710  (i)}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896\charrsid11156953  the }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9986896 website located at }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid15805862 https://zed.dev}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid3096713  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 [and any of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 

+the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 \rquote s }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 other websites on which a link to the}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 

+se Terms of Use }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 appears] }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896 (}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 collectively, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896 the \'93}{\rtlch\fcs1 

+\ab\af37 \ltrch\fcs0 \b\insrsid9986896 Website}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896 \'94)}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 , (ii) }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710 any }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid15212001 desktop}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710  application}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 (s)}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710  that we offer subj}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid7710 ect to th}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 ese}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6370791 Terms of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896  }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid3096713 (}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 each, an}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid3096713  \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid3096713 Application}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid3096713 \'94)}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710 , and (iii) }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710 the services or other resources available on or enabled via our Website or any Application}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11167911 ,}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710  including }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12592614 our }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12592614\charrsid12592614 collaborative code editor}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid12592614  software (\'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid12592614\charrsid12592614 Tool}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12592614 \'94) available for }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6816168 you to }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid12592614 download }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6816168 on your computer }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12592614 or on a hosted basis}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid7710\charrsid7710 (collectively, with our }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5469606 Tool, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710 Applications and Website, the \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 

+\b\insrsid7710\charrsid7710 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7710\charrsid7710 \'94).}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9986896\charrsid9986896 

+\par }\pard \ltrpar\s72\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid11031202 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 PLEASE RE}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid5469606 AD THIS AGREEMENT}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429\charrsid5469606  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 CAREFULLY.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183\charrsid5469606  }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16458690\charrsid5469606 THESE TERMS}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606  OF USE GOVERN THE USE OF THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183\charrsid5469606 SERVICE}{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606  AND APPLY TO ALL INTERNET USERS VISITING THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183\charrsid5469606 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11629537\charrsid5469606 .}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid8390183\charrsid5469606  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 BY ACCESS}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12455992\charrsid5469606 ING}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5469606\charrsid5469606 ,}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606  OR USING THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183\charrsid5469606 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429\charrsid5469606  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid5469606 IN ANY WAY, INCLUDING BY CLICKING ON THE \'93I ACCEPT\'94 BUTTON}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12455992\charrsid5469606 ,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid3021615\charrsid5469606  }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid8085629\charrsid5469606 COMPLETING THE REGISTRATION PROCESS}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12455992\charrsid5469606 ,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5469606\charrsid5469606  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid5469606 BROWSING THE WEBSITE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5929724\charrsid5469606 AND/}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 OR DOWNLOADING }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9901219\charrsid5469606 THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 APPLICATION}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5469606\charrsid5469606  OR THE TOOL}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 , 

+YOU REPRESENT THAT (1) YOU HAVE READ, UNDERSTAND, AND AGREE TO BE BOUND BY THE TERMS OF USE, (2) YOU ARE OF LEGAL AGE TO FORM A BINDING CONTRACT WITH }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358\charrsid5469606 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid5469606 , AND (3) YOU HAVE THE AUTHORITY TO ENTER INTO THE TERMS OF USE PERSONALLY OR ON BEHALF OF }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1013857\charrsid5469606 THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1530575\charrsid5469606 

+ENTITY }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 YOU HAVE NAMED AS THE USER, AND TO BIND THAT }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1530575\charrsid5469606 ENTITY }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 

+TO THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5395338\charrsid5469606 THIS AGREEMENT, AND IN WHICH CASE, ALL REFERENCES TO \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid5395338\charrsid5469606 YOU}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid5395338\charrsid5469606 \'94 OR \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid5395338\charrsid5469606 YOUR}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5395338\charrsid5469606 \'94 IN THIS AGREEMENT WILL ALSO BE DEEMED TO REFER TO SUCH ENTITY}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183\charrsid5469606  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8085629\charrsid5469606 

+IF YOU DO NOT AGREE TO BE BOUND BY THE TERMS OF USE, YOU MAY NOT ACCESS OR USE THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183\charrsid5469606 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid5469606 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+\par }\pard \ltrpar\s72\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid6951037 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037 PLEASE BE AWARE THAT SECTION}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\insrsid6951037  }{\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid7807245  REF _Ref110233665 \\r \\h }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid7807245 {\*\datafield 

+08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100310030003200330033003600360035000000}}}{\fldrslt {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid3021615 14}}}\sectd \ltrsect

+\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037  CONTAINS PROVISIONS GOVERNING HOW TO RESOLVE DISPUTES BETWEEN YOU AND }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\insrsid5510358 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037 AMONG OTHER THINGS, SECTION }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid3021615 14}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037  INCLUDES AN AGREEMENT TO ARBITRATE WHICH REQUIRES, WITH LIMITED EXCEPTIONS, THAT ALL 

+DISPUTES BETWEEN YOU AND US SHALL BE RESOLVED BY BINDING AND FINAL ARBITRATION.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037 SECTION }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037 1

+}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid3021615 4}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037  ALSO CONTAINS A CLASS ACTION AND JURY TRIAL WAIVER.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\insrsid6951037\charrsid6951037 PLEASE READ SECTION }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037 1}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid3021615 4}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037  CAREFULLY.

+\par }\pard \ltrpar\s72\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid11031202 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037\charrsid6951037 

+UNLESS YOU OPT OUT OF THE AGREEMENT TO ARBITRATE WITHIN 30 DAYS: (1) YOU WILL O

+NLY BE PERMITTED TO PURSUE DISPUTES OR CLAIMS AND SEEK RELIEF AGAINST US ON AN INDIVIDUAL BASIS, NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY CLASS OR REPRESENTATIVE ACTION OR PROCEEDING AND YOU WAIVE YOUR RIGHT TO PARTICIPATE IN A CLASS ACTION LAWSUIT OR CL

+ASS-WIDE ARBITRATION; AND (2) YOU ARE WAIVING YOUR RIGHT TO PURSUE DISPUTES OR CLAIMS AND SEEK RELIEF IN A COURT OF LAW AND TO HAVE A JURY TRIAL.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6951037 

+\par }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 PLEASE NOTE THAT }{\rtlch\fcs1 \af37 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 The Agreement IS subject to change by }{\rtlch\fcs1 \af37 \ltrch\fcs0 \caps\insrsid5510358 Zed}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637  in its sole discretion at any time}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 When changes are made, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  will make a new copy of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 

+Terms of Use and/or }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Supplemental Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 , as applicable,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ available on the Website }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 and}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  within }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7045062 any}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6975429 

+ affected}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  Application.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 We will also update the \'93Last Updated\'94

+ date at the top of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1798460 Agreement}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 If we make any material changes, and you have }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16218050 provided us an email address when creating}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ an Account (as defined in Section 2.1 }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11759368 (Registering Your Account) }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+below) we will also send an e-mail to you at the last e-mail address you provided to us pursuant to the Agreement.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid22678 Unless otherwise stated in such update, a}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ny changes to the Agreement will be effective immediately for new users of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4545280 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 

+Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4545280\charrsid7358637  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+and will be effective thirty (30) days after posting notice of such changes on the Website for existing Registered 

+Users, provided that any material changes shall be effective for Registered Users who have an Account with us upon the earlier of thirty (30) days after posting notice of such changes on the Website or thirty (30) days after dispatch of an e-mail notice o

+f such changes to Registered Users (defined in Section 2.1}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11759368  (Registering Your Account)}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  below).}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  

+}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  may require}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5469606  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+you to provide consent to the updated Agreement in a specified manner before further use of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is permitted.}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638\charrsid7358637 IF YOU DO NOT AGREE TO ANY CHANGE(S) AFTER RECEIVING A NOTICE OF SUCH CHANGE(S), YOU SHALL STOP USING THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638 SERVICE}

+{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638\charrsid7358637 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638\charrsid7358637 OTHERWISE, YOUR CONTINUED USE OF THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8215638 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638\charrsid7358637  CONSTITUTES YOUR ACCEPTANCE OF SUCH CHANGE(S}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8215638 ). }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+PLEASE REGULARLY CHECK THE WEBSITE TO VIEW THE THEN-CURRENT TERMS}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid1798460 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid7873369\charrsid8607742 \hich\af37\dbch\af0\loch\f37 1.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid7873369\charrsid8607742 USE OF THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8607742 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 The }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 

+ and the information and content available on the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742  are protected by copyright laws throughout the world.}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 Unless otherwise specified by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8607742  in a separate license, your right to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7045062 access and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 use any and all }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid1322206 of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 is 

+subject to the Agreement.

+\par {\*\bkmkstart _Ref107824524}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid987135 \hich\af37\dbch\af0\loch\f37 1.1\tab}}\pard\plain \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\insrsid8085629\charrsid987135 Application License.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid987135 Subject to your compliance with the Agreement, }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid987135  grants you a limited non-exclusive, non-transferable, non-sublicensable, revocable license to download, install}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13581395 

+, evaluate,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid987135  and use a copy of the Application on a single computer that you own or control and to run such copy of the Application solely for your own personal or internal business purposes.}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid987135 {\*\bkmkend _Ref107824524}

+\par {\*\bkmkstart _Ref107824537}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid290974 \hich\af37\dbch\af0\loch\f37 1.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid12720549 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid290974 Tool License}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid987135 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid290974\charrsid987135 Subject to your compliance w}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid290974\charrsid290974 ith the}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid290974\charrsid290974  Agreement}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9918851  and the limitations set forth in Section 5}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid290974\charrsid290974 , Zed gra

+nts you a limited non-exclusive, non-t}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid290974\charrsid16395734 

+ransferable, non-sublicensable: (a) perpetual right and license to install and use the Tool on a single computer that you own or control and to run such copy of the Tool; and/or (b) right to access and use the hosted Tool, and in each case }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid16395734\charrsid987135 for the sole purpose of enabling you to use the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16395734 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16395734\charrsid987135 

+ in the manner permitted by the Agreement}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid290974\charrsid16395734  and in accordance with the documentation}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid987135 .}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7222915 In the event you choose to download the Tool, you may do so }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 from the Website. Zed will not be obliged to }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid7222915\charrsid7222915 use or deliver any tangible media in connection with the (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 y}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7222915\charrsid7222915 

+) delivery, installation, updating or problem resolution of any }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 Tool}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7222915\charrsid7222915  (including any new releases); or (}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid26922 z}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7222915\charrsid7222915 ) delivery, correction or updating of documentation.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid16395734 The Tool }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5837626 or other software offered by Zed as part of the Service }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16395734 may contain }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6816168\charrsid987135 open-source}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid987135  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16395734 software }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6816168 and }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid6816168\charrsid987135 may be offered under an }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6816168 open-source }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid987135 

+license that we will make available to you.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid987135 There may be provisions in the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6816168\charrsid987135 open-source}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid987135  license that expressly override some of these terms.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549\charrsid12720549 

+{\*\bkmkend _Ref107824537}

+\par {\*\bkmkstart _Ref107558864}{\*\bkmkstart _Ref_ContractCompanion_9kb9Ur013}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \ab\af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid12720549 \hich\af37\dbch\af0\loch\f37 1.3\tab}}\pard \ltrpar

+\s46\qj \li0\ri0\sa240\widctlpar\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid722758 {\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid12720549 Supplemental Terms.}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \cs17\super\insrsid9319257\charrsid9319257  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid7358637 Your use of, and participation in, certain }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid12720549\charrsid7358637  may be subject to additional terms (}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid12720549\charrsid7358637 \'93Supplemental Terms\'94}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid7358637 

+) and such Supplemental Terms will either be listed in the Terms of Use or will be presented to you for your acceptance when you sign up to use the supplemental Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid7358637 If the Terms of Use are inconsistent with the Supplemental Terms, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9319257 then }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid12720549\charrsid7358637 the Supplemental Terms shall control with respect to such Service.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9583119\charrsid722758 {\*\bkmkstart _Ref107824557}

+{\*\bkmkend _Ref107558864}

+\par {\*\bkmkstart _Ref108343249}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid8027675 \hich\af37\dbch\af0\loch\f37 1.4\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4609068 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8027675 Updates.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  

+}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 You understand that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9049967\charrsid8027675 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid9049967\charrsid8027675  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 are evolving.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 

+As a result, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675  may require you to accept updates to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9049967\charrsid8027675 the }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527 Tool}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid22678\charrsid8027675  or Applications }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 that you have installed on your computer.}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 You acknowledge and agree that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8027675  may update }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9049967\charrsid8027675 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9049967\charrsid8027675  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 with or without notifying you.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8027675 

+You may need to update third-party software from time to time in order to use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9049967\charrsid8027675 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8027675 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 {\*\bkmkend _Ref_ContractCompanion_9kb9Ur013}{\*\bkmkend _Ref107824557}{\*\bkmkend _Ref108343249} }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8027675\charrsid8027675 

+ }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13701500\charrsid13701500 {\*\bkmkstart _Ref107568468}

+\par {\*\bkmkstart _Ref108342634}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid5510358 \hich\af37\dbch\af0\loch\f37 1.5\tab}}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\insrsid13177336\charrsid13177336  Communications}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13177336 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 {\*\bkmkend _Ref107568468}{\*\bkmkend _Ref108342634} }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11628649 

+

+\par {\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \ai\af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid11628649 \hich\af37\dbch\af0\loch\f37 (a)\tab}}\pard\plain \ltrpar

+\s48\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl2\outlinelevel2\adjustright\rin0\lin0\itap0\pararsid11628649 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {

+\rtlch\fcs1 \ai\af37 \ltrch\fcs0 \i\insrsid11628649 Generally.}{\rtlch\fcs1 \ai\af37 \ltrch\fcs0 \i\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219 By entering into this Agreement or using }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid9049967 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219 , you agree to receive communications from us, including via e-mail, text message, calls, and}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid5725259 /or}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219  push notifications.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219 Communications from us and our affiliated companies may include}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9319257 ,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219  but are not limited to: operational communications concerning your Account or the use of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219 , updates concerning new and existing features on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid7802219 , communications concerning promotions run by us or our third-party partners, and news concerning the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid7802219  and industry developments. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11628649 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid7873369\charrsid7358637 \hich\af37\dbch\af0\loch\f37 2.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid7873369\charrsid7358637 REGISTRATION}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid6515459 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 

+\par {\*\bkmkstart _Ref107568528}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid7873369 \hich\af37\dbch\af0\loch\f37 2.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid7873369 Registering Your Account}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 In order to access certain features of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 you may be required to become a Registered User.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 For purposes of the Agreement, a }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 \'93Registered User\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+ is a user who has registered an account on the Website (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 \'93Account\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 )}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid26922 , and/or}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid26922\charrsid26922  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922\charrsid26922 has a valid account on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 certain

+ other service (e.g., GitHub) (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922\charrsid26922 \'93}{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \b\insrsid26922\charrsid26922 SNS}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922\charrsid26922 \'94}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 ) t}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922\charrsid26922 hrough which the user has connected to the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid26922\charrsid26922  (each such account, a }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 \'93Third-Party Account}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid6515459 \'94}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid6515459\charrsid12805604 )}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid686176 {\*\bkmkend _Ref107568528}.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+\par {\*\bkmkstart _Ref107569554}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid26922\charrsid26922 \hich\af37\dbch\af0\loch\f37 2.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid26922\charrsid26922 Access Through a SNS.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2827188 To use some of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 the functionality of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , you }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8345311 will need to}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8345311 authenticate }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 your Account with }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2827188 your }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid26922 GitHub }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Third-Party Account, by allowing }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ to access your Third-Party Account, as is permitted under the applicable terms and conditions that govern your use of each Third-Party Account.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 You represent that you are entitled to disclose your Third-Party Account login information to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ and/or grant }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ access to your Third-Party Account (including, but not limited to, for use for the purposes described herein) without breach by you of any of the terms and conditions that govern your use of the applicable Third-Party Account and without obligating }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  to pay any fees or making }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  subject to any usage limitations imposed by such third-party service providers.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 By granting }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  access to any Third-Party Accounts, you understand that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  may access, make available and store (if applicable) any information, data, text, software, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7222915 

+articles, applications, packages, designs, features, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 sound, photographs, graphics, video, messages, tags and/or other materials accessible through }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 (collectively, }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 \'93Content\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) that you have provided to and stored in your Third-Party Account (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 \'93}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid7222915 SNS }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637  Content\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+) so that it is available on and through }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 via your Account}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6515459 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Unless otherwise specified in the Agreement, all SNS Content shall be considered to be }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid5070732 Your Content}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  (as defined in 

+}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid5337249 Section }{\field{\*\fldinst {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11876500  REF _Ref107568852 \\w \\h }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11876500 {\*\datafield 

+08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100300037003500360038003800350032000000}}}{\fldrslt {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7807245 3.1}}}\sectd \ltrsect

+\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9456234  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3745495\charrsid5337249 (Types of Content)}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) for all purposes of the Agreement.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Depending on the Third-Party Accounts you choose and subject to the privacy settings that you have set in such Third-Party Accounts, personally identifiable information that you post to your Third-Party Accounts may be available on and through your Accoun

+t on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Please note that if a Third-Party Account or associated service becomes unavailable}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6515459 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  or }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote 

+s access to such Third-Party Account is terminated by the third-party service provider, then SNS Content will no longer be available on and through }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183   }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+PLEASE NOTE THAT YOUR RELATIONSHIP WITH THE THIRD-PARTY SERVICE PROVIDERS ASSOCIATED WITH YOUR THIRD-PARTY ACCOUNTS IS GOVERNED SOLELY BY YOUR AGREEMENT(S) WITH SUCH THIRD-PARTY SERVICE PROVIDERS, AND }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 

+ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  DISCLAIMS ANY LIABILITY FOR PERSONAL

+LY IDENTIFIABLE INFORMATION THAT MAY BE PROVIDED TO IT BY SUCH THIRD-PARTY SERVICE PROVIDERS IN VIOLATION OF THE PRIVACY SETTINGS THAT YOU HAVE SET IN SUCH THIRD-PARTY ACCOUNTS.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  makes no effort to review any SNS Content for any purpose, including but not limited to, for accuracy, legality or noninfringement, and }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is not responsible for any SNS Content}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6515459 .}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637 {\*\bkmkend _Ref107569554}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 2.3\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Registration Data}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 In registering an account on the Website}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid1710880  or }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4420262 authenticating}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1710880  your Account with }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4420262 your Third-Party Account}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , you agree to (a) provide true, accurate, current}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid15936580 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ and complete information about yourself as prompted by the registration form (the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 \'93Registration Data\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 )}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5725259 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  and (b) maintain and promptly update the Registration Data to keep it true, accurate, current}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid15936580 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  and complete.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You represent that you are (

+}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5337249 i}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7883253 at least thirteen (13) years old}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid6515459 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3742238\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 and (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5337249 ii}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) not a person barred from using }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 under the laws of the United States, your place of residence or any other applicable jurisdiction.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 

+ }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You are responsible for all activities that occur under your Account.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 You agree that you shall monitor your Account to restrict use by minors, and you will accept full responsibility for any unauthorized use of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 by minors}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16603438 .}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You may not share your Account or password with anyone, and you agree to notify }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  immediately of any unauthorized use of your password or any other breach of security.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 If you provide any information that is untrue, inaccurate, not current or incomplete, or }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ has reasonable grounds to suspect that any information you provide is untrue, inaccurate, not current or incomplete, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ has the right to suspend or terminate your Account and refuse any and all current or future use of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 (or any portion thereof).}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 You agree not to create an Account using a false identity or information, or on behalf of someone other than yourself.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 You agree that you shall not }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13503161 use a }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13503161\charrsid13503161 Third-Party Account}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid13503161  that you do not own or are not authorized to use to log in to create an Account. }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ reserves the right to remove or reclaim any usernames at any time and for any reason, including but not limited to, claims by a third party that a username violates the third party\rquote s rights.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You agree not to create an Account or use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 if you have been previously removed by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 , or if you have been previously banned from any of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 .

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 2.4\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Your Account}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Notwithstanding anything to the contrary herein, you acknowledge and agree that you shall have no ownership or other property interest in your Account, and you further acknowledge and agree that all rights in and

+ to your Account are and shall forever be owned by and inure to the benefit of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6515459 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid1800369 \hich\af37\dbch\af0\loch\f37 3.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid1800369 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\insrsid984291\charrsid1800369 RESPONSIBILITY FOR CONTENT}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid6515459 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid1800369 

+\par {\*\bkmkstart _Ref107568852}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 3.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 Types of Content}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 You acknowledge that all }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8537998 Content}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , including }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , is the sole responsibility of the party from whom such Content originated.}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 This means that you, and not }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+, are entirely responsible for all Content that you upload, post, e-mail, transmit or otherwise make available (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 \'93Make Available\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 ) through }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 \'93Your Content\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+), and that you and other Registered Users of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , and not }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , are similarly responsible for all Content that you and they Make Available through }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 (}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid8085629 \'93User Content\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ).{\*\bkmkend _Ref107568852}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 3.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Storage}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1735266\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Unless expressly agreed to by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  in writing elsewhere, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ has no obligation to store any of Your Content that you Make Available on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ has no responsibility or liability for the deletion or accuracy of any Content, including Your Content; the failure to store, transmit or receive transmission of Content; or the security, privacy, storage, or trans

+mission of other communications originating with or involving use of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Certain }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637  may enable you to specify the level at which such }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ restrict access to Your Content.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You are solely responsible for applying the appropriate level of access to Your Content.}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 If you do not choose, the system may default to its most permissive setting.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }

+{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You agree that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ retains the right to create reasonable limits on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote 

+s use and storage of the Content, including Your Content, such as limits on file size, storage space, processing capacity, and similar limits described on the Website and as otherwise determined by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed

+}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  in its sole discretion. 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 4.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 OWNERSHIP}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid6913897\charrsid13175079 \hich\af37\dbch\af0\loch\f37 4.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \ab\af37\afs22 

+\ltrch\fcs0 \b\insrsid6913897\charrsid13175079 The }{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \b\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 Except with respect to Your Content and User Content, you agree that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629  and its suppliers own all rights, title and interest in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 (including but not limited to, any computer code, themes, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6766409 concepts, }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid8085629 objects,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6766409  artwork,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6766409\charrsid6766409  text, data, articles, images, pho

+tographs, graphics, applications, packages, designs, features, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 methods of operation, moral rights, documentation, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5337249 and }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5337249  software}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11801578 )}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1664216 .}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 You will not remove, alter or obscure any copyright, trademark, service mark or other proprietary rights notices incorporated in or accompanying any }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 .

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 4.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Trademarks}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358\charrsid5601434 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13701201\charrsid5601434  Industries}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  and all related graphics, logos, service marks and trade names used on or in connection with any }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 or in connection with the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  are the trademarks of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ and may not be used without permission in connection with your}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4215344 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  or any third-party}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid4215344 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  products or services.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Other trademarks, service marks and trade names that may appear on or in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 are the property of their respective owners.

+\par {\*\bkmkstart _Ref107824809}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 4.3\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 

+Your Content}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  does not claim ownership of Your Content.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 However, when you as a Registered User post or

+ publish Your Content on or in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+, you represent that you own and/or have a royalty-free, perpetual, irrevocable, worldwide, non-exclusive right (including any moral rights) and license to use, license, reproduce, modify, adapt, publish, translate

+, create derivative works from, distribute, derive revenue or other remuneration from, and communicate to the public, perform and display Your Content (in whole or in part) worldwide and/or to incorporate it in other works in any form, media or technology

+ now known or later developed, for the full term of any worldwide intellectual property right that may exist in Your Content.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 {\*\bkmkend _Ref107824809} }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+\par {\*\bkmkstart _Ref107824820}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 4.4\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid13058527 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 License to Your Content}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Subject to any applicable account settings that you select, you grant }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  a fully paid, royalty-free, per

+petual, irrevocable, worldwide, royalty-free, non-exclusive and fully sublicensable right (including any moral rights) and license to use, license, distribute, reproduce, modify, adapt, publicly perform, and publicly display Your Content (in whole or in p

+art) for the purposes of operating and providing }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6913897\charrsid8607742  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 to you and to our other Registered Users.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Please remember that other Registered Users may search for, see, use, modify and reproduce any of Your Content that you submit to any \'93public\'94 area of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6766409  or }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+You warrant that the holder of any worldwide intellectual property right, including moral rights, in Your Content, has completely and effectively waived all such rights and validly and irrevocably granted to you the right to grant the license stated above

+}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You agree that you, not }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 

+Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , are responsible for all of Your Content that you Make Available on or in }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 Service}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid4215344 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13058527 {\*\bkmkend _Ref107824820} }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid13058527 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \ab\af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid13058527\charrsid13058527 \hich\af37\dbch\af0\loch\f37 4.5\tab}}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid13058527\charrsid13058527 Performance Data}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13058527 . }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527 You hereby grant Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid13058527 

+ a perpetual, irrevocable, non-exclusive, transferable, freely sublicensable license to collect, analyze, use and exploit aggregated non-personally identifiable data related to the provision and performance of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid13058527 Service, and your}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid13058527  use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527 of the Service }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid13058527 (

+\'93}{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \b\insrsid13058527\charrsid13058527 Performance Data}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid13058527 \'94) in any manner, including without limitation to operate and improve the }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527 Service and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid13058527 other }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527 Zed\rquote s}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid13058527\charrsid13058527  products and services.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid13058527 

+\par {\*\bkmkstart _Ref107824829}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 4.6\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Username}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Notwithstanding anything contained herein to the contrary, by submitting Your Content to any forums, comments}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4215344 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ or any other area on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6913897 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , you hereby expressly permit }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  to identify you

+ by your username (which may be a pseudonym) as the contributor of Your Content in any publication in any form, media or technology now known or later developed in connection with Your Content.{\*\bkmkend _Ref107824829}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \ab\af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid13175079\charrsid13175079 \hich\af37\dbch\af0\loch\f37 4.7\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid9918851 {\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid13175079\charrsid13175079 Feedback.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 You}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851  may from time to time provide }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9918851\charrsid9918851  suggestions or comments for enhancements or improvements, new features or functionality or other feedback with respect to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 the Service }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9918851\charrsid9918851 (\'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\insrsid9918851\charrsid9918851 Feedback}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851 \'94). }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 Zed}{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851  will have full discretion to determine whether or not to proceed with the development of any requested enhancements, improvements, new features or functionality. }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9918851 You}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851  hereby grant to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851 

+ a non-exclusive, worldwide, perpetual, irrevocable, royalty-free, fully paid-up, fully sublicensable and transferable right and license to use the Feedback for any purpose, including without limitation to incorporate the Feedback into any of }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851 \rquote s products and services}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 ,}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid9918851\charrsid9918851  and otherwise exploit the Feedback without restriction; provided, however that in no event shall }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851  identify 

+}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 you}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851  publicly without }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851 your}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851\charrsid9918851 

+ prior approval. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9918851  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13175079\charrsid13175079 You represent and warrant that you have all rights necessary to submit the Feedback.}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13175079\charrsid13175079 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\caps\f37\fs22\insrsid12720549 \hich\af37\dbch\af0\loch\f37 5.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid11273450 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37 \ltrch\fcs0 \b\insrsid12720549 USER CONDUCT AND }{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid12720549\charrsid15886968 CERTAIN RESTRICTIONS.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid12720549\charrsid16017562 As a condition of use, you agree not to use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid12720549\charrsid8607742  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 for any purpose that is prohibited by this Agreement or by applicable law.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid12720549\charrsid16017562 You shall not (and shall not permit any third party) }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid722758 to: ({\*\bkmkstart _Hlk115339545}a)}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6317605  }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid722758\charrsid16017562 license, sell, rent, lease, transfer, assign, reproduce, distribute, host or otherwise commercially exploit }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562  or any portion of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid5010714 , }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9918851\charrsid9918851 or otherwise offer access to the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid9918851\charrsid9918851  to a third party other than }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 a}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9918851\charrsid9918851 uthorized }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 u}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9918851\charrsid9918851 sers}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid6816717 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562 

+ (b) frame or utilize framing techniques to enclose any trademark, logo, or other }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562 

+ (including images, text, page layout or form) of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562 ; (c) use any metatags or other \'93hidden text\'94 using }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562 \rquote 

+s name or trademarks; (d) modify, translate, adapt, merge, make derivative works of, disassemble, decompile, reverse compile or reverse engineer any part of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562 

+ except to the extent the foregoing restrictions are expressly prohibited by applicable law; (e) use any manual or automated software, devices or other processes (including but not limited to spiders, robots, scrapers, crawlers, avatar

+s, data mining tools or the like) to \'93scrape\'94

+ or download data from any web pages contained in the Website (except that we grant the operators of public search engines revocable permission to use spiders to copy materials from the Website for the sole purpose of and solely to the extent }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid6816717 necessary for creating publicly available searchable indices of the materials, but not caches or archives of such materials}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 )}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11801578 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 (f) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid6816717 

+remove or destroy any copyright notices or other proprietary markings contained on or in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid722758 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714  (g) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 circumvent or otherwise interfere with any authentication or security measures of }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5010714 the Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 , or otherwise interfere with or disrupt the integrity or performance of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 the}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714  Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 ; (h) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 use or access the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5010714 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714  to develop a product or service that is competitive with the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 Service }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5010714\charrsid5010714 or engage in competitive analysis or benchmarking}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 ; (i) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 

+create, derive, access, inspect or permit or assist any third party to create, access, inspect or derive data or datasets in the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5010714\charrsid5010714  other than through designated GUI / SDK / API end points or the source code to any software composing the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5010714\charrsid5010714 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714  (j) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 transmit}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714  }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5010714\charrsid5010714 unlawful, infringing or harmful data or code, or any data or code that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 you are}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 

+ not authorized to transmit, either to or from }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5010714\charrsid5010714 Service;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758  or }{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \insrsid6317605 (}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13058527 k}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6317605 )}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562  take any action or (}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid13058527 l}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 ) Make Available any Content on or through }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549\charrsid8607742  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 

+that: (i) is unlawful, threatening, abusive, harassing, defamatory, libelous, deceptive, fraudulent, invasive of another\rquote s privacy, tortious, obscene, offensive, or profane; (ii) constitutes un

+authorized or unsolicited advertising, junk or bulk e-mail; (i}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6317605 ii}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 

+) involves commercial activities and/or sales, such as contests, sweepstakes, barter, advertising, or pyramid schemes without }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 \rquote 

+s prior written consent; (}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6317605 i}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 v) impersonates any person or entity, including any employee or representative of }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 ;}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6317605  or}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562  (v) interferes with or attempt}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid12720549\charrsid6320703 s}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562  to interfere with the proper functioning of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549\charrsid8607742  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 or uses }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549 the }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549\charrsid8607742  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12720549\charrsid16017562 in any way not expressly permitted by this Agreement}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid12720549 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 {\*\bkmkend _Hlk115339545}Without limiting the foregoing, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid722758\charrsid16017562 except as expressly stated herein, no part of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid722758\charrsid16017562  may be copied, reproduced, distributed, republished, downloa}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid6816717 ded, displayed, posted or transmitted in any form or by any means}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid722758 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 The }{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid2427524 rights granted to you in th}{\rtlch\fcs1 

+\ab\af37\afs22 \ltrch\fcs0 \insrsid722758 is Agreement are subject to your compliance with the restrictions set forth in this }{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \insrsid14295472 s}{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \insrsid722758 ection}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid6816717 Any future release, up}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid722758\charrsid16017562 date or other addition to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid722758\charrsid16017562 

+ shall be subject to the Agreement.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549\charrsid16017562 

+, its suppliers and service providers reserve all rights not granted in the Agreement. Any unauthorized use of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid462650 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid462650  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549\charrsid16017562 terminates the licenses granted by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid12720549\charrsid16017562  pursuant to the Agreement}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12720549 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid11273450 \hich\af37\dbch\af0\loch\f37 6.\tab}}\pard \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4356118 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid984291\charrsid11273450 INVESTIGATIONS}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\insrsid3694660 ,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid676335  MONITORING}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid3694660 , &}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\insrsid3435047\charrsid11273450  NO OBLIGATION TO PRE-SCREEN CONTENT}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\insrsid4215344\charrsid11273450 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid11273450  may, but is not obligated to, }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid676335 investigate, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid11273450 monitor}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid462650\charrsid11273450 , pre-screen,}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid11273450\charrsid11273450  remove, refuse,}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid11273450  or review }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid3435018\charrsid11273450 the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 Service}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid11273450  and}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid462650\charrsid11273450 /or}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid11273450  Content}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid462650\charrsid11273450 , including Your Content}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid11273450\charrsid11273450  and User Content}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid462650\charrsid11273450 ,}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid8085629\charrsid11273450  at any time. }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118\charrsid7358637 By entering into the Agreement, you hereby provide your irrevocable consent to such monitoring.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118\charrsid7358637 You acknowledge and agree that you have no expectation of privacy concerning the transmission of Your Content, including without

+ limitation chat, text, or voice communications.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118\charrsid7358637 In the event that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118\charrsid7358637  pre-screens, refuses or removes any Content, you acknowledge that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid4356118\charrsid7358637  will do so for }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118\charrsid7358637 \rquote s benefit, not yours.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid4356118\charrsid11273450 

+\par }\pard \ltrpar\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4356118 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid11273450 Without limiting the foregoing, }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335  reserves the right to: (a) remove or 

+refuse to post any of your Content for any or no reason in our sole discretion; (b) take any action with respect to any of your Content that we deem necessary or appropriate in our sole discretion, including if we believe that such Content violates this A

+greement, infringes any intellectual property right or other right of any person or entity, threatens the personal safety of users of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid676335\charrsid676335  or the public, or could create liability for the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335 

+; (c) disclose your identity or other information about you

+ to any third party who claims that material posted by you violates their rights, including their intellectual property rights or their right to privacy; (d) take appropriate legal action, including without limitation, referral to}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid4356118  and cooperation with}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335  law enforcement}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118  and/or other applicable legal authorities}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335 , for any illegal or unauthorized use of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118  or if }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118  otherwise believes that criminal activity has occurred}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid11616764  and/or}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335  (e) terminate or suspend your access to all or part of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid676335\charrsid676335  for any or no reason, including without limitation, any violation of this Agreement.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335 

+ Upon determination of any possible violations by you of any provision of the Agreement, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335 

+, may, at its sole discretion immediately terminate your license to use the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335 

+, or change, alter or remove Your Content, in whole or in part, without prior notice to you.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335\charrsid676335 

+\par }\pard \ltrpar\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid676335 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450 If}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid4356118  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450  believes that criminal activity has occurred, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450  reserves the right to}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid676335 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4356118  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid676335\charrsid11273450 except to the extent prohibited by applicable law}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9048421 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450 

+ disclose any information or materials on or in the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450 , including Your Content, in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450 \rquote s possession in connection with your use of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid11273450\charrsid11273450 , to (i) comply with applicable laws, legal process or governmental request}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7930367 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450 

+ (ii) enforce the Agreement, (iii) respond to any claims that Your Content violates the rights of third parties, (iv) respond to your requests for customer service, or (v) protect the rights, property or personal safety of }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450 , its Registered Users or the public, and all enforcement or other government officials, as }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11273450\charrsid11273450  in its sole discretion believes to be necessary or appropriate.

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 7.\tab}}\pard \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 INTERACTIONS WITH OTHER USERS}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid4215344 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 7.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 User Responsibility}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 You are solely responsible for your interactions with other Registered Users and any other parties with whom you interact}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7930367  through the Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid8085629 ; provided, however, that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+ reserves the right, but has no obligation, to intercede in such disputes.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 You agree that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  will not be responsible for any liability incurred as the result of such interactions}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4215344 .}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 7.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Content Provided by Other Users}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7930367 T}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 he }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018\charrsid8607742  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 may contain User Content provided by other Registered Users.}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is not responsible for and does not control User Content. }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  does not approve, endorse or make any representations or warranties with respect to}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid9529644 ,}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637  User Content.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+You use all User Content and interact with other Registered Users at your own risk.

+\par {\*\bkmkstart _Ref107826149}{\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid5510358 \hich\af37\dbch\af0\loch\f37 8.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid7545100 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid5510358 NO }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 FEES}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid5510358 .}{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \super\insrsid4673694 

+{\*\bkmkend _Ref107826149} }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4673694\charrsid4673694 Company does not currently charge a fee for its Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4673694 . }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid4673694\charrsid4673694 However, it reserves the right to charge such fees in the future.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7545100 

+\par {\*\bkmkstart _Ref107568885}{\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 9.\tab}}\pard \ltrpar

+\s44\qj \li0\ri0\sa240\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 DISCLAIMER OF WARRANTIES AND CONDITIONS}{

+\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7110561 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 {\*\bkmkend _Ref107568885}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 9.1\tab}}\pard\plain \ltrpar\s46\qj \li0\ri0\sa240\keepn\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 As Is}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+ YOU EXPRESSLY UNDERSTAND AND AGREE THAT TO THE EXTENT PERMITTED BY APPLICABLE LAW, YOUR USE OF }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13175079 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629  IS AT YOUR SOLE RISK, AND }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13175079 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018\charrsid8607742  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ARE PROVIDED ON AN \'93AS IS\'94 AND \'93AS AVAILABLE\'94 BASIS, WITH ALL FAULTS.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+ PARTIES EXPRESSLY DISCLAIM ALL WARRANTIES, REPRESENTATIONS, AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEM

+ENT}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \dbch\af31506\insrsid12010400  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 ARISING FROM USE OF THE WEBSITE.

+\par {\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid5510358 \hich\af37\dbch\af0\loch\f37 (a)\tab}}\pard\plain \ltrpar

+\s48\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl2\outlinelevel2\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  PARTIES MAKE NO WARRANTY, REPRESENTATION OR CONDITION THAT: (1) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637 WILL MEET YOUR REQUIREMENTS; (2) YOUR USE OF }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid3435018 THE }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  WILL BE UNINTERRUPTED, TIMELY, SECURE OR ERROR-FREE; OR (3) THE RESULTS THAT MAY BE OBTAINED FROM USE OF }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid3435018 THE }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629\charrsid7358637  WILL BE ACCURATE OR RELIABLE.

+\par {\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid7358637 \hich\af37\dbch\af0\loch\f37 (b)\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ANY CONTENT DOWNLOADED FROM OR OTHERWISE ACCESSED THROUGH }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ IS ACCESSED AT YOUR OWN RISK, AND YOU SHALL BE SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR PROPERTY, INCLUDING, BUT NOT LIMITED TO, YOUR COMPUTER SYSTEM AND ANY DEVICE YOU USE TO ACCESS }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , OR ANY OTHER LOSS THAT RESULTS FROM ACCESSING SUCH CONTENT.

+\par {\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid7358637 \hich\af37\dbch\af0\loch\f37 (c)\tab}THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  MAY BE SUBJECT TO DELAYS, CANCELLATIONS AND OTHER DISRUPTIONS.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  MAKES NO WARRANTY, REPRESENTATION OR CONDITION WITH RESPECT TO }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+, INCLUDING BUT NOT LIMITED TO, THE QUALITY, EFFECTIVENESS, REPUTATION AND OTHER CHARACTERISTICS OF }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .

+\par {\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid7358637 \hich\af37\dbch\af0\loch\f37 (d)\tab}NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  OR THROUGH }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637  WILL CREATE ANY WARRANTY NOT EXPRESSLY MADE HEREIN.

+\par {\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid7358637 \hich\af37\dbch\af0\loch\f37 (e)\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 From time to time, }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637  may offer new \'93beta\'94

+ features with which its users may experiment. Such features or tools are offered solely for experimental purposes and without any warranty of any kind, and may be modified or discontinued at }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid5510358 Zed}

+{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 \rquote s sole discretion.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 

+The provisions of this section apply with full force to such features or tools.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 9.2\tab}}\pard\plain \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 No Liability for Conduct of Third Parties}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ YOU ACKNOWLEDGE AND AGREE THAT }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  PARTIES ARE NOT LIABLE, AND YOU AGREE NOT TO SEEK TO HOLD }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  PARTIES LIABLE, FOR THE CONDUCT OF THIRD PARTIES, INCLUDING OPERATORS OF EXTERNAL SITES, AND THAT THE RISK OF }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid12010400 ECONOMIC DAMAGES}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  FROM SUCH THIRD PARTIES RESTS ENTIRELY WITH YOU}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 9.3\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid8085629 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 No Liability for Conduct of Other Users}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+YOU ARE SOLELY RESPONSIBLE FOR ALL OF YOUR COMMUNICATIONS AND INTERACTIONS WITH OTHER USERS OF }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 YOU UNDERSTAND THAT }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  DOES NOT MAKE ANY ATTEMPT TO VERIFY THE STATEMENTS OF USERS}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11360041 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid686739  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid5510358 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16672008\charrsid7366451  MAKES NO WARRANTY THAT THE GOODS OR }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16672008\charrsid7366451 

+ PROVIDED BY THIRD PARTIES WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid4280288  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid16672008\charrsid7366451  MAKES NO WARRANTY REGARDING THE QUALITY OF ANY SUCH GOODS OR }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16672008\charrsid7366451 

+, OR THE ACCURACY, TIMELINESS, TRUTHFULNESS, COMPLETENESS OR RELIABILITY OF ANY USER CONTENT OBTAINED THROUGH }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018\charrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid16672008\charrsid7366451 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8085629 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid13395722\charrsid7873369 \hich\af37\dbch\af0\loch\f37 9.4\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid13395722 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid13395722\charrsid7873369 Third-Party Materials}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid13395722\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13395722\charrsid7358637 As a part of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 the }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13395722\charrsid7358637 , you may have access to materials that are hosted by another party.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid13395722\charrsid7358637 You agree that it is impossible for }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13395722\charrsid7358637 

+ to monitor such materials and that you access these materials at your own risk.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13395722 

+\par {\*\bkmkstart _Ref107569174}{\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 10.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid3040103 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 LIMITATION OF LIABILITY}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 {\*\bkmkend _Ref107569174}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 10.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\keepn\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid3040103 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 Disclaimer of Certain Damages}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 YOU UNDERSTAND AND AGREE THAT}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13249622 , TO THE FULLEST EXTENT PROVIDED BY LAW, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 IN NO EVENT SHALL }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  PARTIES BE LIABLE FOR ANY L

+OSS OF PROFITS, REVENUE OR DATA, INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES, OR DAMAGES OR COSTS DUE TO LOSS OF PRODUCTION OR USE, BUSINESS INTERRUPTION, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3625343 OR }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid8085629 PROCUREMENT OF SUBSTITUTE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid223431\charrsid8085629  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 IN EACH CASE WHETHER OR NOT }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629  HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, ARISING OUT OF OR IN CONNECTION WITH THE AGREEMENT OR ANY COMMUNICATIONS, INTERACTIONS OR MEETINGS WITH OTHER USERS OF }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , ON ANY THEORY OF LIABILITY, RESULTING FROM: (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 a}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ) THE USE OR INABILITY TO USE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 ; (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 b}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ) THE COST OF PROCUREMENT OF SUBSTITUTE GOODS OR }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  RESULTING FROM ANY GOODS, DATA, INFORMATION OR }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629  PURCHASED OR OBTAINED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid12222103 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  OR MESSAGES RECEIVED FOR TRANSACTIONS ENTERED INTO THROUGH }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ; (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 c}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ) UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 d}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+) STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ; OR (}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8407714 e}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ) ANY OTHER MATTER RELATED TO }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 

+SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , WHETHER BASED ON WARRANTY, COPYRIGHT, CONTRACT, TORT (INCLUDING NEGLIGENCE), OR ANY OTHER LEGAL THEORY.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 THE FOREGOING }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3625343 LIMITATION OF}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  LIABILITY SHALL NOT APPLY TO LIABILITY OF A }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  PARTY FOR (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 i}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 ) DEATH OR PERSONAL INJURY CAUSED BY A }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  PARTY\rquote S NEGLIGENCE; OR FOR (}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8407714 ii}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 ) ANY INJURY CAUSED BY A }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629  PARTY\rquote S FRAUD OR FRAUDULENT MISREPRESENTATION}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 

+\par {\*\bkmkstart _Ref107568947}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 10.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Cap on Liability}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \insrsid8390183\charrsid13528286  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13249622 TO THE FULLEST EXTENT PROVIDED BY LAW, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  PARTIES }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13249622 WILL NOT }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+BE LIABLE TO YOU FOR MORE THAN THE GREATER OF (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 a}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) THE TOTAL AMOUNT PAID TO }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\caps\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637  by you}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid686739  (if any)}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 

+ during the one-month period prior to the act, omission or occurrence giving rise to such liability}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid12136496 ;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637  (}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8407714 b}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 ) }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid6374365 $100;}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\cs17\caps\super\insrsid12136496\charrsid12136496  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid6374365 or (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6374365\charrsid8407714 c}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid6374365 ) }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 THE REMEDY OR PENALTY IMPOSED BY THE STATUTE UNDER WHICH SUCH CLAIM ARISES.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 THE FOREGOING CAP ON LIABILITY SHALL NOT APPLY TO LIABILITY OF A }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  PARTY FOR (}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8407714 i}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) DEATH OR PERSONAL INJURY CAUSED BY A }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  PARTY\rquote S NEGLIGENCE; OR FOR (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8407714 ii}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) ANY INJURY CAUSED BY A }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  PARTY\rquote S FRAUD OR FRAUDULENT MISREPRESENTATION}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 {\*\bkmkend _Ref107568947}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 10.3\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 User Content}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 EXCEPT FOR }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote S OBLIGATIONS TO PROTECT YOUR PERSONAL DATA AS SET FORTH IN THE }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 \rquote S PRIVACY POLICY, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  ASSUMES NO RESPONSIBILITY FOR 

+THE TIMELINESS, DELETION, MIS-DELIVERY OR FAILURE TO STORE ANY CONTENT (INCLUDING, BUT NOT LIMITED TO, YOUR CONTENT AND USER CONTENT), USER COMMUNICATIONS OR PERSONALIZATION SETTINGS.

+\par {\*\bkmkstart _Ref107568980}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid12136496 \hich\af37\dbch\af0\loch\f37 10.4\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid12136496 Exclusion of Damages.}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12136496\charrsid7366451 CERTAIN JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid8390183  }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12136496\charrsid7366451 IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE EXCLUSIONS OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MIGHT HAVE ADDITIONAL RIGHTS.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid12136496\charrsid12136496 {\*\bkmkend _Ref107568980}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 10.5\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Basis of the Bargain}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 THE LIMITATIONS OF DAMAGES SET FORTH ABOVE ARE FUNDAMENTAL ELE

+MENTS OF THE BASIS OF THE BARGAIN BETWEEN }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 ZED}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  AND YOU}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 11.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 PROCEDURE FOR MAKING CLAIMS OF COPYRIGHT INFRINGEMENT}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 It is }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote 

+s policy to terminate membership privileges of any Registered User who repeatedly infringes copyright upon prompt notification to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  by the copyright owner or the copyright owner\rquote s legal agent.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Without limiting the foregoing, if you believe that your work has been copied and posted on}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018  the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+ in a way that constitutes copyright infringement, please provide our Copyright Agent with the following information: (a) an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright interest; (b) a descripti

+on of the copyrighted work that you claim has been infringed; (c) a description of the location on }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3435018 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid3435018\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+of the material that you claim is infringing; (d) your address, telephone number and e-mail address; (e) a written statement by you that you have 

+a good faith belief that the disputed use is not authorized by the copyright owner, its agent or the law; and (f) a statement by you, made under penalty of perjury, that the above information in your notice is accurate and that you are the copyright owner

+ or authorized to act on the copyright owner\rquote s behalf.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Contact information for }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote s Copyright Agent for notice of claims of copyright infringem}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid11864119 ent is as follows:}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid11864119\charrsid11864119  Copyright Manager, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \cf32\chshdng0\chcfpat0\chcbpat33\insrsid11864119\charrsid11864119 4425 NE 34th Ave}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\cf32\insrsid11864119\charrsid11864119 , }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \cf32\chshdng0\chcfpat0\chcbpat33\insrsid11864119\charrsid11864119 Portland OR 97211}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015\charrsid11864119 .}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 12.\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 TERM AND TERMINATION}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 12.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4738878 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 Term}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+The Agreement commences on}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4738878  the earlier of: (a)}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4738878\charrsid4738878 

+the date you first used }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid4738878\charrsid4738878 or}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4738878\charrsid8085629  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 the date when you accept}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid7545100 

+ the Agreement}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  (as described in the preamble above)}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4738878 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  and }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid4738878 will }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 remain in full force and effect while you use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 , unless terminated earlier in accordance with the Agreement.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid4738878 

+\par {\*\bkmkstart _Ref107569016}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 12.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Termination of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291  by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid10714704 I}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 f you have materially breached any provision of the Agreement, or if }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is required to do so by law (e.g., where the provision of the Website, the Application, the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid13058527 Tool }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 or the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is, or b

+ecomes, unlawful), }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  has the right to, immediately and without notice, suspend or terminate any }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  provided to you.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+You agree that all terminations for cause shall be made in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote s sole discretion and that }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  shall not be liable to you or any third party for any termination of your Account.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 {\*\bkmkend _Ref107569016}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 12.3\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Termination of }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291  by You}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 If you want to terminate the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ provided by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , you may do so by (a) notifying }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  at any time and (b) closing your Account for all of the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ that you use.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Your notice should be sent, in writing, to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote s address set forth below.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 12.4\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Effect of Termination}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Termination of any Service includes removal of access to such Service and barring of further use of the Service.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Termination of all }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  also includes dele

+tion of your password and all related information, files and Content associated with or inside your Account (or any part thereof), including Your Content.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 Upon termination of any Service, your right to use such Service will automatically terminate immediately}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  You understand that any termination of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ may involve deletion of Your Content associated therewith from our live databases.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  will not have any liability whatsoever to you for any suspension or termination, including for deletion of Your Content.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 All provisions of the Agreement which by their nature should survive, shall survive termination of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 , including without limitation, ownership provisions,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527  performance data rights,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ warranty disclaimers, and limitation of liability.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 12.5\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 No Subsequent Registration}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 If your registration(s) with}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid14838117 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  or ability to access}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid14838117 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  or any other }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 

+Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  community}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid14838117 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is discontinued by }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  due to your violation of any portion of the Agreement or for conduct otherwise inappropriate for the community, then you agree th

+at you shall not attempt to re-register with or access }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 or any }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ community through use of a different member name or otherwise, and you acknowledge that you will not be entitled to receive a refund for fees related to those }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637  to which your access has been terminated.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+In the event that you violate the immediately preceding sentence, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ reserves the right, in its sole discretion, to immediately take any or all of the actions set forth herein without any notice or warning to you.

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 13.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 INTERNATIONAL USERS}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 The }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+can be accessed from countries around the world and may contain references to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ and Content that are not available in your country.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 These references do not imply that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  intends to announce such }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ or Content in your country.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 The }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 are controlled and offered by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  from its facilities in the United States of America.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  makes no representations that }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 are appropriate or available for use in other locations.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Those who access or use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 from other countries do so at their own volition and are responsible for compliance with local law.

+\par {\*\bkmkstart _Ref110231050}{\*\bkmkstart _Ref110233665}{\*\bkmkstart _Ref107568792}{\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \ai\af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid12729868 \hich\af37\dbch\af0\loch\f37 14.\tab}}\pard \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid12729868 {\rtlch\fcs1 \ai\af37\afs22 \ltrch\fcs0 \b\cf34\insrsid12729868 ARBITRATION AGREEMENT}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\cf34\insrsid8085629\charrsid3040103 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\cf34\insrsid14295472 {\*\bkmkend _Ref110231050} Please read this section (the \'93Arbitration Agreement\'94) carefully. It is part of your contract with }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\cf34\insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\cf34\insrsid14295472  and affects your rights. It contains procedures for MANDATORY BINDING ARBITRATION AND A CLASS ACTION WAIVER.}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 

+\cf34\insrsid8390183\charrsid13528286 {\*\bkmkend _Ref110233665} }{\rtlch\fcs1 \af37 \ltrch\fcs0 \cf34\insrsid12729868\charrsid12729868 {\*\bkmkend _Ref107568792}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 14.1\tab}}\pard\plain \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid12729868 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Applicability of Arbitration Agreement. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+Subject to the terms of this Arbitration Agreement, you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid1397381 \hich\af37\dbch\af43\loch\f37 agree that any dispute, claim, disagreements arising out of or relating in any way to your access to or use of the }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid1397381 \hich\af37\dbch\af43\loch\f37 , any }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 c}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid1397381 \hich\af37\dbch\af43\loch\f37 ommun}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 ications you receive, any products sold or distributed through the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  or the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37 of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110\charrsid16074195 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 and prior versions of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 T\hich\af37\dbch\af43\loch\f37 erms}{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+, including claims and disputes that arose between us before the effective date of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 \hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid10505110\charrsid10505110 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37 of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 \hich\f37  (each, a \'93}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Dispute}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\loch\af37\dbch\af43\hich\f37 \'94\loch\f37 ) will be resolved by binding arbitration, rather than in court, except that: (1) you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  may assert claims or seek relie\hich\af37\dbch\af43\loch\f37 f in small claims court if such claims qualify and remain in small claims court; and (2) you or }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+ may seek equitable relief in court for infringement or other misuse of intellectual property rights (such as trademarks, trade dress, domain names, trade se\hich\af37\dbch\af43\loch\f37 \hich\f37 

+crets, copyrights, and patents). For purposes of this Arbitration Agreement, \'93\loch\f37 \hich\f37 Dispute\'94\loch\f37  will also include disputes that arose or involve facts occurring before the existence of this or any prior versions of the }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110\charrsid10505110 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37 of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  as well as claims that may arise \hich\af37\dbch\af43\loch\f37 

+after the termination of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 \hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 14.2\tab}}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 Informal Dispute Resolution. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+There might be instances when a Dispute arises between you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 

+\hich\af37\dbch\af43\loch\f37 . If that occurs, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+ is committed to working with you to reach a reasonable resolution. You and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 

+\hich\af37\dbch\af43\loch\f37  agree that goo\hich\af37\dbch\af43\loch\f37 \hich\f37 d faith informal efforts to resolve Disputes can result in a prompt, low\u8208\'5f\loch\f37 cost and mutually beneficial outcome}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid14295472 \hich\af37\dbch\af43\loch\f37 \hich\f37  (\'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\dbch\af43\insrsid14295472 \hich\af37\dbch\af43\loch\f37 Informal Dispute Resolution}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid14295472 

+\loch\af37\dbch\af43\hich\f37 \'94\loch\f37 )}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 . You and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  therefore agree that before either party commences arbitration against the other (or initiates an act\hich\af37\dbch\af43\loch\f37 \hich\f37 

+ion in small claims court if a party so elects), we will personally meet and confer telephonically or via videoconference, in a good faith effort to resolve informally any Dispute covered by this Arbitration Agreement (\'93}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37 Informal Dispute Resolution Conferen\hich\af37\dbch\af43\loch\f37 ce}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \loch\af37\dbch\af43\hich\f37 \'94

+\loch\f37 ). If you are represented by counsel, your counsel may participate in the conference, but you will also participate in the conference.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 14.3\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid9924602 {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+The party initiating a Dispute must give notice to the other party in writing of its intent to initiate an \hich\af37\dbch\af43\loch\f37 Informal Dispute Resolution Conference}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 

+\hich\af37\dbch\af43\loch\f37 \hich\f37  (\'93}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid4201944 \hich\af37\dbch\af43\loch\f37 Notice}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \loch\af37\dbch\af43\hich\f37 \'94

+\loch\f37 )}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 , which shall occur within 45 days after the other party receives such }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 

+\hich\af37\dbch\af43\loch\f37 N}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 otice, unless an extension is mutually agreed upon by the parties. Notice to }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  that you intend to initiate an Informal Dispute Resolu\hich\af37\dbch\af43\loch\f37 

+tion Conference should be sent by email to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602 \hich\af37\dbch\af43\loch\f37 hi@zed.dev}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+ or regular mail to our offices located at }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 \hich\af37\dbch\af43\loch\f37 2590 Welton Street Ste. 200 PMB 1916}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602 

+\hich\af37\dbch\af43\loch\f37 , }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 \hich\af37\dbch\af43\loch\f37 Denver, CO 80205}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9924602 

+\hich\af37\dbch\af43\loch\f37 \hich\f37 . The Notice must include: (1) your name, telephone number, mailing address, e\u8208\'5f\loch\f37 mail address associated wit\hich\af37\dbch\af43\loch\f37 \hich\f37 

+h your account (if you have one); (2) the name, telephone number, mailing address and e\u8208\'5f\loch\f37 mail address of your counsel, if any; and (3) a description of your Dispute.

+\par }\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid6520563 {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+The Informal Dispute Resolution Conference shall be individualized such that a separate con\hich\af37\dbch\af43\loch\f37 

+ference must be held each time either party initiates a Dispute, even if the same law firm or group of law firms represents multiple users in similar cases, unless all parties agree; multiple individuals initiating a Dispute cannot participate in the same

+\hich\af37\dbch\af43\loch\f37  \hich\af37\dbch\af43\loch\f37 Informal Dispute Resolution Conference unless all parties agree. In the time between a party receiving }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 

+the N}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+otice and the Informal Dispute Resolution Conference, nothing in this Arbitration Agreement shall prohibit the parties from engaging in informal com\hich\af37\dbch\af43\loch\f37 munications to resolve the initiating party\hich\f37 \rquote \loch\f37 

+s Dispute. Engaging in the Informal Dispute Resolution Conference is a condition precedent and requirement that must be fulfilled before commencing arbitration. The statute of limitations and any filing fee dead\hich\af37\dbch\af43\loch\f37 l

+\hich\af37\dbch\af43\loch\f37 ines shall be tolled while the parties engage in the Informal Dispute Resolution Conference process required by this section.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \ab\af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid6520563 \hich\af37\dbch\af43\loch\f37 14.4\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid6520563 {\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid6520563 \hich\af37\dbch\af43\loch\f37 Waiver of Jury Trial.}{

+\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 YOU AND }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+ HEREBY WAIVE ANY CONSTITUTIONAL AND STATUTORY RIGHTS TO SUE IN COURT AND HAVE A TRIAL IN FRONT O\hich\af37\dbch\af43\loch\f37 F A JUDGE OR A JURY. }{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12849725 

+\hich\af37\dbch\af43\loch\f37 You and }{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12849725 \hich\af37\dbch\af43\loch\f37 

+ are instead electing that all Disputes shall be resolved by arbitration under this Arbitration Agreement, except as specified in the }{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37 s}{\rtlch\fcs1 \ab\af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12849725 \hich\af37\dbch\af43\loch\f37 \hich\f37 ection entitled \'93\loch\f37 \hich\f37 Applicability of Arbitration Agreement\'94\loch\f37  above. There is no judge or jury\hich\af37\dbch\af43\loch\f37 

+ in arbitration, and court review of an arbitration award is subject to very limited review. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \ab\af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid6520563 \hich\af37\dbch\af43\loch\f37 14.5\tab}}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid6520563 \hich\af37\dbch\af43\loch\f37 Waiver of Class and Other Non-Individualized Relief.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12849725 \hich\af37\dbch\af43\loch\f37 YOU AND }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 ZED}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid12849725 \hich\af37\dbch\af43\loch\f37  AGREE THAT, EXCEPT AS SPECIFIED IN SECTION }{\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37  REF _Ref110232422 \\r \\h }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 {\*\datafield 08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100310030003200330032003400320032000000}}}{\fldrslt {\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid13058527 \hich\af37\dbch\af43\loch\f37 14.9}}}\sectd \ltrsect\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12849725 

+\hich\af37\dbch\af43\loch\f37 , EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON AN INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE BASIS, AND THE PARTIES HEREBY WAIVE ALL RIGHTS TO HAVE ANY DISPUTE BE BROUGHT, 

+\hich\af37\dbch\af43\loch\f37 

+HEARD, ADMINISTERED, RESOLVED, OR ARBITRATED ON A CLASS, COLLECTIVE, REPRESENTATIVE, OR MASS ACTION BASIS. ONLY INDIVIDUAL RELIEF IS AVAILABLE, AND DISPUTES OF MORE THAN ONE CUSTOMER OR USER CANNOT BE ARBITRATED OR CONSOLIDATED WITH THOSE OF ANY OTHER CUS

+\hich\af37\dbch\af43\loch\f37 T\hich\af37\dbch\af43\loch\f37 OMER OR USER}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+. Subject to this Arbitration Agreement, the arbitrator may award declaratory or injunctive relief only in favor of the individual party seeking relief and only to the extent necessary to provide relief warranted by the party}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid7807245 \loch\af37\dbch\af43\hich\f37 \rquote \loch\f37 s}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  individual claim\hich\af37\dbch\af43\loch\f37 

+. Nothing in this paragraph is intended to, nor shall it, affect the terms and conditions under }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37 S}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 ection }{\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37  REF _Ref110232422 \\r \\h }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid13528286 {\*\datafield 08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100310030003200330032003400320032000000}}}{\fldrslt {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13058527 

+\hich\af37\dbch\af43\loch\f37 14.9}}}\sectd \ltrsect\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 \hich\f37 entitled \'93\loch\f37 \hich\f37 Batch Arbitration.\'94\loch\f37  Notwithstanding anything \hich\af37\dbch\af43\loch\f37 \hich\f37 

+to the contrary in this Arbitration Agreement, if a court decides by means of a final decision, not subject to any further appeal or recourse, that the limitations of this section, \'93\loch\f37 \hich\f37 

+Waiver of Class and Other Non-Individualized Relief,\'94\loch\f37  are invalid or unen\hich\af37\dbch\af43\loch\f37 f\hich\af37\dbch\af43\loch\f37 

+orceable as to a particular claim or request for relief (such as a request for public injunctive relief), you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  agree that that particular claim or request for relief (and only that particular claim or request for relief) shall be severed from the ar\hich\af37\dbch\af43\loch\f37 

+bitration and may be litigated in the state or federal courts located in the State of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid2169198 \hich\af37\dbch\af43\loch\f37 Colorado}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 . All other Disputes shall be arbitrated or litigated in small claims court. This section does not prevent you or }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 

+\hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  from participating in a class-wide settlemen\hich\af37\dbch\af43\loch\f37 t of claims.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 14.6\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid411218 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Rules and Forum.}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 The }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Term}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid411218 \hich\af37\dbch\af43\loch\f37  of U}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 s}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid411218 \hich\af37\dbch\af43\loch\f37 e}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 \hich\f37 

+ evidence a transaction involving interstate commerce; and notwithstanding any other provision herein with respect to the applicable substantive law, the Federal Arbitration Act, 9 U.S.C. \'a7\loch\f37  1 }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\i\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 et seq.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 , will gover\hich\af37\dbch\af43\loch\f37 

+n the interpretation and enforcement of this Arbitration Agreement and any arbitration proceedings. If the}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37  Inf}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 ormal }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Dispute Resolution}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid14295472 \hich\af37\dbch\af43\loch\f37 p}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 rocess described}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+above does not resolve satisfactorily within }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 sixty}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  (}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 60}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 ) days after receipt of your }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Notice}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 , you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 

+\hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  \hich\af37\dbch\af43\loch\f37 \hich\f37 

+agree that either party shall have the right to finally resolve the Dispute through binding arbitration. The arbitration will be administered by the American Arbitration Association (\'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid4201944 \hich\af37\dbch\af43\loch\f37 AAA}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \loch\af37\dbch\af43\hich\f37 \'94\loch\f37 

+), in accordance with the Consumer Arbitration Rules (the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \loch\af37\dbch\af43\hich\f37 \'93}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid4201944 

+\hich\af37\dbch\af43\loch\f37 AAA \hich\af37\dbch\af43\loch\f37 Rules}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \loch\af37\dbch\af43\hich\f37 \'94}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 ) then in effect, except as modified by this section of this Arbitration Agreement. The AAA Rules are currently available at https://www.adr.org/sites/default/files/Consumer%20Rules.pdf.

+\par }\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid411218 {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+A party who wishes to initiate arbitration must provide the oth\hich\af37\dbch\af43\loch\f37 \hich\f37 er party with a request for arbitration (the \'93}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 Request}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \loch\af37\dbch\af43\hich\f37 \'94\loch\f37 ). }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 

+\hich\af37\dbch\af43\loch\f37 \hich\f37 The Request must include: (1) the name, telephone number, mailing address, e\u8208\'5f\loch\f37 

+mail address of the party seeking arbitration and the account username (if applicable) as well as the email address associ\hich\af37\dbch\af43\loch\f37 \hich\f37 

+ated with any applicable account; (2) a statement of the legal claims being asserted and the factual bases of those claims; (3) a description of the remedy sought and an accurate, good\u8208\'5f\loch\f37 

+faith calculation of the amount in controversy in United States Dollar\hich\af37\dbch\af43\loch\f37 s\hich\af37\dbch\af43\loch\f37 

+; (4) a statement certifying completion of the Informal Dispute Resolution process as described above; and (5) evidence that the requesting party has paid any necessary filing fees in connection with such arbitration.}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  

+\par \hich\af37\dbch\af43\loch\f37 If the party requesting arbitration i\hich\af37\dbch\af43\loch\f37 s represented by counsel, the Request shall also include counsel\hich\f37 \rquote \loch\f37 

+s name, telephone number, mailing address, and email address. Such counsel must also sign the Request. By signing the Request, counsel certifies to the best of counsel\hich\f37 \rquote \loch\f37 s knowledge, informati\hich\af37\dbch\af43\loch\f37 o

+\hich\af37\dbch\af43\loch\f37 

+n, and belief, formed after an inquiry reasonable under the circumstances, that: (1) the Request is not being presented for any improper purpose, such as to harass, cause unnecessary delay, or needlessly increase the cost of dispute resolution; (2) the cl

+\hich\af37\dbch\af43\loch\f37 a\hich\af37\dbch\af43\loch\f37 

+ims, defenses and other legal contentions are warranted by existing law or by a nonfrivolous argument for extending, modifying, or reversing existing law or for establishing new law; and (3) the factual and damages contentions have evidentiary support or,

+\hich\af37\dbch\af43\loch\f37  \hich\af37\dbch\af43\loch\f37 if specifically so identified, will likely have evidentiary support after a reasonable opportunity for further investigation or discovery.}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\par \hich\af37\dbch\af43\loch\f37 Unless you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+ otherwise agree, or the Batch Arbitration process discussed in }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37 S}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 ection }{\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37  REF _Ref110232422 \\r \\h }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 {\*\datafield 

+08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100310030003200330032003400320032000000}}}{\fldrslt {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13058527 \hich\af37\dbch\af43\loch\f37 1\hich\af37\dbch\af43\loch\f37 4.9}}}

+\sectd \ltrsect\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+ is triggered, the arbitration will be conducted in the county where you reside.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 Subject to the AAA Rules, the arbitrator may direct a limited and reasonable exchange of information betw\hich\af37\dbch\af43\loch\f37 

+een the parties, consistent with the expedited nature of the arbitration. If the AAA is not available to arbitrate, the parties will select an alternative arbitral forum. Your responsibility to pay any AAA fees and costs will be solely as set forth in the

+\hich\af37\dbch\af43\loch\f37  \hich\af37\dbch\af43\loch\f37 applicable AAA Rules.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\par \hich\af37\dbch\af43\loch\f37 You and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+ agree that all materials and documents exchanged during the arbitration proceedings shall be kept confidential and shall not be shared with anyone except the parties\hich\f37 \rquote \loch\f37  attorneys, accountants, or business advisors, and then

+\hich\af37\dbch\af43\loch\f37  subject to the condition that they agree to keep all materials and documents exchanged during the arbitration proceedings confidential.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  

+}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid411218 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 14.7\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid411218 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Arbitrator.}{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  The arbitrator will be either a retired judge or an attorney licensed to practice law in the state of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13701201 

+\hich\af37\dbch\af43\loch\f37 Colo\hich\af37\dbch\af43\loch\f37 rado}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13701201\charrsid16074195 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 and will be selected by the parties from the AAA}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid7807245 \loch\af37\dbch\af43\hich\f37 \rquote }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37 s roster of consumer dispute arbitrators. If the parties are unable to agree upon an arbitrator within thirty-five (35) days of delivery of the Request, then the AAA will appoint the arbitrator in accor

+\hich\af37\dbch\af43\loch\f37 dance with the AAA Rules, provided that if the Batch Arbitration process under }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37 S}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 ection }{\field{\*\fldinst {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37  REF _Ref110232422 \\r \\h }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid13528286 {\*\datafield 08d0c9ea79f9bace118c8200aa004ba90b02000000080000000e0000005f005200650066003100310030003200330032003400320032000000}}}{\fldrslt {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13058527 

+\hich\af37\dbch\af43\loch\f37 14.9}}}\sectd \ltrsect\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid13528286 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 is triggered, the AAA will appoint the arbitrator for each batch.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 14.8\tab}}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Autho\hich\af37\dbch\af43\loch\f37 rity of Arbitrator.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+The arbitrator shall have exclusive authority to resolve any Dispute, including, without limitation, disputes arising out of or related to the interpretation or application of the Arbitration Agreement, including the enforceability, revocability, scope, o

+\hich\af37\dbch\af43\loch\f37 r\hich\af37\dbch\af43\loch\f37  validity of the Arbitration Agreement or any portion of the Arbitration Agreement, except for the following: (1) all Disputes arising out of or relating to the }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 s}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 \hich\f37 ection entitled \'93\loch\f37 \hich\f37 

+Waiver of Class and Other Non-Individualized Relief,\'94\loch\f37  including any claim tha\hich\af37\dbch\af43\loch\f37 t all or part of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 s}{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 \hich\f37 ection entitled \'93\loch\f37 \hich\f37 Waiver of Class and Other Non-Individualized Relief\'94\loch\f37 

+ is unenforceable, illegal, void or voidable, or that such }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 s}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+\hich\f37 ection entitled \'93\loch\f37 \hich\f37 Waiver of Class and Other Non-Individualized Relief\'94\loch\f37  has been breached, shall be decided\hich\af37\dbch\af43\loch\f37 \hich\f37 

+ by a court of competent jurisdiction and not by an arbitrator; (2) except as expressly contemplated in the section entitled \'93\loch\f37 \hich\f37 Batch Arbitration,\'94\loch\f37 

+ all Disputes about the payment of arbitration fees shall be decided only by a court of competent jurisdiction\hich\af37\dbch\af43\loch\f37  \hich\af37\dbch\af43\loch\f37 

+and not by an arbitrator; (3) all Disputes about whether either party has satisfied any condition precedent to arbitration shall be decided only by a court of competent jurisdiction and not by an arbitrator; and (4) all Disputes about which version of the

+\hich\af37\dbch\af43\loch\f37  \hich\af37\dbch\af43\loch\f37 Arbitration Agreement applies shall be decided only by a court of competent jurisdiction and not by an arbitrator.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid12729868\charrsid16074195  }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 The arbitration proceeding will not be consolidated with any other matters or joined with any other cases or parties, except as expressly pro

+\hich\af37\dbch\af43\loch\f37 \hich\f37 vided in the section entitled \'93\loch\f37 \hich\f37 Batch Arbitration.\'94\loch\f37 

+ The arbitrator shall have the authority to grant motions dispositive of all or part of any Dispute. The arbitrator shall issue a written award and statement of decision describing the essential findings a\hich\af37\dbch\af43\loch\f37 n

+\hich\af37\dbch\af43\loch\f37 

+d conclusions on which the award is based, including the calculation of any damages awarded. The award of the arbitrator is final and binding upon you and us. Judgment on the arbitration award may be entered in any court having jurisdiction.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 14.9\tab}}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Attorneys\hich\f37 \rquote \loch\f37  Fees and Costs. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+The parties shall bear their own attorneys\hich\f37 \rquote \loch\f37 

+ fees and costs in arbitration unless the arbitrator finds that either the substance of the Dispute or the relief sought in the Request was frivolous or was brought for an improper purpo\hich\af37\dbch\af43\loch\f37 

+se (as measured by the standards set forth in Federal Rule of Civil Procedure 11(b)). If you or }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  need to invoke the authority of a court of competent jurisdiction to compel arbitration, then the party that obtains an order compelling arbitration in such

+\hich\af37\dbch\af43\loch\f37  action shall have the right to collect from the other party its reasonable costs, necessary disbursements, and reasonable attorneys}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid7807245 \loch\af37\dbch\af43\hich\f37 

+\rquote }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  fees incurred in securing an order compelling arbitration. The prevailing party in any court action relating to whether ei

+\hich\af37\dbch\af43\loch\f37 ther party has satisfied any condition precedent to arbitration, including the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Inf}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 ormal }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Dispute Resolution }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid14295472 

+\hich\af37\dbch\af43\loch\f37 p}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 rocess, is entitled to recover their reasonable costs, necessary disbursements, and reasonable attorneys\hich\f37 

+\rquote \loch\f37  fees and costs. 

+\par {\*\bkmkstart _Ref110232422}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 14.10\tab}}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Batch Arbitration. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 To inc\hich\af37\dbch\af43\loch\f37 

+rease the efficiency of administration and resolution of arbitrations, you and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 

+\hich\af37\dbch\af43\loch\f37  agree that in the event that there are one}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid14295472 -}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+hundred (100) or more individual Requests of a substantially similar nature filed against }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37  by or with the assistance of the sam\hich\af37\dbch\af43\loch\f37 

+e law firm, group of law firms, or organizations, within a thirty (30) day period (or as soon as possible thereafter), the AAA shall (1) administer the arbitration demands in batches of 100 Requests per batch (plus, to the extent there are less than 100 R

+\hich\af37\dbch\af43\loch\f37 e\hich\af37\dbch\af43\loch\f37 

+quests left over after the batching described above, a final batch consisting of the remaining Requests); (2) appoint one arbitrator for each batch; and (3) provide for the resolution of each batch as a single consolidated arbitration with one set of fili

+\hich\af37\dbch\af43\loch\f37 n\hich\af37\dbch\af43\loch\f37 \hich\f37 g and administrative fees due per side per batch, one procedural calendar, one hearing (if any) in a place to be determined by the arbitrator, and one final award (\'93}{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Batch Arbitration}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \loch\af37\dbch\af43\hich\f37 \'94\loch\f37 ).

+{\*\bkmkend _Ref110232422}

+\par }\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid411218 {\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 \hich\f37 

+All parties agree that Requests are of a \'93\loch\f37 substantially similar nature\loch\af37\dbch\af43\hich\f37 \'94\loch\f37 

+ if they arise out of or relate to the same event or factual scenario and raise the same or similar legal issues and seek the same or similar relief. To the extent the parties disagree on the application of the Batch Arbitration process, the disagreeing 

+\hich\af37\dbch\af43\loch\f37 p\hich\af37\dbch\af43\loch\f37 \hich\f37 arty shall advise the AAA, and the AAA shall appoint a sole standing arbitrator to determine the applicability of the Batch Arbitration process (\'93}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\b\dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 Administrative Arbitrator}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \loch\af37\dbch\af43\hich\f37 \'94\loch\f37 

+). In an effort to expedite resolution of any such dispute by the Administrative Arb\hich\af37\dbch\af43\loch\f37 

+itrator, the parties agree the Administrative Arbitrator may set forth such procedures as are necessary to resolve any disputes promptly. The Administrative Arbitrator\hich\f37 \rquote \loch\f37 s fees shall be paid by }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 .

+\par \hich\af37\dbch\af43\loch\f37 You and }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid16074195 \hich\af37\dbch\af43\loch\f37 

+ agree to cooperate in good faith with the AAA t\hich\af37\dbch\af43\loch\f37 

+o implement the Batch Arbitration process including the payment of single filing and administrative fees for batches of Requests, as well as any steps to minimize the time and costs of arbitration, which may include: (1) the appointment of a discovery spe

+\hich\af37\dbch\af43\loch\f37 c\hich\af37\dbch\af43\loch\f37 ial master to assist the arbitrator in the resolution of discovery disputes; and (2) the adoption of an expedited calendar of the arbitration proceedings.

+\par \hich\af37\dbch\af43\loch\f37 This Batch Arbitration provision shall in no way be interpreted as authorizing a class, collective an\hich\af37\dbch\af43\loch\f37 

+d/or mass arbitration or action of any kind, or arbitration involving joint or consolidated claims under any circumstances, except as expressly set forth in this provision.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 14.11\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid9924602 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 30-Day Right to Opt Out.}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 You have the right to opt out of the provisions of this Ar

+\hich\af37\dbch\af43\loch\f37 bitration Agreement by sending written notice of your decision to opt out to:}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9924602\charrsid9924602  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 

+\hich\af37\dbch\af43\loch\f37 2590 Welton Street Ste. 200 PMB 1916}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602 \hich\af37\dbch\af43\loch\f37 , }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 

+\hich\af37\dbch\af43\loch\f37 Denver, CO 80205}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9924602 \hich\af37\dbch\af43\loch\f37 

+, within thirty (30) days after first becoming subject to this Arbitration Agreement. Your notice must include your name and\hich\af37\dbch\af43\loch\f37  address, the email address you used to set up your }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid5510358\charrsid9924602 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9924602 \hich\af37\dbch\af43\loch\f37 

+ account (if you have one), and an unequivocal statement that you want to opt out of this Arbitration Agreement. If you opt out of this Arbitration Agreement, all other parts of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690\charrsid9924602 

+\hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110\charrsid9924602 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9924602 

+\hich\af37\dbch\af43\loch\f37  will continue to apply to you. Opting out of this Arbitration Agreement has no effect on any other arbitration agreements that you may currently have, or may enter in the future, with us.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37 14.12\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid411218 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37 Invalidity, Expiration. }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37 Except as provided in the section entitled \loch\af37\dbch\af43\hich\f37 \'93\loch\f37 \hich\f37 Waiver of Class or Other Non-Individualized Relief\'94

+\loch\f37 , if any part or parts of this Arbitration Agreement are found under the law to be invalid or unenforceable, then such specific part or parts shall be of no force and effect and shall be severed and the \hich\af37\dbch\af43\loch\f37 r

+\hich\af37\dbch\af43\loch\f37 emainder of the Arbitration Agreement shall continue in full force and effect.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37 You further agree that any Dispute that you have with }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37  as detailed in this }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Arbitration Agreement }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid15955053 \hich\af37\dbch\af43\loch\f37 must be initiated via arbitration within the applicable statute of limitation\hich\af37\dbch\af43\loch\f37 

+ for that claim or controversy, or it will be forever time barred. Likewise, you agree that all applicable statutes of limitation will apply to such arbitration in the same manner as those statutes of limitation would apply in the applicable court of comp

+\hich\af37\dbch\af43\loch\f37 e\hich\af37\dbch\af43\loch\f37 tent jurisdiction.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12729868 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\fs22\loch\af37\hich\af37\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 14.13\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid12729868 {\rtlch\fcs1 \af37 \ltrch\fcs0 \b\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 Modification. }{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 Notwithstanding any provision in }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 \hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 

+\af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  to the contrary, we agree that if }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  makes any future material change to this Arbitration Agreement, }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid14295472 \hich\af37\dbch\af43\loch\f37 we}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  will notify you. Unless you reject the change within thirty (30) d

+\hich\af37\dbch\af43\loch\f37 ays of such change become effective by writing to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 

+\hich\af37\dbch\af43\loch\f37  at }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 \hich\af37\dbch\af43\loch\f37 2590 Welton Street Ste. 200 PMB 1916}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602 

+\hich\af37\dbch\af43\loch\f37 , }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 \hich\af37\dbch\af43\loch\f37 Denver, CO 80205}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 

+\hich\af37\dbch\af43\loch\f37 , your continued use of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 

+\hich\af37\dbch\af43\loch\f37 , including the acceptance of products and services offered on the S}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid2650620 \hich\af37\dbch\af43\loch\f37 ervice}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  following the posting of changes to\hich\af37\dbch\af43\loch\f37  this Arbitration Agreement constitutes your acceptance of any such changes.}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868\delrsid2113717\charrsid9926853 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+Changes to this Arbitration Agreement do not provide you with a new opportunity to opt out of the Arbitration Agreement if you have previously agreed to a version of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 \hich\af37\dbch\af43\loch\f37 

+these Terms}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  o\hich\af37\dbch\af43\loch\f37 f Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+ and did not validly opt out of arbitration.}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+If you reject any change or update to this Arbitration Agreement, and you were bound by an existing agreement to arbitrate Disputes arising out of or relating in any way to your access to or use of the }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37 Serv\hich\af37\dbch\af43\loch\f37 ice}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 , any }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid14295472 \hich\af37\dbch\af43\loch\f37 c}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 ommunications you receive, any products sold or distributed through the }{\rtlch\fcs1 \af37 

+\ltrch\fcs0 \dbch\af43\insrsid8390183 \hich\af37\dbch\af43\loch\f37 Service}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  or }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 

+\hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+, the provisions of th}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 is}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Arbitration Agreement }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 as of the date you first accepted the }{\rtlch\fcs1 \ab\af37 

+\ltrch\fcs0 \dbch\af43\insrsid12729868 \hich\af37\dbch\af43\loch\f37 Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37  }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 

+\hich\af37\dbch\af43\loch\f37 of Use }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 (or accepted any subsequent changes to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 

+\hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 \ab\af37 \ltrch\fcs0 \dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+) remain in full force and effect. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid5510358 \hich\af37\dbch\af43\loch\f37 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 \hich\af37\dbch\af43\loch\f37 

+ will continue to honor any valid opt outs of the Arbitration Agreement that you made to a prior version of }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid16458690 \hich\af37\dbch\af43\loch\f37 these Terms}{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\dbch\af43\insrsid10505110 \hich\af37\dbch\af43\loch\f37  of Use}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid9926853 .}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid12729868\charrsid12729868 

+\par {\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid3107918\charrsid7358637 \hich\af37\dbch\af0\loch\f37 15.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid3107918 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid3107918\charrsid7358637 THIRD-PARTY }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8390183 SERVICE}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid3107918 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid3107918\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid3107918\charrsid984291 \hich\af37\dbch\af0\loch\f37 15.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\keepn\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid3107918 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid3107918\charrsid984291 Third-Party Websites, Applications and Ads}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \dbch\af31506\insrsid13058527 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629  }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid1322206 The }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 

+may contain links to third-party websites (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid3107918\charrsid8085629 \'93Third-Party Websites\'94)}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid3107918 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid3107918\charrsid3107918  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 applications (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid3107918\charrsid8085629 \'93Third-Party Applications\'94}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid3107918\charrsid8085629 ) and advertisements for third parties (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid3107918\charrsid8085629 \'93Third-Party Ads\'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 )}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6828972  (collectively, the \'93}{\rtlch\fcs1 \ab\af37\afs22 \ltrch\fcs0 \b\insrsid6828972\charrsid6828972 Third-Party Services}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid6828972 \'94)}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 

+When you click on a link to a Third-Party Website, Third-Party Application or Third-Party Ad, we will not warn you that you have left }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629  and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2104621 you become}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2104621\charrsid8085629  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid3107918\charrsid8085629 subject to the terms and conditions (including privacy policies) of another website or destination.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid3107918\charrsid8085629 Such Third-Party Websites, Third-Party Applications and Third-Party Ads are not under the control of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid3107918\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 

+ is not responsible for any Third-Party Websites, Third-Party Applications or Third-Party Ads.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid3107918\charrsid8085629  provides these Third-Party Websites, Third-Party Applications and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid8085629 Third-Party}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 

+ Ads only as a convenience and does not review, approve, monitor, endorse, warrant, or make any representations with respect to Third-Party Websites, Third-Party Applications or Third-Party Ads, or any product or service provided in connection ther}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918 e}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 with.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 

+You use all links in Third-Party Websites, Third-Party Applications and Third-Party Ads at your own risk.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 

+When you leave our Website, }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918 this}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629  Agreement and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918 our }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid3107918\charrsid8085629 policies no longer govern.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629 You should review applicabl

+e terms and policies, including privacy and data gathering practices, of any Third-Party Websites}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629  Third-Party Applications,}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918  or Third-Party Ads,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3107918\charrsid8085629  and make whatever investigation you feel necessary or appropriate before proceeding with any transaction with a

+ny third party. 

+\par {\*\bkmkstart _Ref107825869}{\listtext\pard\plain\ltrpar \s48 \rtlch\fcs1 \af37\afs20 \ltrch\fcs0 \b\f37\fs22\insrsid6828972 \hich\af37\dbch\af0\loch\f37 (a)\tab}}\pard\plain \ltrpar

+\s48\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl2\outlinelevel2\adjustright\rin0\lin0\itap0\pararsid6828972 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37 \ltrch\fcs0 \insrsid6828972 Sharing Your Content and Information Through Third-Party Services. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972 

+ may provide tools through the Service that enable you to export information, including Your Content, to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13701201 or from }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972 

+Third-Party Services, including through features that allow you to }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13701201 authenticate or }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972 link your Account with }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13701201 

+or through }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972 an SNS account, or through our implementation of third-party buttons }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13701201 or other functionality =}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972 

+. By using one of these tools, you agree that }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972  may transfer that information to the applicable Third-Party Service. }{\rtlch\fcs1 \af37 \ltrch\fcs0 

+\insrsid5510358 Zed}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972  is not responsible for any Third-Party Service\rquote s use of your exported information. }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid6828972\charrsid6828972 

+\par {\*\bkmkend _Ref107825869}{\listtext\pard\plain\ltrpar \s44 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\caps\f37\fs22\insrsid984291\charrsid7358637 \hich\af37\dbch\af0\loch\f37 16.\tab}}\pard\plain \ltrpar

+\s44\qj \li0\ri0\sa240\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ls1\outlinelevel0\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid984291\charrsid7358637 GENERAL PROVISIONS}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .

+\par {\*\bkmkstart _Ref107569111}{\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.1\tab}}\pard\plain \ltrpar\s46\qj \fi720\li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls3\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 \f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid984291 Electronic Communications}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid8085629 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid8085629 The communications between you and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  may take place via electronic means, whether you visit }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  or send }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  e-mails, or whether }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  posts notices on }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 

+or communicates with you via e-mail.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 For contractual purposes, you (a) consent to receive communications from }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  in an electronic form; and (b) agree that all terms and conditions, agreements, notices, disclosures, and other communications that }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629  provides to you electronically satisfy any legal requirement that such communications would satisfy if it were to be in writing.}

+{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 The foregoing does not affect your statutory rights}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2971893 

+, including but not limited to the Electronic Signatures in Global and National Commerce Act at 15 U.S.C. \'a77001 et seq. (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid2971893\charrsid15882623 \'93E-Sign}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid15882623\charrsid15882623 \'94}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2971893 )}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8085629 {\*\bkmkend _Ref107569111}

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.2\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Assignment}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 The Agreement, and your rights and obligations hereunder, may not be assig

+ned, subcontracted, delegated or otherwise transferred by you without }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote 

+s prior written consent, and any attempted assignment, subcontract, delegation, or transfer in violation of the foregoing will be null and void.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.3\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Force Majeure}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ shall not be liable for any delay or failure to perform resulting from causes outside its reasonable control, including, but not limited to, acts of God, war, terrorism, riots, embargos, acts of civil or military authorities, fire, floods, accidents, str

+ikes or shortages of transportation facilities, fuel, energy, labor or materials.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8607742 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.4\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid8986704 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Questions, Complaints, Claims}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 If you have any questions, complaints or claims with respect to }{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , please contact us at: }{\field\fldedit{\*\fldinst {\rtlch\fcs1 \af37 

+\ltrch\fcs0 \insrsid9586278 HYPERLINK "mailto:hi@zed.dev"}{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid9586278 {\*\datafield }}}{\fldrslt {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \cs19\ul\cf23\insrsid2637059\charrsid6248995 hi@zed.dev}}}\sectd \ltrsect

+\linex0\endnhere\titlepg\sectunlocked1\sectlinegrid360\sectdefaultcl\sectrsid3040103\sftnbj {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015\charrsid2637059 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2637059   }{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637 We will do our best to address your concerns.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+If you feel that your concerns have been addressed incompletely, we invite you to let us know for further investigation.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid8986704 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.5\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Exclusive Venue}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 To the extent the parties are permit

+ted under this Agreement to initiate litigation in a court, both you and }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ agree that all claims and disputes arising out of or relating to the Agreement will be litigated exclusively in the state or federal courts located in }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2169198 Denver}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid10172954 , Colorado}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\i\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.6\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Governing Law}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 

+The Terms and any action related thereto will be governed and interpreted by and under the laws of the State of }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid10172954 COLORADO}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\caps\insrsid16517015\charrsid8607742 ,}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8085629\charrsid7358637 

+ consistent with the Federal Arbitration Act, without giving effect to any principles that provide for the application of the law of another jurisdiction.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\caps\insrsid8085629\charrsid7358637 The United Nations Convention on Contracts for the International Sale of Goods does not apply to the AGREEMENT}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \caps\insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid15231950 \hich\af37\dbch\af0\loch\f37 16.7\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid15231950 Choice of Language.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid15231950 It is the express wish of the parties that the Agreement and all related documents have been drawn up in English}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid10172954 .}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid15231950\charrsid10172954  

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.8\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Notice}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 Where }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  requires that you provide an e-mail address, you are responsible for providing }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  with your most current e-mail address.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 In the event that the last e-mail address you provided to }

+{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  is not valid, or for any reason is not capable of delivering to you any notices required/ permitted by the Agreement, }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 \rquote s dispatch of the e-mail containing such notice will nonetheless constitute effective notice.}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You may give notice to }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ at the following address: }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 \hich\af37\dbch\af43\loch\f37 2590 Welton Street Ste. 200 PMB 1916}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602 \hich\af37\dbch\af43\loch\f37 

+, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602\charrsid9924602 \hich\af37\dbch\af43\loch\f37 Denver, CO 80205}{\rtlch\fcs1 \af37 \ltrch\fcs0 \dbch\af43\insrsid9924602 \hich\af37\dbch\af43\loch\f37 . S}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 uch notice shall be deemed given when received by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  by letter delivered by nationally recogn

+ized overnight delivery service or }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid3021615\charrsid7358637 first-class}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  postage prepaid mail at the above address.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.9\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Waiver}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+Any waiver or failure to enforce any provision of the Agreement on one occasion will not be deemed a waiver of any other provision or of such provision on any other occasion.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.10\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Severability}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+If any portion of this Agreement is held invalid or unenforceable, that portion shall be construed in a manner to reflect, as nearly as possible, the original intention of the parties, and the remaining portions shall remain in full force and effect.

+

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.11\tab}}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Export Control}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid13058527\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You may not use, export, import, or transfer }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 except as authorized by U.S. law, the laws of the jurisdiction in which you obtained}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid2896802  the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , and any other applicable laws.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 In particular, but without limitation, }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 may not be exported or re-exported (a) into any United States embargoed countries, or (b) to anyone on the U.S. Treasury Department\rquote s list of Specially Designated Nationals or the U.S. Department of Commerce\rquote 

+s Denied Person\rquote s List or Entity List. By using }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+, you represent and warrant that (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid266646 i}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+) you are not located in a country that is subject to a U.S. Government embargo, or that has been designated by the U.S. Government as a \'93terrorist supporting\'94 country and (}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid266646 ii}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 ) you are not listed on any U.S. Government list of prohibited or restricted parties}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637  You also will not use }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206 the }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid1322206\charrsid7358637  }{

+\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 for any purpose prohibited by U.S. law, including the development, design, manufacture or production of missiles, nuclear, chemical or biological weapons.}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 You acknowledge and agree that products, services or technology provided by }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 

+\af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637  are subject to the export control laws and regulations of the United States.}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 You shall comply with these laws and regulations and shall not, without prior U.S. government authorization, export, re-export, or transfer }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid5510358 Zed}{\rtlch\fcs1 \af37\afs22 

+\ltrch\fcs0 \insrsid8085629\charrsid7358637  products, services or technology, either directly or indirectly, to any country in violation of such laws and regulations.

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.12\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\keep\keepn\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Consumer Complaints}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 In accordance with California Civil Code \'a7

+1789.3, you may report complaints to the Complaint Assistance Unit of the Division of Consumer }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183 Service}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+ of the California Department of Consumer Affairs by contacting them in writing at }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid10249255 1625 North Market Blvd., Suite N 112, }{\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid10249255\charrsid7804399 Sacramento, CA 958}{

+\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid10249255 34}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 , or by telephone at (800) 952-5210}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid16517015 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\insrsid8085629\charrsid7358637 

+\par {\listtext\pard\plain\ltrpar \s46 \rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\f37\fs22\insrsid8085629\charrsid984291 \hich\af37\dbch\af0\loch\f37 16.13\tab}}\pard \ltrpar\s46\qj \li0\ri0\sa240\widctlpar

+\jclisttab\tx1440\wrapdefault\aspalpha\aspnum\faauto\ls1\ilvl1\outlinelevel1\adjustright\rin0\lin0\itap0\pararsid4414305 {\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \b\insrsid8085629\charrsid984291 Entire Agreement}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 

+\b\insrsid8085629\charrsid7358637 .}{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8390183  }{\rtlch\fcs1 \af37\afs22 \ltrch\fcs0 \insrsid8085629\charrsid7358637 

+The Agreement is the final, complete and exclusive agreement of the parties with respect to the subject matter hereof and supersedes and merges all prior discussions between the parties with respect to such subject matter.

+\par }\pard\plain \ltrpar\s28\qj \fi720\li0\ri0\sa240\sl480\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid13058527 \rtlch\fcs1 \af37\afs20\alang1025 \ltrch\fcs0 

+\f37\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af37 \ltrch\fcs0 \insrsid13058527\charrsid13058527 {\*\bkmkend _Hlk108160434}

+\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a

+9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad

+5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6

+b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0

+0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6

+a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f

+c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512

+0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462

+a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865

+6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b

+4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b

+4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100693cbfc5630600003a1b0000160000007468656d652f7468656d652f

+7468656d65312e786d6cec59cf6edb3618bf0fd83b10bab7b11ddb8d833a45ecd8edd6a60d62b7438fb4445bac295120e9a4beb64f306cf71d06ec32741bb00e

+3b34dbcb641bd6edd057d8475292c95841d336d80e8b0f8945fdbeff7ff891be79eb69c2d0111192f2b41bd4afd70244d29047349d758387e3e1b5ad004985d3

+08339e926eb02432b8b5f3f14737f1b68a494210d0a7721b778358a96c7b634386b08ce5759e9114de4db948b0824731db88043e06be09db68d46aed8d04d334

+40294e80ed83e99486048d35cb60a7603e60f0982aa9174226469a35f1280c369ad735422e659f09748459370039113f1e93a72a400c4b052fba41cd7c828d9d

+9b1b783b2762ea1c5a876e683e395d4e10cd1b46a6984d4aa1f561b37363afe46f004cade30683417f502ff919000e43b0d4eae2f26c0eb7eabd82a703b25fd7

+79f76bad5ad3c73bfc37d774eef47abd5627d7c5323520fbb5b986dfaab59bbb0d0f6f4016df5ac3377bbbfd7edbc31b90c5b7d7f0c31b9d76d3c71b50cc683a

+5f43eb800e8739f71232e5ec4e257c0be05bb51cbe42413694d9a5454c79aa2a720df52193240d5123d0a8043fe1620850fdc0b0a22952cb8c4c7108f9bc2b28

+665a10de26d859b74ba15c5bd232910c05cd5437f834c350192b6e6f4e5ebc39f911bd39f9e1f4d9cbd3673f9f3e7f7efaec7bcbcb23bc83d3994bf8d74f2fff

+fee6a41a285de0eb579ffff6cb17d54028a1952abffffadd9fafbefae3c5d740f1fadb2f2b2876059eb8148e2b7c5dc9445c04378e3175717d2e228ad17d725c

+217ca0620f7c7f8919aec0f588efa847021a4615f0f6e289a7e528160b452b38de8d130fb88717e90149e32aa816e5f874bc4867d5b2c5c2c51d627c5425ba8f

+532f988345068d9256b1ecc7c4d3f280e154e119498942fa1d9f1352a1f1634a3db7eed35070c9a70a3da6a88769a547c674e2a5ce8ae80e4d202ccb2a0521da

+9e6ff61fa11e675556ef91231f09b95fd49c976863c23c37dec60b85932a96639c30d7e1f7b08aab941c2d45e8e2065209904e18478388485945f34080bd4ed0

+ef6268519561df67cbc4470a45e7553cef61ce5de41e9ff7639c6455d811855c74e47f22e79c338c0eb8aa82ef73bf40f433c401a7e786fb11255eb8cfabfb87

+74e629b24a0bfd662174fa4147f6da6b42d3ab5e5b519757bdd6ecb657bdf6aad7baf3d355afcd07c98bf6da557b85ceab374f3bfe9a6138a99a85cd043ca58c

+8dd492917bd20cc112368868088b9ace9c03497936ca62f89ab7760f3713d8d020c1d56754c5a318673040d78d8499cc59cf24cab884239c59aee4ad85c210ae

+ec01b0a58f06b6634aacf679649737f572710228d9980d67668e9985a04dcde0a2c2366fe44cc1ecf71156d74a5d585adda866ce169eb4d26488e1ba69b0587a

+13260f04f30a78b90d27712d1a8e1e989148fbdd6ebf4558b4578bef97122219e388e431d276afc7a86e8254e48a39f343ee54c4481fe7dee235475a47b3fd00

+691709922bae798eb8227a1f12a522838bc818e79c2d4796bac5c95274dc0d3aad462b4021cebac114ceacf035c920ea520f7b98cde00a2854c2a6fd5b8bd954

+f92a9a9dc230bf08ea702161fdbe66b0d7073221d51e96b14d0df32a4f01966a4956ff460bdc7a5906d84c7f0f2d36b72019fe332dc08f7e68c9744a42e506db

+59d1beb38f792be50b45c4288e8ed1842dc42186f0eb54057b222ae1f2c17404fd003766dadbe695df9cf3a273efa90cceae6396c5386fb7ba448b4ab67093aa

+a50ee6c9510f6cabd4dd18f7eea69892bf2453dc34fe9f99a2f713b822d88c740442b8b01518e97aed065ca8984317ca621a0e050c0ea67740b6c0ad2bbc86a4

+826b63f35f9023fddfd69ce561ca1ace7cea90ce90a0b01fa9581072006dc964df5b98d5f3bdcbb264392393518eba32b36a4fc8116163dd03db7a6f0f500ca9

+6eba49de060cee6cfef9cf79054d667ac871ebcdeb21e5de6b6be0df9e7c6c3183517e1f36034de1ff5245e32d7ff2b1f486bcd87b5d43f48bd598d52caa0284

+395b41272ffbf754e11db75adbb1d62c6eb40ae5208aeb16c36239106570d183f41fd8ffa80899fd0d426fa8637e08bd15c14f0a9a19a40d64f5353b7820dd20

+ede2040627bb689349b3b2aecda75bedb562b3be94316a158252ee19676bcd2e12ef777476399cf9e2bc5abc4c67e71ef67c6dd7ce753544f66c89c2d2b438c8

+98c0981fafdcdf97f8e409047a0feeef174c49934cf0eb91c0307a8e4c1d40f15b898674e71f000000ffff0300504b0304140006000800000021000dd1909fb6

+0000001b010000270000007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6f

+d3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060

+828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509aff

+b3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff000000

+1c0200001300000000000000000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c00000

+00360100000b00000000000000000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00

+000000000000000000000000190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100693cbfc5

+630600003a1b00001600000000000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d0014000600080000002100

+0dd1909fb60000001b01000027000000000000000000000000006d0900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000680a00000000}

+{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d

+617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169

+6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363

+656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}

+{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;

+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;

+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;

+\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;

+\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;

+\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdpriority0 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdpriority0 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdpriority0 \lsdlocked0 Body Text;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;

+\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority59 \lsdlocked0 Table Grid;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;

+\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;

+\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;

+\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;

+\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;

+\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;

+\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;

+\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;

+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;

+\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;

+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;

+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;

+\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;

+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;

+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;

+\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5;

+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5;

+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;

+\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6;

+\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;

+\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;

+\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis;

+\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography;

+\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4;

+\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4;

+\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1;

+\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1;

+\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2;

+\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2;

+\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3;

+\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4;

+\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4;

+\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5;

+\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5;

+\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6;

+\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6;

+\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark;

+\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1;

+\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1;

+\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2;

+\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3;

+\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3;

+\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4;

+\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4;

+\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5;

+\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;

+\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;

+\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;

+\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore }}

styles/package-lock.json 🔗

@@ -9,67 +9,83 @@
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {
-                "@types/chroma-js": "^2.1.3",
-                "@types/node": "^17.0.23",
+                "@types/chroma-js": "^2.4.0",
+                "@types/node": "^18.14.1",
+                "bezier-easing": "^2.1.0",
                 "case-anything": "^2.1.10",
                 "chroma-js": "^2.4.2",
+                "deepmerge": "^4.3.0",
                 "toml": "^3.0.0",
-                "ts-node": "^10.7.0"
-            }
-        },
-        "node_modules/@cspotcode/source-map-consumer": {
-            "version": "0.8.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
-            "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
-            "engines": {
-                "node": ">= 12"
+                "ts-node": "^10.9.1"
             }
         },
         "node_modules/@cspotcode/source-map-support": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
-            "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
             "dependencies": {
-                "@cspotcode/source-map-consumer": "0.8.0"
+                "@jridgewell/trace-mapping": "0.3.9"
             },
             "engines": {
                 "node": ">=12"
             }
         },
+        "node_modules/@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
+        },
+        "node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+            "dependencies": {
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
+            }
+        },
         "node_modules/@tsconfig/node10": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
-            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
         },
         "node_modules/@tsconfig/node12": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
-            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
         },
         "node_modules/@tsconfig/node14": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
-            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
         },
         "node_modules/@tsconfig/node16": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
-            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
         },
         "node_modules/@types/chroma-js": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
-            "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
+            "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
         },
         "node_modules/@types/node": {
-            "version": "17.0.23",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
-            "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+            "version": "18.14.1",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
+            "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
         },
         "node_modules/acorn": {
-            "version": "8.7.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
-            "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
+            "version": "8.8.2",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
             "bin": {
                 "acorn": "bin/acorn"
             },
@@ -90,6 +106,11 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "node_modules/bezier-easing": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        },
         "node_modules/case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -111,6 +132,14 @@
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
+        "node_modules/deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
         "node_modules/diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -130,11 +159,11 @@
             "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
         },
         "node_modules/ts-node": {
-            "version": "10.7.0",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
-            "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
             "dependencies": {
-                "@cspotcode/source-map-support": "0.7.0",
+                "@cspotcode/source-map-support": "^0.8.0",
                 "@tsconfig/node10": "^1.0.7",
                 "@tsconfig/node12": "^1.0.7",
                 "@tsconfig/node14": "^1.0.0",
@@ -145,7 +174,7 @@
                 "create-require": "^1.1.0",
                 "diff": "^4.0.1",
                 "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.0",
+                "v8-compile-cache-lib": "^3.0.1",
                 "yn": "3.1.1"
             },
             "bin": {
@@ -172,9 +201,9 @@
             }
         },
         "node_modules/typescript": {
-            "version": "4.6.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "version": "4.9.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
             "peer": true,
             "bin": {
                 "tsc": "bin/tsc",
@@ -185,9 +214,9 @@
             }
         },
         "node_modules/v8-compile-cache-lib": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
-            "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
         },
         "node_modules/yn": {
             "version": "3.1.1",
@@ -199,53 +228,67 @@
         }
     },
     "dependencies": {
-        "@cspotcode/source-map-consumer": {
-            "version": "0.8.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
-            "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
-        },
         "@cspotcode/source-map-support": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
-            "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+            "version": "0.8.1",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+            "requires": {
+                "@jridgewell/trace-mapping": "0.3.9"
+            }
+        },
+        "@jridgewell/resolve-uri": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
+        },
+        "@jridgewell/sourcemap-codec": {
+            "version": "1.4.14",
+            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
+        },
+        "@jridgewell/trace-mapping": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
             "requires": {
-                "@cspotcode/source-map-consumer": "0.8.0"
+                "@jridgewell/resolve-uri": "^3.0.3",
+                "@jridgewell/sourcemap-codec": "^1.4.10"
             }
         },
         "@tsconfig/node10": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
-            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
         },
         "@tsconfig/node12": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
-            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
         },
         "@tsconfig/node14": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
-            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
         },
         "@tsconfig/node16": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
-            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
+            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
         },
         "@types/chroma-js": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
-            "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+            "version": "2.4.0",
+            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
+            "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
         },
         "@types/node": {
-            "version": "17.0.23",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
-            "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+            "version": "18.14.1",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
+            "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
         },
         "acorn": {
-            "version": "8.7.0",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
-            "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
+            "version": "8.8.2",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="
         },
         "acorn-walk": {
             "version": "8.2.0",
@@ -257,6 +300,11 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "bezier-easing": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        },
         "case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -272,6 +320,11 @@
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
+        "deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="
+        },
         "diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -288,11 +341,11 @@
             "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
         },
         "ts-node": {
-            "version": "10.7.0",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
-            "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
             "requires": {
-                "@cspotcode/source-map-support": "0.7.0",
+                "@cspotcode/source-map-support": "^0.8.0",
                 "@tsconfig/node10": "^1.0.7",
                 "@tsconfig/node12": "^1.0.7",
                 "@tsconfig/node14": "^1.0.0",
@@ -303,20 +356,20 @@
                 "create-require": "^1.1.0",
                 "diff": "^4.0.1",
                 "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.0",
+                "v8-compile-cache-lib": "^3.0.1",
                 "yn": "3.1.1"
             }
         },
         "typescript": {
-            "version": "4.6.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "version": "4.9.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
             "peer": true
         },
         "v8-compile-cache-lib": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
-            "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
         },
         "yn": {
             "version": "3.1.1",

styles/package.json 🔗

@@ -10,11 +10,19 @@
     "author": "",
     "license": "ISC",
     "dependencies": {
-        "@types/chroma-js": "^2.1.3",
-        "@types/node": "^17.0.23",
+        "@types/chroma-js": "^2.4.0",
+        "@types/node": "^18.14.1",
+        "bezier-easing": "^2.1.0",
         "case-anything": "^2.1.10",
         "chroma-js": "^2.4.2",
+        "deepmerge": "^4.3.0",
         "toml": "^3.0.0",
-        "ts-node": "^10.7.0"
+        "ts-node": "^10.9.1"
+    },
+    "prettier": {
+        "semi": false,
+        "printWidth": 80,
+        "htmlWhitespaceSensitivity": "strict",
+        "tabWidth": 4
     }
 }

styles/src/buildLicenses.ts 🔗

@@ -1,73 +1,92 @@
-import * as fs from "fs";
-import toml from "toml";
-import {
-  schemeMeta
-} from "./colorSchemes";
-import { Meta } from "./themes/common/colorScheme";
-import https from "https";
-import crypto from "crypto";
+import * as fs from "fs"
+import toml from "toml"
+import { schemeMeta } from "./colorSchemes"
+import { Meta, Verification } from "./themes/common/colorScheme"
+import https from "https"
+import crypto from "crypto"
 
 const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml`
 
 // Use the cargo-about configuration file as the source of truth for supported licenses.
 function parseAcceptedToml(file: string): string[] {
-  let buffer = fs.readFileSync(file).toString();
+    let buffer = fs.readFileSync(file).toString()
 
-  let obj = toml.parse(buffer);
+    let obj = toml.parse(buffer)
 
-  if (!Array.isArray(obj.accepted)) {
-    throw Error("Accepted license source is malformed")
-  }
+    if (!Array.isArray(obj.accepted)) {
+        throw Error("Accepted license source is malformed")
+    }
 
-  return obj.accepted
+    return obj.accepted
 }
 
-
 function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
-  for (let meta of schemeMeta) {
-    // FIXME: Add support for conjuctions and conditions
-    if (licenses.indexOf(meta.license.SPDX) < 0) {
-      throw Error(`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`)
+    for (let meta of schemeMeta) {
+        // FIXME: Add support for conjuctions and conditions
+        if (licenses.indexOf(meta.license.SPDX) < 0) {
+            throw Error(
+                `License for theme ${meta.name} (${meta.license.SPDX}) is not supported`
+            )
+        }
     }
-  }
 }
 
+function getLicenseText(
+    schemeMeta: Meta[],
+    callback: (meta: Meta, license_text: string) => void
+) {
+    for (let meta of schemeMeta) {
+        if (typeof meta.license.license_text == "string") {
+            callback(meta, meta.license.license_text)
+        } else {
+            let license_text_obj: Verification = meta.license.license_text
+            // The following copied from the example code on nodejs.org:
+            // https://nodejs.org/api/http.html#httpgetoptions-callback
+            https
+                .get(license_text_obj.https_url, (res) => {
+                    const { statusCode } = res
 
-function getLicenseText(schemeMeta: Meta[], callback: (meta: Meta, license_text: string) => void) {
-  for (let meta of schemeMeta) {
-    // The following copied from the example code on nodejs.org: 
-    // https://nodejs.org/api/http.html#httpgetoptions-callback
-    https.get(meta.license.https_url, (res) => {
-      const { statusCode } = res;
-
-      if (statusCode < 200 || statusCode >= 300) {
-        throw new Error(`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`);
-      }
+                    if (statusCode < 200 || statusCode >= 300) {
+                        throw new Error(
+                            `Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`
+                        )
+                    }
 
-      res.setEncoding('utf8');
-      let rawData = '';
-      res.on('data', (chunk) => { rawData += chunk; });
-      res.on('end', () => {
-        const hash = crypto.createHash('sha256').update(rawData).digest('hex');
-        if (meta.license.license_checksum == hash) {
-          callback(meta, rawData)
-        } else {
-          throw Error(`Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`)
+                    res.setEncoding("utf8")
+                    let rawData = ""
+                    res.on("data", (chunk) => {
+                        rawData += chunk
+                    })
+                    res.on("end", () => {
+                        const hash = crypto
+                            .createHash("sha256")
+                            .update(rawData)
+                            .digest("hex")
+                        if (license_text_obj.license_checksum == hash) {
+                            callback(meta, rawData)
+                        } else {
+                            throw Error(
+                                `Checksum for ${meta.name} did not match file downloaded from ${license_text_obj.https_url}`
+                            )
+                        }
+                    })
+                })
+                .on("error", (e) => {
+                    throw e
+                })
         }
-      });
-    }).on('error', (e) => {
-      throw e
-    });
-  }
+    }
 }
 
 function writeLicense(schemeMeta: Meta, text: String) {
-  process.stdout.write(`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`)
+    process.stdout.write(
+        `## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`
+    )
 }
 
-const accepted_licenses = parseAcceptedToml(accepted_licenses_file);
+const accepted_licenses = parseAcceptedToml(accepted_licenses_file)
 checkLicenses(schemeMeta, accepted_licenses)
 
 getLicenseText(schemeMeta, (meta, text) => {
-  writeLicense(meta, text)
-});
+    writeLicense(meta, text)
+})

styles/src/buildThemes.ts 🔗

@@ -1,50 +1,52 @@
-import * as fs from "fs";
-import { tmpdir } from "os";
-import * as path from "path";
-import colorSchemes, {
-  staffColorSchemes,
-} from "./colorSchemes";
-import app from "./styleTree/app";
-import { ColorScheme } from "./themes/common/colorScheme";
-import snakeCase from "./utils/snakeCase";
+import * as fs from "fs"
+import { tmpdir } from "os"
+import * as path from "path"
+import colorSchemes, { staffColorSchemes } from "./colorSchemes"
+import app from "./styleTree/app"
+import { ColorScheme } from "./themes/common/colorScheme"
+import snakeCase from "./utils/snakeCase"
 
 const assetsDirectory = `${__dirname}/../../assets`
-const themeDirectory = `${assetsDirectory}/themes`;
-const staffDirectory = `${themeDirectory}/staff`;
+const themeDirectory = `${assetsDirectory}/themes`
+const staffDirectory = `${themeDirectory}/staff`
 
-const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"));
+const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
 
 // Clear existing themes
 function clearThemes(themeDirectory: string) {
-  if (!fs.existsSync(themeDirectory)) {
-    fs.mkdirSync(themeDirectory, { recursive: true });
-  } else {
-    for (const file of fs.readdirSync(themeDirectory)) {
-      if (file.endsWith(".json")) {
-        const name = file.replace(/\.json$/, "");
-        if (!colorSchemes.find((colorScheme) => colorScheme.name === name)) {
-          fs.unlinkSync(path.join(themeDirectory, file));
+    if (!fs.existsSync(themeDirectory)) {
+        fs.mkdirSync(themeDirectory, { recursive: true })
+    } else {
+        for (const file of fs.readdirSync(themeDirectory)) {
+            if (file.endsWith(".json")) {
+                const name = file.replace(/\.json$/, "")
+                if (
+                    !colorSchemes.find(
+                        (colorScheme) => colorScheme.name === name
+                    )
+                ) {
+                    fs.unlinkSync(path.join(themeDirectory, file))
+                }
+            }
         }
-      }
     }
-  }
 }
 
-clearThemes(themeDirectory);
-clearThemes(staffDirectory);
+clearThemes(themeDirectory)
+clearThemes(staffDirectory)
 
 function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
-  for (let colorScheme of colorSchemes) {
-    let styleTree = snakeCase(app(colorScheme));
-    let styleTreeJSON = JSON.stringify(styleTree, null, 2);
-    let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`);
-    let outPath = path.join(outputDirectory, `${colorScheme.name}.json`);
-    fs.writeFileSync(tempPath, styleTreeJSON);
-    fs.renameSync(tempPath, outPath);
-    console.log(`- ${outPath} created`);
-  }
+    for (let colorScheme of colorSchemes) {
+        let styleTree = snakeCase(app(colorScheme))
+        let styleTreeJSON = JSON.stringify(styleTree, null, 2)
+        let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`)
+        let outPath = path.join(outputDirectory, `${colorScheme.name}.json`)
+        fs.writeFileSync(tempPath, styleTreeJSON)
+        fs.renameSync(tempPath, outPath)
+        console.log(`- ${outPath} created`)
+    }
 }
 
 // Write new themes to theme directory
-writeThemes(colorSchemes, themeDirectory);
-writeThemes(staffColorSchemes, staffDirectory);
+writeThemes(colorSchemes, themeDirectory)
+writeThemes(staffColorSchemes, staffDirectory)

styles/src/colorSchemes.ts 🔗

@@ -1,54 +1,54 @@
-import fs from "fs";
-import path from "path";
-import { ColorScheme, Meta } from "./themes/common/colorScheme";
+import fs from "fs"
+import path from "path"
+import { ColorScheme, Meta } from "./themes/common/colorScheme"
 
-const colorSchemes: ColorScheme[] = [];
-export default colorSchemes;
+const colorSchemes: ColorScheme[] = []
+export default colorSchemes
 
-const schemeMeta: Meta[] = [];
-export { schemeMeta };
+const schemeMeta: Meta[] = []
+export { schemeMeta }
 
-const staffColorSchemes: ColorScheme[] = [];
-export { staffColorSchemes };
+const staffColorSchemes: ColorScheme[] = []
+export { staffColorSchemes }
 
-const experimentalColorSchemes: ColorScheme[] = [];
-export { experimentalColorSchemes };
+const experimentalColorSchemes: ColorScheme[] = []
+export { experimentalColorSchemes }
 
-const themes_directory = path.resolve(`${__dirname}/themes`);
+const themes_directory = path.resolve(`${__dirname}/themes`)
 
-function for_all_color_schemes_in(themesPath: string, callback: (module: any, path: string) => void) {
-  for (const fileName of fs.readdirSync(themesPath)) {
-    if (fileName == "template.ts") continue;
-    const filePath = path.join(themesPath, fileName);
+function for_all_color_schemes_in(
+    themesPath: string,
+    callback: (module: any, path: string) => void
+) {
+    for (const fileName of fs.readdirSync(themesPath)) {
+        if (fileName == "template.ts") continue
+        const filePath = path.join(themesPath, fileName)
 
-    if (fs.statSync(filePath).isFile()) {
-      const colorScheme = require(filePath);
-      callback(colorScheme, path.basename(filePath));
+        if (fs.statSync(filePath).isFile()) {
+            const colorScheme = require(filePath)
+            callback(colorScheme, path.basename(filePath))
+        }
     }
-  }
 }
 
 function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
-  for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
-    if (colorScheme.dark) colorSchemes.push(colorScheme.dark);
-    if (colorScheme.light) colorSchemes.push(colorScheme.light);
-  })
+    for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
+        if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
+        if (colorScheme.light) colorSchemes.push(colorScheme.light)
+    })
 }
 
-fillColorSchemes(themes_directory, colorSchemes);
-fillColorSchemes(
-  path.resolve(`${themes_directory}/staff`),
-  staffColorSchemes
-);
+fillColorSchemes(themes_directory, colorSchemes)
+fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
 
 function fillMeta(themesPath: string, meta: Meta[]) {
-  for_all_color_schemes_in(themesPath, (colorScheme, path) => {
-    if (colorScheme.meta) {
-      meta.push(colorScheme.meta)
-    } else {
-      throw Error(`Public theme ${path} must have a meta field`)
-    }
-  })
+    for_all_color_schemes_in(themesPath, (colorScheme, path) => {
+        if (colorScheme.meta) {
+            meta.push(colorScheme.meta)
+        } else {
+            throw Error(`Public theme ${path} must have a meta field`)
+        }
+    })
 }
 
-fillMeta(themes_directory, schemeMeta);
+fillMeta(themes_directory, schemeMeta)

styles/src/common.ts 🔗

@@ -1,66 +1,45 @@
 export const fontFamilies = {
-  sans: "Zed Sans",
-  mono: "Zed Mono",
-};
+    sans: "Zed Sans",
+    mono: "Zed Mono",
+}
 
 export const fontSizes = {
-  "3xs": 8,
-  "2xs": 10,
-  xs: 12,
-  sm: 14,
-  md: 16,
-  lg: 18,
-  xl: 20,
-};
+    "3xs": 8,
+    "2xs": 10,
+    xs: 12,
+    sm: 14,
+    md: 16,
+    lg: 18,
+    xl: 20,
+}
 
 export type FontWeight =
-  | "thin"
-  | "extra_light"
-  | "light"
-  | "normal"
-  | "medium"
-  | "semibold"
-  | "bold"
-  | "extra_bold"
-  | "black";
+    | "thin"
+    | "extra_light"
+    | "light"
+    | "normal"
+    | "medium"
+    | "semibold"
+    | "bold"
+    | "extra_bold"
+    | "black"
 export const fontWeights: { [key: string]: FontWeight } = {
-  thin: "thin",
-  extra_light: "extra_light",
-  light: "light",
-  normal: "normal",
-  medium: "medium",
-  semibold: "semibold",
-  bold: "bold",
-  extra_bold: "extra_bold",
-  black: "black",
-};
+    thin: "thin",
+    extra_light: "extra_light",
+    light: "light",
+    normal: "normal",
+    medium: "medium",
+    semibold: "semibold",
+    bold: "bold",
+    extra_bold: "extra_bold",
+    black: "black",
+}
 
 export const sizes = {
-  px: 1,
-  xs: 2,
-  sm: 4,
-  md: 6,
-  lg: 8,
-  xl: 12,
-};
-
-// export const colors = {
-//   neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
-//   rose: colorRamp("#F43F5EFF"),
-//   red: colorRamp("#EF4444FF"),
-//   orange: colorRamp("#F97316FF"),
-//   amber: colorRamp("#F59E0BFF"),
-//   yellow: colorRamp("#EAB308FF"),
-//   lime: colorRamp("#84CC16FF"),
-//   green: colorRamp("#22C55EFF"),
-//   emerald: colorRamp("#10B981FF"),
-//   teal: colorRamp("#14B8A6FF"),
-//   cyan: colorRamp("#06BBD4FF"),
-//   sky: colorRamp("#0EA5E9FF"),
-//   blue: colorRamp("#3B82F6FF"),
-//   indigo: colorRamp("#6366F1FF"),
-//   violet: colorRamp("#8B5CF6FF"),
-//   purple: colorRamp("#A855F7FF"),
-//   fuschia: colorRamp("#D946E4FF"),
-//   pink: colorRamp("#EC4899FF"),
-// }
+    px: 1,
+    xs: 2,
+    sm: 4,
+    md: 6,
+    lg: 8,
+    xl: 12,
+}

styles/src/styleTree/app.ts 🔗

@@ -1,72 +1,70 @@
-import { text } from "./components";
-import contactFinder from "./contactFinder";
-import contactsPopover from "./contactsPopover";
-import commandPalette from "./commandPalette";
-import editor from "./editor";
-import projectPanel from "./projectPanel";
-import search from "./search";
-import picker from "./picker";
-import workspace from "./workspace";
-import contextMenu from "./contextMenu";
-import sharedScreen from "./sharedScreen";
-import projectDiagnostics from "./projectDiagnostics";
-import contactNotification from "./contactNotification";
-import updateNotification from "./updateNotification";
-import simpleMessageNotification from "./simpleMessageNotification";
-import projectSharedNotification from "./projectSharedNotification";
-import tooltip from "./tooltip";
-import terminal from "./terminal";
-import contactList from "./contactList";
-import incomingCallNotification from "./incomingCallNotification";
-import { ColorScheme } from "../themes/common/colorScheme";
-import feedback from "./feedback";
+import { text } from "./components"
+import contactFinder from "./contactFinder"
+import contactsPopover from "./contactsPopover"
+import commandPalette from "./commandPalette"
+import editor from "./editor"
+import projectPanel from "./projectPanel"
+import search from "./search"
+import picker from "./picker"
+import workspace from "./workspace"
+import contextMenu from "./contextMenu"
+import sharedScreen from "./sharedScreen"
+import projectDiagnostics from "./projectDiagnostics"
+import contactNotification from "./contactNotification"
+import updateNotification from "./updateNotification"
+import simpleMessageNotification from "./simpleMessageNotification"
+import projectSharedNotification from "./projectSharedNotification"
+import tooltip from "./tooltip"
+import terminal from "./terminal"
+import contactList from "./contactList"
+import incomingCallNotification from "./incomingCallNotification"
+import { ColorScheme } from "../themes/common/colorScheme"
+import feedback from "./feedback"
+import welcome from "./welcome"
+import copilot from "./copilot"
 
 export default function app(colorScheme: ColorScheme): Object {
-  return {
-    meta: {
-      name: colorScheme.name,
-      isLight: colorScheme.isLight,
-    },
-    commandPalette: commandPalette(colorScheme),
-    contactNotification: contactNotification(colorScheme),
-    projectSharedNotification: projectSharedNotification(colorScheme),
-    incomingCallNotification: incomingCallNotification(colorScheme),
-    picker: picker(colorScheme),
-    workspace: workspace(colorScheme),
-    contextMenu: contextMenu(colorScheme),
-    editor: editor(colorScheme),
-    projectDiagnostics: projectDiagnostics(colorScheme),
-    projectPanel: projectPanel(colorScheme),
-    contactsPopover: contactsPopover(colorScheme),
-    contactFinder: contactFinder(colorScheme),
-    contactList: contactList(colorScheme),
-    search: search(colorScheme),
-    sharedScreen: sharedScreen(colorScheme),
-    breadcrumbs: {
-      ...text(colorScheme.highest, "sans", "variant"),
-      padding: {
-        left: 6,
-      },
-    },
-    updateNotification: updateNotification(colorScheme),
-    simpleMessageNotification: simpleMessageNotification(colorScheme),
-    tooltip: tooltip(colorScheme),
-    terminal: terminal(colorScheme),
-    feedback: feedback(colorScheme),
-    colorScheme: {
-      ...colorScheme,
-      players: Object.values(colorScheme.players),
-      ramps: {
-        neutral: colorScheme.ramps.neutral.colors(100, "hex"),
-        red: colorScheme.ramps.red.colors(100, "hex"),
-        orange: colorScheme.ramps.orange.colors(100, "hex"),
-        yellow: colorScheme.ramps.yellow.colors(100, "hex"),
-        green: colorScheme.ramps.green.colors(100, "hex"),
-        cyan: colorScheme.ramps.cyan.colors(100, "hex"),
-        blue: colorScheme.ramps.blue.colors(100, "hex"),
-        violet: colorScheme.ramps.violet.colors(100, "hex"),
-        magenta: colorScheme.ramps.magenta.colors(100, "hex"),
-      },
-    },
-  };
+    return {
+        meta: {
+            name: colorScheme.name,
+            isLight: colorScheme.isLight,
+        },
+        commandPalette: commandPalette(colorScheme),
+        contactNotification: contactNotification(colorScheme),
+        projectSharedNotification: projectSharedNotification(colorScheme),
+        incomingCallNotification: incomingCallNotification(colorScheme),
+        picker: picker(colorScheme),
+        workspace: workspace(colorScheme),
+        copilot: copilot(colorScheme),
+        welcome: welcome(colorScheme),
+        contextMenu: contextMenu(colorScheme),
+        editor: editor(colorScheme),
+        projectDiagnostics: projectDiagnostics(colorScheme),
+        projectPanel: projectPanel(colorScheme),
+        contactsPopover: contactsPopover(colorScheme),
+        contactFinder: contactFinder(colorScheme),
+        contactList: contactList(colorScheme),
+        search: search(colorScheme),
+        sharedScreen: sharedScreen(colorScheme),
+        updateNotification: updateNotification(colorScheme),
+        simpleMessageNotification: simpleMessageNotification(colorScheme),
+        tooltip: tooltip(colorScheme),
+        terminal: terminal(colorScheme),
+        feedback: feedback(colorScheme),
+        colorScheme: {
+            ...colorScheme,
+            players: Object.values(colorScheme.players),
+            ramps: {
+                neutral: colorScheme.ramps.neutral.colors(100, "hex"),
+                red: colorScheme.ramps.red.colors(100, "hex"),
+                orange: colorScheme.ramps.orange.colors(100, "hex"),
+                yellow: colorScheme.ramps.yellow.colors(100, "hex"),
+                green: colorScheme.ramps.green.colors(100, "hex"),
+                cyan: colorScheme.ramps.cyan.colors(100, "hex"),
+                blue: colorScheme.ramps.blue.colors(100, "hex"),
+                violet: colorScheme.ramps.violet.colors(100, "hex"),
+                magenta: colorScheme.ramps.magenta.colors(100, "hex"),
+            },
+        },
+    }
 }

styles/src/styleTree/commandPalette.ts 🔗

@@ -1,30 +1,30 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { text, background } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { text, background } from "./components"
 
 export default function commandPalette(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
-  return {
-    keystrokeSpacing: 8,
-    key: {
-      text: text(layer, "mono", "variant", "default", { size: "xs" }),
-      cornerRadius: 2,
-      background: background(layer, "on"),
-      padding: {
-        top: 1,
-        bottom: 1,
-        left: 6,
-        right: 6,
-      },
-      margin: {
-        top: 1,
-        bottom: 1,
-        left: 2,
-      },
-      active: {
-        text: text(layer, "mono", "on", "default", { size: "xs" }),
-        background: withOpacity(background(layer, "on"), 0.2),
-      },
-    },
-  };
+    let layer = colorScheme.highest
+    return {
+        keystrokeSpacing: 8,
+        key: {
+            text: text(layer, "mono", "variant", "default", { size: "xs" }),
+            cornerRadius: 2,
+            background: background(layer, "on"),
+            padding: {
+                top: 1,
+                bottom: 1,
+                left: 6,
+                right: 6,
+            },
+            margin: {
+                top: 1,
+                bottom: 1,
+                left: 2,
+            },
+            active: {
+                text: text(layer, "mono", "on", "default", { size: "xs" }),
+                background: withOpacity(background(layer, "on"), 0.2),
+            },
+        },
+    }
 }

styles/src/styleTree/components.ts 🔗

@@ -1,210 +1,294 @@
-import { fontFamilies, fontSizes, FontWeight } from "../common";
-import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme";
+import { fontFamilies, fontSizes, FontWeight } from "../common"
+import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme"
 
 function isStyleSet(key: any): key is StyleSets {
-  return [
-    "base",
-    "variant",
-    "on",
-    "accent",
-    "positive",
-    "warning",
-    "negative",
-  ].includes(key);
+    return [
+        "base",
+        "variant",
+        "on",
+        "accent",
+        "positive",
+        "warning",
+        "negative",
+    ].includes(key)
 }
 
 function isStyle(key: any): key is Styles {
-  return [
-    "default",
-    "active",
-    "disabled",
-    "hovered",
-    "pressed",
-    "inverted",
-  ].includes(key);
+    return [
+        "default",
+        "active",
+        "disabled",
+        "hovered",
+        "pressed",
+        "inverted",
+    ].includes(key)
 }
 function getStyle(
-  layer: Layer,
-  possibleStyleSetOrStyle?: any,
-  possibleStyle?: any
+    layer: Layer,
+    possibleStyleSetOrStyle?: any,
+    possibleStyle?: any
 ): Style {
-  let styleSet: StyleSets = "base";
-  let style: Styles = "default";
-  if (isStyleSet(possibleStyleSetOrStyle)) {
-    styleSet = possibleStyleSetOrStyle;
-  } else if (isStyle(possibleStyleSetOrStyle)) {
-    style = possibleStyleSetOrStyle;
-  }
-
-  if (isStyle(possibleStyle)) {
-    style = possibleStyle;
-  }
-
-  return layer[styleSet][style];
+    let styleSet: StyleSets = "base"
+    let style: Styles = "default"
+    if (isStyleSet(possibleStyleSetOrStyle)) {
+        styleSet = possibleStyleSetOrStyle
+    } else if (isStyle(possibleStyleSetOrStyle)) {
+        style = possibleStyleSetOrStyle
+    }
+
+    if (isStyle(possibleStyle)) {
+        style = possibleStyle
+    }
+
+    return layer[styleSet][style]
 }
 
-export function background(layer: Layer, style?: Styles): string;
+export function background(layer: Layer, style?: Styles): string
 export function background(
-  layer: Layer,
-  styleSet?: StyleSets,
-  style?: Styles
-): string;
+    layer: Layer,
+    styleSet?: StyleSets,
+    style?: Styles
+): string
 export function background(
-  layer: Layer,
-  styleSetOrStyles?: StyleSets | Styles,
-  style?: Styles
+    layer: Layer,
+    styleSetOrStyles?: StyleSets | Styles,
+    style?: Styles
 ): string {
-  return getStyle(layer, styleSetOrStyles, style).background;
+    return getStyle(layer, styleSetOrStyles, style).background
 }
 
-export function borderColor(layer: Layer, style?: Styles): string;
+export function borderColor(layer: Layer, style?: Styles): string
 export function borderColor(
-  layer: Layer,
-  styleSet?: StyleSets,
-  style?: Styles
-): string;
+    layer: Layer,
+    styleSet?: StyleSets,
+    style?: Styles
+): string
 export function borderColor(
-  layer: Layer,
-  styleSetOrStyles?: StyleSets | Styles,
-  style?: Styles
+    layer: Layer,
+    styleSetOrStyles?: StyleSets | Styles,
+    style?: Styles
 ): string {
-  return getStyle(layer, styleSetOrStyles, style).border;
+    return getStyle(layer, styleSetOrStyles, style).border
 }
 
-export function foreground(layer: Layer, style?: Styles): string;
+export function foreground(layer: Layer, style?: Styles): string
 export function foreground(
-  layer: Layer,
-  styleSet?: StyleSets,
-  style?: Styles
-): string;
+    layer: Layer,
+    styleSet?: StyleSets,
+    style?: Styles
+): string
 export function foreground(
-  layer: Layer,
-  styleSetOrStyles?: StyleSets | Styles,
-  style?: Styles
+    layer: Layer,
+    styleSetOrStyles?: StyleSets | Styles,
+    style?: Styles
 ): string {
-  return getStyle(layer, styleSetOrStyles, style).foreground;
+    return getStyle(layer, styleSetOrStyles, style).foreground
 }
 
 interface Text {
-  family: keyof typeof fontFamilies;
-  color: string;
-  size: number;
-  weight?: FontWeight;
-  underline?: boolean;
+    family: keyof typeof fontFamilies
+    color: string
+    size: number
+    weight?: FontWeight
+    underline?: boolean
+}
+
+export interface TextProperties {
+    size?: keyof typeof fontSizes
+    weight?: FontWeight
+    underline?: boolean
+    color?: string
+    features?: FontFeatures
 }
 
-interface TextProperties {
-  size?: keyof typeof fontSizes;
-  weight?: FontWeight;
-  underline?: boolean;
-  color?: string;
+interface FontFeatures {
+    /** Contextual Alternates: Applies a second substitution feature based on a match of a character pattern within a context of surrounding patterns */
+    calt?: boolean
+    /** Case-Sensitive Forms: Shifts various punctuation marks up to a position that works better with all-capital sequences */
+    case?: boolean
+    /** Capital Spacing: Adjusts inter-glyph spacing for all-capital text */
+    cpsp?: boolean
+    /** Fractions: Replaces figures separated by a slash with diagonal fractions */
+    frac?: boolean
+    /** Standard Ligatures: Replaces a sequence of glyphs with a single glyph which is preferred for typographic purposes */
+    liga?: boolean
+    /** Oldstyle Figures: Changes selected figures from the default or lining style to oldstyle form. */
+    onum?: boolean
+    /** Ordinals: Replaces default alphabetic glyphs with the corresponding ordinal forms for use after figures */
+    ordn?: boolean
+    /** Proportional Figures: Replaces figure glyphs set on uniform (tabular) widths with corresponding glyphs set on proportional widths */
+    pnum?: boolean
+    /** Stylistic set 01 */
+    ss01?: boolean
+    /** Stylistic set 02 */
+    ss02?: boolean
+    /** Stylistic set 03 */
+    ss03?: boolean
+    /** Stylistic set 04 */
+    ss04?: boolean
+    /** Stylistic set 05 */
+    ss05?: boolean
+    /** Stylistic set 06 */
+    ss06?: boolean
+    /** Stylistic set 07 */
+    ss07?: boolean
+    /** Stylistic set 08 */
+    ss08?: boolean
+    /** Stylistic set 09 */
+    ss09?: boolean
+    /** Stylistic set 10 */
+    ss10?: boolean
+    /** Stylistic set 11 */
+    ss11?: boolean
+    /** Stylistic set 12 */
+    ss12?: boolean
+    /** Stylistic set 13 */
+    ss13?: boolean
+    /** Stylistic set 14 */
+    ss14?: boolean
+    /** Stylistic set 15 */
+    ss15?: boolean
+    /** Stylistic set 16 */
+    ss16?: boolean
+    /** Stylistic set 17 */
+    ss17?: boolean
+    /** Stylistic set 18 */
+    ss18?: boolean
+    /** Stylistic set 19 */
+    ss19?: boolean
+    /** Stylistic set 20 */
+    ss20?: boolean
+    /** Subscript: Replaces default glyphs with subscript glyphs */
+    subs?: boolean
+    /** Superscript: Replaces default glyphs with superscript glyphs */
+    sups?: boolean
+    /** Swash: Replaces default glyphs with swash glyphs for stylistic purposes */
+    swsh?: boolean
+    /** Titling: Replaces default glyphs with titling glyphs for use in large-size settings */
+    titl?: boolean
+    /** Tabular Figures: Replaces figure glyphs set on proportional widths with corresponding glyphs set on uniform (tabular) widths */
+    tnum?: boolean
+    /** Slashed Zero: Replaces default zero with a slashed zero for better distinction between "0" and "O" */
+    zero?: boolean
 }
 
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  styleSet: StyleSets,
-  style: Styles,
-  properties?: TextProperties
-): Text;
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    styleSet: StyleSets,
+    style: Styles,
+    properties?: TextProperties
+): Text
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  styleSet: StyleSets,
-  properties?: TextProperties
-): Text;
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    styleSet: StyleSets,
+    properties?: TextProperties
+): Text
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  style: Styles,
-  properties?: TextProperties
-): Text;
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    style: Styles,
+    properties?: TextProperties
+): Text
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  properties?: TextProperties
-): Text;
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    properties?: TextProperties
+): Text
 export function text(
-  layer: Layer,
-  fontFamily: keyof typeof fontFamilies,
-  styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
-  styleOrProperties?: Styles | TextProperties,
-  properties?: TextProperties
+    layer: Layer,
+    fontFamily: keyof typeof fontFamilies,
+    styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
+    styleOrProperties?: Styles | TextProperties,
+    properties?: TextProperties
 ) {
-  let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
-
-  if (typeof styleSetStyleOrProperties === "object") {
-    properties = styleSetStyleOrProperties;
-  }
-  if (typeof styleOrProperties === "object") {
-    properties = styleOrProperties;
-  }
-
-  let size = fontSizes[properties?.size || "sm"];
-  let color = properties?.color || style.foreground;
-
-  return {
-    family: fontFamilies[fontFamily],
-    ...properties,
-    color,
-    size,
-  };
+    let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+
+    if (typeof styleSetStyleOrProperties === "object") {
+        properties = styleSetStyleOrProperties
+    }
+    if (typeof styleOrProperties === "object") {
+        properties = styleOrProperties
+    }
+
+    let size = fontSizes[properties?.size || "sm"]
+    let color = properties?.color || style.foreground
+
+    return {
+        family: fontFamilies[fontFamily],
+        ...properties,
+        color,
+        size,
+    }
 }
 
 export interface Border {
-  color: string;
-  width: number;
-  top?: boolean;
-  bottom?: boolean;
-  left?: boolean;
-  right?: boolean;
-  overlay?: boolean;
+    color: string
+    width: number
+    top?: boolean
+    bottom?: boolean
+    left?: boolean
+    right?: boolean
+    overlay?: boolean
 }
 
 export interface BorderProperties {
-  width?: number;
-  top?: boolean;
-  bottom?: boolean;
-  left?: boolean;
-  right?: boolean;
-  overlay?: boolean;
+    width?: number
+    top?: boolean
+    bottom?: boolean
+    left?: boolean
+    right?: boolean
+    overlay?: boolean
 }
 
 export function border(
-  layer: Layer,
-  styleSet: StyleSets,
-  style: Styles,
-  properties?: BorderProperties
-): Border;
+    layer: Layer,
+    styleSet: StyleSets,
+    style: Styles,
+    properties?: BorderProperties
+): Border
 export function border(
-  layer: Layer,
-  styleSet: StyleSets,
-  properties?: BorderProperties
-): Border;
+    layer: Layer,
+    styleSet: StyleSets,
+    properties?: BorderProperties
+): Border
 export function border(
-  layer: Layer,
-  style: Styles,
-  properties?: BorderProperties
-): Border;
-export function border(layer: Layer, properties?: BorderProperties): Border;
+    layer: Layer,
+    style: Styles,
+    properties?: BorderProperties
+): Border
+export function border(layer: Layer, properties?: BorderProperties): Border
 export function border(
-  layer: Layer,
-  styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
-  styleOrProperties?: Styles | BorderProperties,
-  properties?: BorderProperties
+    layer: Layer,
+    styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
+    styleOrProperties?: Styles | BorderProperties,
+    properties?: BorderProperties
 ): Border {
-  let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
-
-  if (typeof styleSetStyleOrProperties === "object") {
-    properties = styleSetStyleOrProperties;
-  }
-  if (typeof styleOrProperties === "object") {
-    properties = styleOrProperties;
-  }
-
-  return {
-    color: style.border,
-    width: 1,
-    ...properties,
-  };
+    let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+
+    if (typeof styleSetStyleOrProperties === "object") {
+        properties = styleSetStyleOrProperties
+    }
+    if (typeof styleOrProperties === "object") {
+        properties = styleOrProperties
+    }
+
+    return {
+        color: style.border,
+        width: 1,
+        ...properties,
+    }
+}
+
+
+export function svg(color: string, asset: String, width: Number, height: Number) {
+    return {
+        color,
+        asset,
+        dimensions: {
+            width,
+            height,
+        }
+    }
 }

styles/src/styleTree/contactFinder.ts 🔗

@@ -1,70 +1,70 @@
-import picker from "./picker";
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, foreground, text } from "./components";
+import picker from "./picker"
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, foreground, text } from "./components"
 
 export default function contactFinder(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
+    let layer = colorScheme.middle
 
-  const sideMargin = 6;
-  const contactButton = {
-    background: background(layer, "variant"),
-    color: foreground(layer, "variant"),
-    iconWidth: 8,
-    buttonWidth: 16,
-    cornerRadius: 8,
-  };
+    const sideMargin = 6
+    const contactButton = {
+        background: background(layer, "variant"),
+        color: foreground(layer, "variant"),
+        iconWidth: 8,
+        buttonWidth: 16,
+        cornerRadius: 8,
+    }
 
-  const pickerStyle = picker(colorScheme);
-  const pickerInput = {
-    background: background(layer, "on"),
-    cornerRadius: 6,
-    text: text(layer, "mono",),
-    placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
-    selection: colorScheme.players[0],
-    border: border(layer),
-    padding: {
-      bottom: 4,
-      left: 8,
-      right: 8,
-      top: 4,
-    },
-    margin: {
-      left: sideMargin,
-      right: sideMargin,
+    const pickerStyle = picker(colorScheme)
+    const pickerInput = {
+        background: background(layer, "on"),
+        cornerRadius: 6,
+        text: text(layer, "mono"),
+        placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
+        selection: colorScheme.players[0],
+        border: border(layer),
+        padding: {
+            bottom: 4,
+            left: 8,
+            right: 8,
+            top: 4,
+        },
+        margin: {
+            left: sideMargin,
+            right: sideMargin,
+        },
     }
-  };
 
-  return {
-    picker: {
-      emptyContainer: {},
-      item: {
-        ...pickerStyle.item,
-        margin: { left: sideMargin, right: sideMargin },
-      },
-      noMatches: pickerStyle.noMatches,
-      inputEditor: pickerInput,
-      emptyInputEditor: pickerInput
-    },
-    rowHeight: 28,
-    contactAvatar: {
-      cornerRadius: 10,
-      width: 18,
-    },
-    contactUsername: {
-      padding: {
-        left: 8,
-      },
-    },
-    contactButton: {
-      ...contactButton,
-      hover: {
-        background: background(layer, "variant", "hovered"),
-      },
-    },
-    disabledContactButton: {
-      ...contactButton,
-      background: background(layer, "disabled"),
-      color: foreground(layer, "disabled"),
-    },
-  };
+    return {
+        picker: {
+            emptyContainer: {},
+            item: {
+                ...pickerStyle.item,
+                margin: { left: sideMargin, right: sideMargin },
+            },
+            noMatches: pickerStyle.noMatches,
+            inputEditor: pickerInput,
+            emptyInputEditor: pickerInput,
+        },
+        rowHeight: 28,
+        contactAvatar: {
+            cornerRadius: 10,
+            width: 18,
+        },
+        contactUsername: {
+            padding: {
+                left: 8,
+            },
+        },
+        contactButton: {
+            ...contactButton,
+            hover: {
+                background: background(layer, "variant", "hovered"),
+            },
+        },
+        disabledContactButton: {
+            ...contactButton,
+            background: background(layer, "disabled"),
+            color: foreground(layer, "disabled"),
+        },
+    }
 }

styles/src/styleTree/contactList.ts 🔗

@@ -1,186 +1,182 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import {
-  background,
-  border,
-  borderColor,
-  foreground,
-  text,
-} from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, borderColor, foreground, text } from "./components"
 
 export default function contactsPanel(colorScheme: ColorScheme) {
-  const nameMargin = 8;
-  const sidePadding = 12;
+    const nameMargin = 8
+    const sidePadding = 12
 
-  let layer = colorScheme.middle;
+    let layer = colorScheme.middle
 
-  const contactButton = {
-    background: background(layer, "on"),
-    color: foreground(layer, "on"),
-    iconWidth: 8,
-    buttonWidth: 16,
-    cornerRadius: 8,
-  };
-  const projectRow = {
-    guestAvatarSpacing: 4,
-    height: 24,
-    guestAvatar: {
-      cornerRadius: 8,
-      width: 14,
-    },
-    name: {
-      ...text(layer, "mono", { size: "sm" }),
-      margin: {
-        left: nameMargin,
-        right: 6,
-      },
-    },
-    guests: {
-      margin: {
-        left: nameMargin,
-        right: nameMargin,
-      },
-    },
-    padding: {
-      left: sidePadding,
-      right: sidePadding,
-    },
-  };
+    const contactButton = {
+        background: background(layer, "on"),
+        color: foreground(layer, "on"),
+        iconWidth: 8,
+        buttonWidth: 16,
+        cornerRadius: 8,
+    }
+    const projectRow = {
+        guestAvatarSpacing: 4,
+        height: 24,
+        guestAvatar: {
+            cornerRadius: 8,
+            width: 14,
+        },
+        name: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: {
+                left: nameMargin,
+                right: 6,
+            },
+        },
+        guests: {
+            margin: {
+                left: nameMargin,
+                right: nameMargin,
+            },
+        },
+        padding: {
+            left: sidePadding,
+            right: sidePadding,
+        },
+    }
 
-  return {
-    background: background(layer),
-    padding: { top: 12, bottom: 0 },
-    userQueryEditor: {
-      background: background(layer, "on"),
-      cornerRadius: 6,
-      text: text(layer, "mono", "on"),
-      placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
-      selection: colorScheme.players[0],
-      border: border(layer, "on"),
-      padding: {
-        bottom: 4,
-        left: 8,
-        right: 8,
-        top: 4,
-      },
-      margin: {
-        left: 6,
-      },
-    },
-    userQueryEditorHeight: 33,
-    addContactButton: {
-      margin: { left: 6, right: 12 },
-      color: foreground(layer, "on"),
-      buttonWidth: 28,
-      iconWidth: 16,
-    },
-    rowHeight: 28,
-    sectionIconSize: 8,
-    headerRow: {
-      ...text(layer, "mono", { size: "sm" }),
-      margin: { top: 14 },
-      padding: {
-        left: sidePadding,
-        right: sidePadding,
-      },
-      active: {
-        ...text(layer, "mono", "active", { size: "sm" }),
-        background: background(layer, "active"),
-      },
-    },
-    leaveCall: {
-      background: background(layer),
-      border: border(layer),
-      cornerRadius: 6,
-      margin: {
-        top: 1,
-      },
-      padding: {
-        top: 1,
-        bottom: 1,
-        left: 7,
-        right: 7,
-      },
-      ...text(layer, "sans", "variant", { size: "xs" }),
-      hover: {
-        ...text(layer, "sans", "hovered", { size: "xs" }),
-        background: background(layer, "hovered"),
-        border: border(layer, "hovered"),
-      },
-    },
-    contactRow: {
-      padding: {
-        left: sidePadding,
-        right: sidePadding,
-      },
-      active: {
-        background: background(layer, "active"),
-      },
-    },
-    contactAvatar: {
-      cornerRadius: 10,
-      width: 18,
-    },
-    contactStatusFree: {
-      cornerRadius: 4,
-      padding: 4,
-      margin: { top: 12, left: 12 },
-      background: foreground(layer, "positive"),
-    },
-    contactStatusBusy: {
-      cornerRadius: 4,
-      padding: 4,
-      margin: { top: 12, left: 12 },
-      background: foreground(layer, "negative"),
-    },
-    contactUsername: {
-      ...text(layer, "mono", { size: "sm" }),
-      margin: {
-        left: nameMargin,
-      },
-    },
-    contactButtonSpacing: nameMargin,
-    contactButton: {
-      ...contactButton,
-      hover: {
-        background: background(layer, "hovered"),
-      },
-    },
-    disabledButton: {
-      ...contactButton,
-      background: background(layer, "on"),
-      color: foreground(layer, "on"),
-    },
-    callingIndicator: {
-      ...text(layer, "mono", "variant", { size: "xs" }),
-    },
-    treeBranch: {
-      color: borderColor(layer),
-      width: 1,
-      hover: {
-        color: borderColor(layer),
-      },
-      active: {
-        color: borderColor(layer),
-      },
-    },
-    projectRow: {
-      ...projectRow,
-      background: background(layer),
-      icon: {
-        margin: { left: nameMargin },
-        color: foreground(layer, "variant"),
-        width: 12,
-      },
-      name: {
-        ...projectRow.name,
-        ...text(layer, "mono", { size: "sm" }),
-      },
-      hover: {
-        background: background(layer, "hovered"),
-      },
-      active: {
-        background: background(layer, "active"),
-      },
-    },
-  };
+    return {
+        background: background(layer),
+        padding: { top: 12 },
+        userQueryEditor: {
+            background: background(layer, "on"),
+            cornerRadius: 6,
+            text: text(layer, "mono", "on"),
+            placeholderText: text(layer, "mono", "on", "disabled", {
+                size: "xs",
+            }),
+            selection: colorScheme.players[0],
+            border: border(layer, "on"),
+            padding: {
+                bottom: 4,
+                left: 8,
+                right: 8,
+                top: 4,
+            },
+            margin: {
+                left: 6,
+            },
+        },
+        userQueryEditorHeight: 33,
+        addContactButton: {
+            margin: { left: 6, right: 12 },
+            color: foreground(layer, "on"),
+            buttonWidth: 28,
+            iconWidth: 16,
+        },
+        rowHeight: 28,
+        sectionIconSize: 8,
+        headerRow: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: { top: 14 },
+            padding: {
+                left: sidePadding,
+                right: sidePadding,
+            },
+            active: {
+                ...text(layer, "mono", "active", { size: "sm" }),
+                background: background(layer, "active"),
+            },
+        },
+        leaveCall: {
+            background: background(layer),
+            border: border(layer),
+            cornerRadius: 6,
+            margin: {
+                top: 1,
+            },
+            padding: {
+                top: 1,
+                bottom: 1,
+                left: 7,
+                right: 7,
+            },
+            ...text(layer, "sans", "variant", { size: "xs" }),
+            hover: {
+                ...text(layer, "sans", "hovered", { size: "xs" }),
+                background: background(layer, "hovered"),
+                border: border(layer, "hovered"),
+            },
+        },
+        contactRow: {
+            padding: {
+                left: sidePadding,
+                right: sidePadding,
+            },
+            active: {
+                background: background(layer, "active"),
+            },
+        },
+        contactAvatar: {
+            cornerRadius: 10,
+            width: 18,
+        },
+        contactStatusFree: {
+            cornerRadius: 4,
+            padding: 4,
+            margin: { top: 12, left: 12 },
+            background: foreground(layer, "positive"),
+        },
+        contactStatusBusy: {
+            cornerRadius: 4,
+            padding: 4,
+            margin: { top: 12, left: 12 },
+            background: foreground(layer, "negative"),
+        },
+        contactUsername: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: {
+                left: nameMargin,
+            },
+        },
+        contactButtonSpacing: nameMargin,
+        contactButton: {
+            ...contactButton,
+            hover: {
+                background: background(layer, "hovered"),
+            },
+        },
+        disabledButton: {
+            ...contactButton,
+            background: background(layer, "on"),
+            color: foreground(layer, "on"),
+        },
+        callingIndicator: {
+            ...text(layer, "mono", "variant", { size: "xs" }),
+        },
+        treeBranch: {
+            color: borderColor(layer),
+            width: 1,
+            hover: {
+                color: borderColor(layer),
+            },
+            active: {
+                color: borderColor(layer),
+            },
+        },
+        projectRow: {
+            ...projectRow,
+            background: background(layer),
+            icon: {
+                margin: { left: nameMargin },
+                color: foreground(layer, "variant"),
+                width: 12,
+            },
+            name: {
+                ...projectRow.name,
+                ...text(layer, "mono", { size: "sm" }),
+            },
+            hover: {
+                background: background(layer, "hovered"),
+            },
+            active: {
+                background: background(layer, "active"),
+            },
+        },
+    }
 }

styles/src/styleTree/contactNotification.ts 🔗

@@ -1,45 +1,45 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, foreground, text } from "./components"
 
-const avatarSize = 12;
-const headerPadding = 8;
+const avatarSize = 12
+const headerPadding = 8
 
 export default function contactNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.lowest;
-  return {
-    headerAvatar: {
-      height: avatarSize,
-      width: avatarSize,
-      cornerRadius: 6,
-    },
-    headerMessage: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, right: headerPadding },
-    },
-    headerHeight: 18,
-    bodyMessage: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
-    },
-    button: {
-      ...text(layer, "sans", "on", { size: "xs" }),
-      background: background(layer, "on"),
-      padding: 4,
-      cornerRadius: 6,
-      margin: { left: 6 },
-      hover: {
-        background: background(layer, "on", "hovered"),
-      },
-    },
-    dismissButton: {
-      color: foreground(layer, "variant"),
-      iconWidth: 8,
-      iconHeight: 8,
-      buttonWidth: 8,
-      buttonHeight: 8,
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-  };
+    let layer = colorScheme.lowest
+    return {
+        headerAvatar: {
+            height: avatarSize,
+            width: avatarSize,
+            cornerRadius: 6,
+        },
+        headerMessage: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: headerPadding, right: headerPadding },
+        },
+        headerHeight: 18,
+        bodyMessage: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
+        },
+        button: {
+            ...text(layer, "sans", "on", { size: "xs" }),
+            background: background(layer, "on"),
+            padding: 4,
+            cornerRadius: 6,
+            margin: { left: 6 },
+            hover: {
+                background: background(layer, "on", "hovered"),
+            },
+        },
+        dismissButton: {
+            color: foreground(layer, "variant"),
+            iconWidth: 8,
+            iconHeight: 8,
+            buttonWidth: 8,
+            buttonHeight: 8,
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+    }
 }

styles/src/styleTree/contactsPopover.ts 🔗

@@ -1,29 +1,16 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function contactsPopover(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  const sidePadding = 12;
-  return {
-    background: background(layer),
-    cornerRadius: 6,
-    padding: { top: 6 },
-    margin: { top: -6 },
-    shadow: colorScheme.popoverShadow,
-    border: border(layer),
-    width: 300,
-    height: 400,
-    inviteRowHeight: 28,
-    inviteRow: {
-      padding: {
-        left: sidePadding,
-        right: sidePadding,
-      },
-      border: border(layer, { top: true }),
-      text: text(layer, "sans", "variant", { size: "sm" }),
-      hover: {
-        text: text(layer, "sans", "hovered", { size: "sm" }),
-      },
-    },
-  }
+    let layer = colorScheme.middle
+    const sidePadding = 12
+    return {
+        background: background(layer),
+        cornerRadius: 6,
+        padding: { top: 6, bottom: 6 },
+        shadow: colorScheme.popoverShadow,
+        border: border(layer),
+        width: 300,
+        height: 400,
+    }
 }

styles/src/styleTree/contextMenu.ts 🔗

@@ -1,41 +1,49 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, borderColor, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, borderColor, text } from "./components"
 
 export default function contextMenu(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  return {
-    background: background(layer),
-    cornerRadius: 10,
-    padding: 4,
-    shadow: colorScheme.popoverShadow,
-    border: border(layer),
-    keystrokeMargin: 30,
-    item: {
-      iconSpacing: 8,
-      iconWidth: 14,
-      padding: { left: 6, right: 6, top: 2, bottom: 2 },
-      cornerRadius: 6,
-      label: text(layer, "sans", { size: "sm" }),
-      keystroke: {
-        ...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
-        padding: { left: 3, right: 3 },
-      },
-      hover: {
-        background: background(layer, "hovered"),
-        label: text(layer, "sans", "hovered", { size: "sm" }),
-      },
-      active: {
-        background: background(layer, "active"),
-        label: text(layer, "sans", "active", { size: "sm" }),
-      },
-      activeHover: {
-        background: background(layer, "active"),
-        label: text(layer, "sans", "active", { size: "sm" }),
-      },
-    },
-    separator: {
-      background: borderColor(layer),
-      margin: { top: 2, bottom: 2 },
-    },
-  };
+    let layer = colorScheme.middle
+    return {
+        background: background(layer),
+        cornerRadius: 10,
+        padding: 4,
+        shadow: colorScheme.popoverShadow,
+        border: border(layer),
+        keystrokeMargin: 30,
+        item: {
+            iconSpacing: 8,
+            iconWidth: 14,
+            padding: { left: 6, right: 6, top: 2, bottom: 2 },
+            cornerRadius: 6,
+            label: text(layer, "sans", { size: "sm" }),
+            keystroke: {
+                ...text(layer, "sans", "variant", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+                padding: { left: 3, right: 3 },
+            },
+            hover: {
+                background: background(layer, "hovered"),
+                label: text(layer, "sans", "hovered", { size: "sm" }),
+                keystroke: {
+                    ...text(layer, "sans", "hovered", {
+                        size: "sm",
+                        weight: "bold",
+                    }),
+                    padding: { left: 3, right: 3 },
+                },
+            },
+            active: {
+                background: background(layer, "active"),
+            },
+            activeHover: {
+                background: background(layer, "active"),
+            },
+        },
+        separator: {
+            background: borderColor(layer),
+            margin: { top: 2, bottom: 2 },
+        },
+    }
 }

styles/src/styleTree/copilot.ts 🔗

@@ -0,0 +1,226 @@
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, foreground, svg, text } from "./components";
+
+
+export default function copilot(colorScheme: ColorScheme) {
+    let layer = colorScheme.middle;
+
+    let content_width = 264;
+
+    let ctaButton = { // Copied from welcome screen. FIXME: Move this into a ZDS component
+        background: background(layer),
+        border: border(layer, "default"),
+        cornerRadius: 4,
+        margin: {
+            top: 4,
+            bottom: 4,
+            left: 8,
+            right: 8
+        },
+        padding: {
+            top: 3,
+            bottom: 3,
+            left: 7,
+            right: 7,
+        },
+        ...text(layer, "sans", "default", { size: "sm" }),
+        hover: {
+            ...text(layer, "sans", "default", { size: "sm" }),
+            background: background(layer, "hovered"),
+            border: border(layer, "active"),
+        },
+    };
+
+    return {
+        outLinkIcon: {
+            icon: svg(foreground(layer, "variant"), "icons/link_out_12.svg", 12, 12),
+            container: {
+                cornerRadius: 6,
+                padding: { left: 6 },
+            },
+            hover: {
+                icon: svg(foreground(layer, "hovered"), "icons/link_out_12.svg", 12, 12)
+            },
+        },
+        modal: {
+            titleText: {
+                ...text(layer, "sans", { size: "xs", "weight": "bold" })
+            },
+            titlebar: {
+                background: background(colorScheme.lowest),
+                border: border(layer, "active"),
+                padding: {
+                    top: 4,
+                    bottom: 4,
+                    left: 8,
+                    right: 8,
+                }
+            },
+            container: {
+                background: background(colorScheme.lowest),
+                padding: {
+                    top: 0,
+                    left: 0,
+                    right: 0,
+                    bottom: 8,
+                }
+            },
+            closeIcon: {
+                icon: svg(foreground(layer, "variant"), "icons/x_mark_8.svg", 8, 8),
+                container: {
+                    cornerRadius: 2,
+                    padding: {
+                        top: 4,
+                        bottom: 4,
+                        left: 4,
+                        right: 4,
+                    },
+                    margin: {
+                        right: 0
+                    }
+                },
+                hover: {
+                    icon: svg(foreground(layer, "on"), "icons/x_mark_8.svg", 8, 8),
+                },
+                clicked: {
+                    icon: svg(foreground(layer, "base"), "icons/x_mark_8.svg", 8, 8),
+                }
+            },
+            dimensions: {
+                width: 280,
+                height: 280,
+            },
+        },
+
+        auth: {
+            content_width,
+
+            ctaButton,
+
+            header: {
+                icon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 92, 32),
+                container: {
+                    margin: {
+                        top: 35,
+                        bottom: 5,
+                        left: 0,
+                        right: 0
+                    }
+                },
+            },
+
+            prompting: {
+                subheading: {
+                    ...text(layer, "sans", { size: "xs" }),
+                    margin: {
+                        top: 6,
+                        bottom: 12,
+                        left: 0,
+                        right: 0
+                    }
+                },
+
+                hint: {
+                    ...text(layer, "sans", { size: "xs", color: "#838994" }),
+                    margin: {
+                        top: 6,
+                        bottom: 2
+                    }
+                },
+
+                deviceCode: {
+                    text:
+                        text(layer, "mono", { size: "sm" }),
+                    cta: {
+                        ...ctaButton,
+                        background: background(colorScheme.lowest),
+                        border: border(colorScheme.lowest, "inverted"),
+                        padding: {
+                            top: 0,
+                            bottom: 0,
+                            left: 16,
+                            right: 16,
+                        },
+                        margin: {
+                            left: 16,
+                            right: 16,
+                        }
+                    },
+                    left: content_width / 2,
+                    leftContainer: {
+                        padding: {
+                            top: 3,
+                            bottom: 3,
+                            left: 0,
+                            right: 6,
+                        },
+                    },
+                    right: content_width * 1 / 3,
+                    rightContainer: {
+                        border: border(colorScheme.lowest, "inverted", { bottom: false, right: false, top: false, left: true }),
+                        padding: {
+                            top: 3,
+                            bottom: 5,
+                            left: 8,
+                            right: 0,
+                        },
+                        hover: {
+                            border: border(layer, "active", { bottom: false, right: false, top: false, left: true }),
+                        },
+                    }
+                },
+            },
+
+            notAuthorized: {
+                subheading: {
+                    ...text(layer, "sans", { size: "xs" }),
+
+                    margin: {
+                        top: 16,
+                        bottom: 16,
+                        left: 0,
+                        right: 0
+                    }
+                },
+
+                warning: {
+                    ...text(layer, "sans", { size: "xs", color: foreground(layer, "warning") }),
+                    border: border(layer, "warning"),
+                    background: background(layer, "warning"),
+                    cornerRadius: 2,
+                    padding: {
+                        top: 4,
+                        left: 4,
+                        bottom: 4,
+                        right: 4,
+                    },
+                    margin: {
+                        bottom: 16,
+                        left: 8,
+                        right: 8
+                    }
+                },
+            },
+
+            authorized: {
+                subheading: {
+                    ...text(layer, "sans", { size: "xs" }),
+
+                    margin: {
+                        top: 16,
+                        bottom: 16
+                    }
+                },
+
+                hint: {
+                    ...text(layer, "sans", { size: "xs", color: "#838994" }),
+                    margin: {
+                        top: 24,
+                        bottom: 4
+                    }
+                },
+
+            },
+        }
+    }
+}

styles/src/styleTree/editor.ts 🔗

@@ -1,285 +1,250 @@
-import { fontWeights } from "../common";
-import { withOpacity } from "../utils/color";
-import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme";
-import {
-  background,
-  border,
-  borderColor,
-  foreground,
-  text,
-} from "./components";
-import hoverPopover from "./hoverPopover";
+import { withOpacity } from "../utils/color"
+import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme"
+import { background, border, borderColor, foreground, text } from "./components"
+import hoverPopover from "./hoverPopover"
+
+import { buildSyntax } from "../themes/common/syntax"
 
 export default function editor(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+    let layer = colorScheme.highest
+
+    const autocompleteItem = {
+        cornerRadius: 6,
+        padding: {
+            bottom: 2,
+            left: 6,
+            right: 6,
+            top: 2,
+        },
+    }
+
+    function diagnostic(layer: Layer, styleSet: StyleSets) {
+        return {
+            textScaleFactor: 0.857,
+            header: {
+                border: border(layer, {
+                    top: true,
+                }),
+            },
+            message: {
+                text: text(layer, "sans", styleSet, "default", { size: "sm" }),
+                highlightText: text(layer, "sans", styleSet, "default", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        }
+    }
 
-  const autocompleteItem = {
-    cornerRadius: 6,
-    padding: {
-      bottom: 2,
-      left: 6,
-      right: 6,
-      top: 2,
-    },
-  };
+    const syntax = buildSyntax(colorScheme)
 
-  function diagnostic(layer: Layer, styleSet: StyleSets) {
     return {
-      textScaleFactor: 0.857,
-      header: {
-        border: border(layer, {
-          top: true,
-        }),
-      },
-      message: {
-        text: text(layer, "sans", styleSet, "default", { size: "sm" }),
-        highlightText: text(layer, "sans", styleSet, "default", {
-          size: "sm",
-          weight: "bold",
-        }),
-      },
-    };
-  }
+        textColor: syntax.primary.color,
+        background: background(layer),
+        activeLineBackground: withOpacity(background(layer, "on"), 0.75),
+        highlightedLineBackground: background(layer, "on"),
+        // Inline autocomplete suggestions, Co-pilot suggestions, etc.
+        suggestion: {
+            color: syntax.predictive.color,
+        },
+        codeActions: {
+            indicator: {
+                color: foreground(layer, "variant"),
+
+                clicked: {
+                    color: foreground(layer, "base"),
+                },
+                hover: {
+                    color: foreground(layer, "on"),
+                },
+                active: {
+                    color: foreground(layer, "on"),
+                },
+            },
+            verticalScale: 0.55,
+        },
+        folds: {
+            iconMarginScale: 2.5,
+            foldedIcon: "icons/chevron_right_8.svg",
+            foldableIcon: "icons/chevron_down_8.svg",
+            indicator: {
+                color: foreground(layer, "variant"),
 
-  const syntax = {
-    primary: {
-      color: colorScheme.ramps.neutral(1).hex(),
-      weight: fontWeights.normal,
-    },
-    "variable.special": {
-      // Highlights for self, this, etc
-      color: colorScheme.ramps.blue(0.7).hex(),
-      weight: fontWeights.normal,
-    },
-    comment: {
-      color: colorScheme.ramps.neutral(0.71).hex(),
-      weight: fontWeights.normal,
-    },
-    punctuation: {
-      color: colorScheme.ramps.neutral(0.86).hex(),
-      weight: fontWeights.normal,
-    },
-    constant: {
-      color: colorScheme.ramps.green(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    keyword: {
-      color: colorScheme.ramps.blue(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    function: {
-      color: colorScheme.ramps.yellow(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    type: {
-      color: colorScheme.ramps.cyan(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    constructor: {
-      color: colorScheme.ramps.blue(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    variant: {
-      color: colorScheme.ramps.blue(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    property: {
-      color: colorScheme.ramps.blue(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    enum: {
-      color: colorScheme.ramps.orange(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    operator: {
-      color: colorScheme.ramps.orange(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    string: {
-      color: colorScheme.ramps.orange(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    number: {
-      color: colorScheme.ramps.green(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    boolean: {
-      color: colorScheme.ramps.green(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    predictive: {
-      color: colorScheme.ramps.neutral(0.57).hex(),
-      weight: fontWeights.normal,
-    },
-    title: {
-      color: colorScheme.ramps.yellow(0.5).hex(),
-      weight: fontWeights.bold,
-    },
-    emphasis: {
-      color: colorScheme.ramps.blue(0.5).hex(),
-      weight: fontWeights.normal,
-    },
-    "emphasis.strong": {
-      color: colorScheme.ramps.blue(0.5).hex(),
-      weight: fontWeights.bold,
-    },
-    linkUri: {
-      color: colorScheme.ramps.green(0.5).hex(),
-      weight: fontWeights.normal,
-      underline: true,
-    },
-    linkText: {
-      color: colorScheme.ramps.orange(0.5).hex(),
-      weight: fontWeights.normal,
-      italic: true,
-    },
-  };
+                clicked: {
+                    color: foreground(layer, "base"),
+                },
+                hover: {
+                    color: foreground(layer, "on"),
+                },
+                active: {
+                    color: foreground(layer, "on"),
+                },
+            },
+            ellipses: {
+                textColor: colorScheme.ramps.neutral(0.71).hex(),
+                cornerRadiusFactor: 0.15,
+                background: {
+                    // Copied from hover_popover highlight
+                    color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
 
-  return {
-    textColor: syntax.primary.color,
-    background: background(layer),
-    activeLineBackground: withOpacity(background(layer, "on"), 0.75),
-    highlightedLineBackground: background(layer, "on"),
-    codeActions: {
-      indicator: foreground(layer, "variant"),
-      verticalScale: 0.55,
-    },
-    diff: {
-      deleted: foreground(layer, "negative"),
-      modified: foreground(layer, "warning"),
-      inserted: foreground(layer, "positive"),
-      removedWidthEm: 0.275,
-      widthEm: 0.16,
-      cornerRadius: 0.05,
-    },
-    /** Highlights matching occurences of what is under the cursor
-     * as well as matched brackets
-     */
-    documentHighlightReadBackground: withOpacity(foreground(layer, "accent"), 0.1),
-    documentHighlightWriteBackground: colorScheme.ramps
-      .neutral(0.5)
-      .alpha(0.4)
-      .hex(), // TODO: This was blend * 2
-    errorColor: background(layer, "negative"),
-    gutterBackground: background(layer),
-    gutterPaddingFactor: 3.5,
-    lineNumber: withOpacity(foreground(layer), 0.35),
-    lineNumberActive: foreground(layer),
-    renameFade: 0.6,
-    unnecessaryCodeFade: 0.5,
-    selection: colorScheme.players[0],
-    guestSelections: [
-      colorScheme.players[1],
-      colorScheme.players[2],
-      colorScheme.players[3],
-      colorScheme.players[4],
-      colorScheme.players[5],
-      colorScheme.players[6],
-      colorScheme.players[7],
-    ],
-    autocomplete: {
-      background: background(colorScheme.middle),
-      cornerRadius: 8,
-      padding: 4,
-      margin: {
-        left: -14,
-      },
-      border: border(colorScheme.middle),
-      shadow: colorScheme.popoverShadow,
-      matchHighlight: foreground(colorScheme.middle, "accent"),
-      item: autocompleteItem,
-      hoveredItem: {
-        ...autocompleteItem,
-        matchHighlight: foreground(colorScheme.middle, "accent", "hovered"),
-        background: background(colorScheme.middle, "hovered"),
-      },
-      selectedItem: {
-        ...autocompleteItem,
-        matchHighlight: foreground(colorScheme.middle, "accent", "active"),
-        background: background(colorScheme.middle, "active"),
-      },
-    },
-    diagnosticHeader: {
-      background: background(colorScheme.middle),
-      iconWidthFactor: 1.5,
-      textScaleFactor: 0.857,
-      border: border(colorScheme.middle, {
-        bottom: true,
-        top: true,
-      }),
-      code: {
-        ...text(colorScheme.middle, "mono", { size: "sm" }),
-        margin: {
-          left: 10,
+                    hover: {
+                        color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
+                    },
+
+                    clicked: {
+                        color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(),
+                    },
+                },
+            },
+            foldBackground: foreground(layer, "variant"),
+        },
+        diff: {
+            deleted: foreground(layer, "negative"),
+            modified: foreground(layer, "warning"),
+            inserted: foreground(layer, "positive"),
+            removedWidthEm: 0.275,
+            widthEm: 0.16,
+            cornerRadius: 0.05,
+        },
+        /** Highlights matching occurences of what is under the cursor
+         * as well as matched brackets
+         */
+        documentHighlightReadBackground: withOpacity(
+            foreground(layer, "accent"),
+            0.1
+        ),
+        documentHighlightWriteBackground: colorScheme.ramps
+            .neutral(0.5)
+            .alpha(0.4)
+            .hex(), // TODO: This was blend * 2
+        errorColor: background(layer, "negative"),
+        gutterBackground: background(layer),
+        gutterPaddingFactor: 3.5,
+        lineNumber: withOpacity(foreground(layer), 0.35),
+        lineNumberActive: foreground(layer),
+        renameFade: 0.6,
+        unnecessaryCodeFade: 0.5,
+        selection: colorScheme.players[0],
+        guestSelections: [
+            colorScheme.players[1],
+            colorScheme.players[2],
+            colorScheme.players[3],
+            colorScheme.players[4],
+            colorScheme.players[5],
+            colorScheme.players[6],
+            colorScheme.players[7],
+        ],
+        autocomplete: {
+            background: background(colorScheme.middle),
+            cornerRadius: 8,
+            padding: 4,
+            margin: {
+                left: -14,
+            },
+            border: border(colorScheme.middle),
+            shadow: colorScheme.popoverShadow,
+            matchHighlight: foreground(colorScheme.middle, "accent"),
+            item: autocompleteItem,
+            hoveredItem: {
+                ...autocompleteItem,
+                matchHighlight: foreground(
+                    colorScheme.middle,
+                    "accent",
+                    "hovered"
+                ),
+                background: background(colorScheme.middle, "hovered"),
+            },
+            selectedItem: {
+                ...autocompleteItem,
+                matchHighlight: foreground(
+                    colorScheme.middle,
+                    "accent",
+                    "active"
+                ),
+                background: background(colorScheme.middle, "active"),
+            },
+        },
+        diagnosticHeader: {
+            background: background(colorScheme.middle),
+            iconWidthFactor: 1.5,
+            textScaleFactor: 0.857,
+            border: border(colorScheme.middle, {
+                bottom: true,
+                top: true,
+            }),
+            code: {
+                ...text(colorScheme.middle, "mono", { size: "sm" }),
+                margin: {
+                    left: 10,
+                },
+            },
+            message: {
+                highlightText: text(colorScheme.middle, "sans", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+                text: text(colorScheme.middle, "sans", { size: "sm" }),
+            },
+        },
+        diagnosticPathHeader: {
+            background: background(colorScheme.middle),
+            textScaleFactor: 0.857,
+            filename: text(colorScheme.middle, "mono", { size: "sm" }),
+            path: {
+                ...text(colorScheme.middle, "mono", { size: "sm" }),
+                margin: {
+                    left: 12,
+                },
+            },
+        },
+        errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
+        warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
+        informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
+        hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
+        invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
+        invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
+        invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
+        invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
+        hoverPopover: hoverPopover(colorScheme),
+        linkDefinition: {
+            color: syntax.linkUri.color,
+            underline: syntax.linkUri.underline,
+        },
+        jumpIcon: {
+            color: foreground(layer, "on"),
+            iconWidth: 20,
+            buttonWidth: 20,
+            cornerRadius: 6,
+            padding: {
+                top: 6,
+                bottom: 6,
+                left: 6,
+                right: 6,
+            },
+            hover: {
+                background: background(layer, "on", "hovered"),
+            },
         },
-      },
-      message: {
-        highlightText: text(colorScheme.middle, "sans", {
-          size: "sm",
-          weight: "bold",
-        }),
-        text: text(colorScheme.middle, "sans", { size: "sm" }),
-      },
-    },
-    diagnosticPathHeader: {
-      background: background(colorScheme.middle),
-      textScaleFactor: 0.857,
-      filename: text(colorScheme.middle, "mono", { size: "sm" }),
-      path: {
-        ...text(colorScheme.middle, "mono", { size: "sm" }),
-        margin: {
-          left: 12,
+        scrollbar: {
+            width: 12,
+            minHeightFactor: 1.0,
+            track: {
+                border: border(layer, "variant", { left: true }),
+            },
+            thumb: {
+                background: withOpacity(background(layer, "inverted"), 0.4),
+                border: {
+                    width: 1,
+                    color: borderColor(layer, "variant"),
+                },
+            },
         },
-      },
-    },
-    errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
-    warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
-    informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
-    hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
-    invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
-    invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
-    invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
-    invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
-    hoverPopover: hoverPopover(colorScheme),
-    linkDefinition: {
-      color: syntax.linkUri.color,
-      underline: syntax.linkUri.underline,
-    },
-    jumpIcon: {
-      color: foreground(layer, "on"),
-      iconWidth: 20,
-      buttonWidth: 20,
-      cornerRadius: 6,
-      padding: {
-        top: 6,
-        bottom: 6,
-        left: 6,
-        right: 6,
-      },
-      hover: {
-        background: background(layer, "on", "hovered"),
-      },
-    },
-    scrollbar: {
-      width: 12,
-      minHeightFactor: 1.0,
-      track: {
-        border: border(layer, "variant", { left: true }),
-      },
-      thumb: {
-        background: withOpacity(background(layer, "inverted"), 0.4),
-        border: {
-          width: 1,
-          color: borderColor(layer, "variant"),
+        compositionMark: {
+            underline: {
+                thickness: 1.0,
+                color: borderColor(layer),
+            },
         },
-      },
-    },
-    compositionMark: {
-      underline: {
-        thickness: 1.0,
-        color: borderColor(layer),
-      },
-    },
-    syntax,
-  };
+        syntax,
+    }
 }

styles/src/styleTree/feedback.ts 🔗

@@ -1,37 +1,44 @@
-
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function feedback(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+    let layer = colorScheme.highest
 
-  return {
-    submit_button: {
-      ...text(layer, "mono", "on"),
-      background: background(layer, "on"),
-      cornerRadius: 6,
-      border: border(layer, "on"),
-      margin: {
-        right: 4,
-      },
-      padding: {
-        bottom: 2,
-        left: 10,
-        right: 10,
-        top: 2,
-      },
-      clicked: {
-        ...text(layer, "mono", "on", "pressed"),
-        background: background(layer, "on", "pressed"),
-        border: border(layer, "on", "pressed"),
-      },
-      hover: {
-        ...text(layer, "mono", "on", "hovered"),
-        background: background(layer, "on", "hovered"),
-        border: border(layer, "on", "hovered"),
-      },
-    },
-    button_margin: 8,
-    info_text: text(layer, "sans", "default", { size: "xs" }),
-  };
+    return {
+        submit_button: {
+            ...text(layer, "mono", "on"),
+            background: background(layer, "on"),
+            cornerRadius: 6,
+            border: border(layer, "on"),
+            margin: {
+                right: 4,
+            },
+            padding: {
+                bottom: 2,
+                left: 10,
+                right: 10,
+                top: 2,
+            },
+            clicked: {
+                ...text(layer, "mono", "on", "pressed"),
+                background: background(layer, "on", "pressed"),
+                border: border(layer, "on", "pressed"),
+            },
+            hover: {
+                ...text(layer, "mono", "on", "hovered"),
+                background: background(layer, "on", "hovered"),
+                border: border(layer, "on", "hovered"),
+            },
+        },
+        button_margin: 8,
+        info_text_default: text(layer, "sans", "default", { size: "xs" }),
+        link_text_default: text(layer, "sans", "default", {
+            size: "xs",
+            underline: true,
+        }),
+        link_text_hover: text(layer, "sans", "hovered", {
+            size: "xs",
+            underline: true,
+        }),
+    }
 }

styles/src/styleTree/hoverPopover.ts 🔗

@@ -1,45 +1,45 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function HoverPopover(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  let baseContainer = {
-    background: background(layer),
-    cornerRadius: 8,
-    padding: {
-      left: 8,
-      right: 8,
-      top: 4,
-      bottom: 4,
-    },
-    shadow: colorScheme.popoverShadow,
-    border: border(layer),
-    margin: {
-      left: -8,
-    },
-  };
+    let layer = colorScheme.middle
+    let baseContainer = {
+        background: background(layer),
+        cornerRadius: 8,
+        padding: {
+            left: 8,
+            right: 8,
+            top: 4,
+            bottom: 4,
+        },
+        shadow: colorScheme.popoverShadow,
+        border: border(layer),
+        margin: {
+            left: -8,
+        },
+    }
 
-  return {
-    container: baseContainer,
-    infoContainer: {
-      ...baseContainer,
-      background: background(layer, "accent"),
-      border: border(layer, "accent"),
-    },
-    warningContainer: {
-      ...baseContainer,
-      background: background(layer, "warning"),
-      border: border(layer, "warning"),
-    },
-    errorContainer: {
-      ...baseContainer,
-      background: background(layer, "negative"),
-      border: border(layer, "negative"),
-    },
-    block_style: {
-      padding: { top: 4 },
-    },
-    prose: text(layer, "sans", { size: "sm" }),
-    highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
-  };
+    return {
+        container: baseContainer,
+        infoContainer: {
+            ...baseContainer,
+            background: background(layer, "accent"),
+            border: border(layer, "accent"),
+        },
+        warningContainer: {
+            ...baseContainer,
+            background: background(layer, "warning"),
+            border: border(layer, "warning"),
+        },
+        errorContainer: {
+            ...baseContainer,
+            background: background(layer, "negative"),
+            border: border(layer, "negative"),
+        },
+        block_style: {
+            padding: { top: 4 },
+        },
+        prose: text(layer, "sans", { size: "sm" }),
+        highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
+    }
 }

styles/src/styleTree/incomingCallNotification.ts 🔗

@@ -1,45 +1,53 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
-export default function incomingCallNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.middle;
-  const avatarSize = 48;
-  return {
-    windowHeight: 74,
-    windowWidth: 380,
-    background: background(layer),
-    callerContainer: {
-      padding: 12,
-    },
-    callerAvatar: {
-      height: avatarSize,
-      width: avatarSize,
-      cornerRadius: avatarSize / 2,
-    },
-    callerMetadata: {
-      margin: { left: 10 },
-    },
-    callerUsername: {
-      ...text(layer, "sans", { size: "sm", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    callerMessage: {
-      ...text(layer, "sans", "variant", { size: "xs" }),
-      margin: { top: -3 },
-    },
-    worktreeRoots: {
-      ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    buttonWidth: 96,
-    acceptButton: {
-      background: background(layer, "accent"),
-      border: border(layer, { left: true, bottom: true }),
-      ...text(layer, "sans", "positive", { size: "xs", weight: "extra_bold" })
-    },
-    declineButton: {
-      border: border(layer, { left: true }),
-      ...text(layer, "sans", "negative", { size: "xs", weight: "extra_bold" })
-    },
-  };
+export default function incomingCallNotification(
+    colorScheme: ColorScheme
+): Object {
+    let layer = colorScheme.middle
+    const avatarSize = 48
+    return {
+        windowHeight: 74,
+        windowWidth: 380,
+        background: background(layer),
+        callerContainer: {
+            padding: 12,
+        },
+        callerAvatar: {
+            height: avatarSize,
+            width: avatarSize,
+            cornerRadius: avatarSize / 2,
+        },
+        callerMetadata: {
+            margin: { left: 10 },
+        },
+        callerUsername: {
+            ...text(layer, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        callerMessage: {
+            ...text(layer, "sans", "variant", { size: "xs" }),
+            margin: { top: -3 },
+        },
+        worktreeRoots: {
+            ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        buttonWidth: 96,
+        acceptButton: {
+            background: background(layer, "accent"),
+            border: border(layer, { left: true, bottom: true }),
+            ...text(layer, "sans", "positive", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+        declineButton: {
+            border: border(layer, { left: true }),
+            ...text(layer, "sans", "negative", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+    }
 }

styles/src/styleTree/picker.ts 🔗

@@ -1,78 +1,82 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, text } from "./components"
 
 export default function picker(colorScheme: ColorScheme) {
-  let layer = colorScheme.lowest;
-  const container = {
-    background: background(layer),
-    border: border(layer),
-    shadow: colorScheme.modalShadow,
-    cornerRadius: 12,
-    padding: {
-      bottom: 4,
+    let layer = colorScheme.lowest
+    const container = {
+        background: background(layer),
+        border: border(layer),
+        shadow: colorScheme.modalShadow,
+        cornerRadius: 12,
+        padding: {
+            bottom: 4,
+        },
     }
-  };
-  const inputEditor = {
-    placeholderText: text(layer, "sans", "on", "disabled"),
-    selection: colorScheme.players[0],
-    text: text(layer, "mono", "on"),
-    border: border(layer, { bottom: true }),
-    padding: {
-      bottom: 8,
-      left: 16,
-      right: 16,
-      top: 8,
-    },
-    margin: {
-      bottom: 4,
-    },
-  };
-  const emptyInputEditor = { ...inputEditor };
-  delete emptyInputEditor.border;
-  delete emptyInputEditor.margin;
+    const inputEditor = {
+        placeholderText: text(layer, "sans", "on", "disabled"),
+        selection: colorScheme.players[0],
+        text: text(layer, "mono", "on"),
+        border: border(layer, { bottom: true }),
+        padding: {
+            bottom: 8,
+            left: 16,
+            right: 16,
+            top: 8,
+        },
+        margin: {
+            bottom: 4,
+        },
+    }
+    const emptyInputEditor = { ...inputEditor }
+    delete emptyInputEditor.border
+    delete emptyInputEditor.margin
 
-  return {
-    ...container,
-    emptyContainer: {
-      ...container,
-      padding: {}
-    },
-    item: {
-      padding: {
-        bottom: 4,
-        left: 12,
-        right: 12,
-        top: 4,
-      },
-      margin: {
-        top: 1,
-        left: 4,
-        right: 4,
-      },
-      cornerRadius: 8,
-      text: text(layer, "sans", "variant"),
-      highlightText: text(layer, "sans", "accent", { weight: "bold" }),
-      active: {
-        background: background(layer, "base", "active"),
-        text: text(layer, "sans", "base", "active"),
-        highlightText: text(layer, "sans", "accent", {
-          weight: "bold",
-        }),
-      },
-      hover: {
-        background: background(layer, "hovered"),
-      },
-    },
-    inputEditor,
-    emptyInputEditor,
-    noMatches: {
-      text: text(layer, "sans", "variant"),
-      padding: {
-        bottom: 8,
-        left: 16,
-        right: 16,
-        top: 8,
-      },
-    },
-  };
+    return {
+        ...container,
+        emptyContainer: {
+            ...container,
+            padding: {},
+        },
+        item: {
+            padding: {
+                bottom: 4,
+                left: 12,
+                right: 12,
+                top: 4,
+            },
+            margin: {
+                top: 1,
+                left: 4,
+                right: 4,
+            },
+            cornerRadius: 8,
+            text: text(layer, "sans", "variant"),
+            highlightText: text(layer, "sans", "accent", { weight: "bold" }),
+            active: {
+                background: withOpacity(
+                    background(layer, "base", "active"),
+                    0.5
+                ),
+                text: text(layer, "sans", "base", "active"),
+                highlightText: text(layer, "sans", "accent", {
+                    weight: "bold",
+                }),
+            },
+            hover: {
+                background: withOpacity(background(layer, "hovered"), 0.5),
+            },
+        },
+        inputEditor,
+        emptyInputEditor,
+        noMatches: {
+            text: text(layer, "sans", "variant"),
+            padding: {
+                bottom: 8,
+                left: 16,
+                right: 16,
+                top: 8,
+            },
+        },
+    }
 }

styles/src/styleTree/projectDiagnostics.ts 🔗

@@ -1,13 +1,13 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, text } from "./components"
 
 export default function projectDiagnostics(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
-  return {
-    background: background(layer),
-    tabIconSpacing: 4,
-    tabIconWidth: 13,
-    tabSummarySpacing: 10,
-    emptyMessage: text(layer, "sans", "variant", { size: "md" }),
-  };
+    let layer = colorScheme.highest
+    return {
+        background: background(layer),
+        tabIconSpacing: 4,
+        tabIconWidth: 13,
+        tabSummarySpacing: 10,
+        emptyMessage: text(layer, "sans", "variant", { size: "md" }),
+    }
 }

styles/src/styleTree/projectPanel.ts 🔗

@@ -1,60 +1,84 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { background, border, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, foreground, text } from "./components"
 
 export default function projectPanel(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  
-  let baseEntry = {
-    height: 24,
-    iconColor: foreground(layer, "variant"),
-    iconSize: 8,
-    iconSpacing: 8,
-  }
+    let layer = colorScheme.middle
 
-  let entry = {
-    ...baseEntry,
-    text: text(layer, "mono", "variant", { size: "sm" }),
-    hover: {
-      background: background(layer, "variant", "hovered"),
-    },
-    active: {
-      background: background(layer, "active"),
-      text: text(layer, "mono", "active", { size: "sm" }),
-    },
-    activeHover: {
-      background: background(layer, "active"),
-      text: text(layer, "mono", "active", { size: "sm" }),
-    },
-  };
+    let baseEntry = {
+        height: 24,
+        iconColor: foreground(layer, "variant"),
+        iconSize: 8,
+        iconSpacing: 8,
+    }
 
-  return {
-    background: background(layer),
-    padding: { left: 12, right: 12, top: 6, bottom: 6 },
-    indentWidth: 8,
-    entry,
-    draggedEntry: {
-      ...baseEntry,
-      text: text(layer, "mono", "on", { size: "sm" }),
-      background: withOpacity(background(layer, "on"), 0.9),
-      border: border(layer),
-    },
-    ignoredEntry: {
-      ...entry,
-      text: text(layer, "mono", "disabled"),
-    },
-    cutEntry: {
-      ...entry,
-      text: text(layer, "mono", "disabled"),
-      active: {
-        background: background(layer, "active"),
-        text: text(layer, "mono", "disabled", { size: "sm" }),
-      },
-    },
-    filenameEditor: {
-      background: background(layer, "on"),
-      text: text(layer, "mono", "on", { size: "sm" }),
-      selection: colorScheme.players[0],
-    },
-  };
+    let entry = {
+        ...baseEntry,
+        text: text(layer, "mono", "variant", { size: "sm" }),
+        hover: {
+            background: background(layer, "variant", "hovered"),
+        },
+        active: {
+            background: colorScheme.isLight
+                ? withOpacity(background(layer, "active"), 0.5)
+                : background(layer, "active"),
+            text: text(layer, "mono", "active", { size: "sm" }),
+        },
+        activeHover: {
+            background: background(layer, "active"),
+            text: text(layer, "mono", "active", { size: "sm" }),
+        },
+    }
+
+    return {
+        openProjectButton: {
+            background: background(layer),
+            border: border(layer, "active"),
+            cornerRadius: 4,
+            margin: {
+                top: 16,
+                left: 16,
+                right: 16,
+            },
+            padding: {
+                top: 3,
+                bottom: 3,
+                left: 7,
+                right: 7,
+            },
+            ...text(layer, "sans", "default", { size: "sm" }),
+            hover: {
+                ...text(layer, "sans", "default", { size: "sm" }),
+                background: background(layer, "hovered"),
+                border: border(layer, "active"),
+            },
+        },
+        background: background(layer),
+        padding: { left: 12, right: 12, top: 6, bottom: 6 },
+        indentWidth: 8,
+        entry,
+        draggedEntry: {
+            ...baseEntry,
+            text: text(layer, "mono", "on", { size: "sm" }),
+            background: withOpacity(background(layer, "on"), 0.9),
+            border: border(layer),
+        },
+        ignoredEntry: {
+            ...entry,
+            text: text(layer, "mono", "disabled"),
+        },
+        cutEntry: {
+            ...entry,
+            text: text(layer, "mono", "disabled"),
+            active: {
+                background: background(layer, "active"),
+                text: text(layer, "mono", "disabled", { size: "sm" }),
+            },
+        },
+        filenameEditor: {
+            background: background(layer, "on"),
+            text: text(layer, "mono", "on", { size: "sm" }),
+            selection: colorScheme.players[0],
+        },
+    }
 }

styles/src/styleTree/projectSharedNotification.ts 🔗

@@ -1,46 +1,54 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
-export default function projectSharedNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.middle;
+export default function projectSharedNotification(
+    colorScheme: ColorScheme
+): Object {
+    let layer = colorScheme.middle
 
-  const avatarSize = 48;
-  return {
-    windowHeight: 74,
-    windowWidth: 380,
-    background: background(layer),
-    ownerContainer: {
-      padding: 12,
-    },
-    ownerAvatar: {
-      height: avatarSize,
-      width: avatarSize,
-      cornerRadius: avatarSize / 2,
-    },
-    ownerMetadata: {
-      margin: { left: 10 },
-    },
-    ownerUsername: {
-      ...text(layer, "sans", { size: "sm", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    message: {
-      ...text(layer, "sans", "variant", { size: "xs" }),
-      margin: { top: -3 },
-    },
-    worktreeRoots: {
-      ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
-      margin: { top: -3 },
-    },
-    buttonWidth: 96,
-    openButton: {
-      background: background(layer, "accent"),
-      border: border(layer, { left: true, bottom: true, }),
-      ...text(layer, "sans", "accent", { size: "xs", weight: "extra_bold" })
-    },
-    dismissButton: {
-      border: border(layer, { left: true }),
-      ...text(layer, "sans", "variant", { size: "xs", weight: "extra_bold" })
-    },
-  };
+    const avatarSize = 48
+    return {
+        windowHeight: 74,
+        windowWidth: 380,
+        background: background(layer),
+        ownerContainer: {
+            padding: 12,
+        },
+        ownerAvatar: {
+            height: avatarSize,
+            width: avatarSize,
+            cornerRadius: avatarSize / 2,
+        },
+        ownerMetadata: {
+            margin: { left: 10 },
+        },
+        ownerUsername: {
+            ...text(layer, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        message: {
+            ...text(layer, "sans", "variant", { size: "xs" }),
+            margin: { top: -3 },
+        },
+        worktreeRoots: {
+            ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        buttonWidth: 96,
+        openButton: {
+            background: background(layer, "accent"),
+            border: border(layer, { left: true, bottom: true }),
+            ...text(layer, "sans", "accent", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+        dismissButton: {
+            border: border(layer, { left: true }),
+            ...text(layer, "sans", "variant", {
+                size: "xs",
+                weight: "extra_bold",
+            }),
+        },
+    }
 }

styles/src/styleTree/search.ts 🔗

@@ -1,96 +1,94 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { background, border, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, foreground, text } from "./components"
 
 export default function search(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
+    let layer = colorScheme.highest
 
-  // Search input
-  const editor = {
-    background: background(layer),
-    cornerRadius: 8,
-    minWidth: 200,
-    maxWidth: 500,
-    placeholderText: text(layer, "mono", "disabled"),
-    selection: colorScheme.players[0],
-    text: text(layer, "mono", "default"),
-    border: border(layer),
-    margin: {
-      right: 12,
-    },
-    padding: {
-      top: 3,
-      bottom: 3,
-      left: 12,
-      right: 8,
-    },
-  };
+    // Search input
+    const editor = {
+        background: background(layer),
+        cornerRadius: 8,
+        minWidth: 200,
+        maxWidth: 500,
+        placeholderText: text(layer, "mono", "disabled"),
+        selection: colorScheme.players[0],
+        text: text(layer, "mono", "default"),
+        border: border(layer),
+        margin: {
+            right: 12,
+        },
+        padding: {
+            top: 3,
+            bottom: 3,
+            left: 12,
+            right: 8,
+        },
+    }
 
-  return {
-    // TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
-    matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
-    tabIconSpacing: 8,
-    tabIconWidth: 14,
-    optionButton: {
-      ...text(layer, "mono", "on"),
-      background: background(layer, "on"),
-      cornerRadius: 6,
-      border: border(layer, "on"),
-      margin: {
-        right: 4,
-      },
-      padding: {
-        bottom: 2,
-        left: 10,
-        right: 10,
-        top: 2,
-      },
-      active: {
-        ...text(layer, "mono", "on", "inverted"),
-        background: background(layer, "on", "inverted"),
-        border: border(layer, "on", "inverted"),
-      },
-      clicked: {
-        ...text(layer, "mono", "on", "pressed"),
-        background: background(layer, "on", "pressed"),
-        border: border(layer, "on", "pressed"),
-      },
-      hover: {
-        ...text(layer, "mono", "on", "hovered"),
-        background: background(layer, "on", "hovered"),
-        border: border(layer, "on", "hovered"),
-      },
-    },
-    editor,
-    invalidEditor: {
-      ...editor,
-      border: border(layer, "negative"),
-    },
-    matchIndex: {
-      ...text(layer, "mono", "variant"),
-      padding: 6,
-    },
-    optionButtonGroup: {
-      padding: {
-        left: 12,
-        right: 12,
-      },
-    },
-    resultsStatus: {
-      ...text(layer, "mono", "on"),
-      size: 18,
-    },
-    dismissButton: {
-      color: foreground(layer, "variant"),
-      iconWidth: 12,
-      buttonWidth: 14,
-      padding: {
-        left: 10,
-        right: 10,
-      },
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-  };
+    return {
+        // TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
+        matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
+        optionButton: {
+            ...text(layer, "mono", "on"),
+            background: background(layer, "on"),
+            cornerRadius: 6,
+            border: border(layer, "on"),
+            margin: {
+                right: 4,
+            },
+            padding: {
+                bottom: 2,
+                left: 10,
+                right: 10,
+                top: 2,
+            },
+            active: {
+                ...text(layer, "mono", "on", "inverted"),
+                background: background(layer, "on", "inverted"),
+                border: border(layer, "on", "inverted"),
+            },
+            clicked: {
+                ...text(layer, "mono", "on", "pressed"),
+                background: background(layer, "on", "pressed"),
+                border: border(layer, "on", "pressed"),
+            },
+            hover: {
+                ...text(layer, "mono", "on", "hovered"),
+                background: background(layer, "on", "hovered"),
+                border: border(layer, "on", "hovered"),
+            },
+        },
+        editor,
+        invalidEditor: {
+            ...editor,
+            border: border(layer, "negative"),
+        },
+        matchIndex: {
+            ...text(layer, "mono", "variant"),
+            padding: 6,
+        },
+        optionButtonGroup: {
+            padding: {
+                left: 12,
+                right: 12,
+            },
+        },
+        resultsStatus: {
+            ...text(layer, "mono", "on"),
+            size: 18,
+        },
+        dismissButton: {
+            color: foreground(layer, "variant"),
+            iconWidth: 12,
+            buttonWidth: 14,
+            padding: {
+                left: 10,
+                right: 10,
+            },
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+    }
 }

styles/src/styleTree/sharedScreen.ts 🔗

@@ -1,9 +1,9 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background } from "./components"
 
 export default function sharedScreen(colorScheme: ColorScheme) {
-  let layer = colorScheme.highest;
-  return {
-    background: background(layer)
-  }
+    let layer = colorScheme.highest
+    return {
+        background: background(layer),
+    }
 }

styles/src/styleTree/simpleMessageNotification.ts 🔗

@@ -1,31 +1,45 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, foreground, text } from "./components"
 
-const headerPadding = 8;
+const headerPadding = 8
 
-export default function simpleMessageNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.middle;
-  return {
-    message: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, right: headerPadding },
-    },
-    actionMessage: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, top: 6, bottom: 6 },
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-    dismissButton: {
-      color: foreground(layer),
-      iconWidth: 8,
-      iconHeight: 8,
-      buttonWidth: 8,
-      buttonHeight: 8,
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-  };
+export default function simpleMessageNotification(
+    colorScheme: ColorScheme
+): Object {
+    let layer = colorScheme.middle
+    return {
+        message: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: headerPadding, right: headerPadding },
+        },
+        actionMessage: {
+            ...text(layer, "sans", { size: "xs" }),
+            border: border(layer, "active"),
+            cornerRadius: 4,
+            padding: {
+                top: 3,
+                bottom: 3,
+                left: 7,
+                right: 7,
+            },
+
+
+            margin: { left: headerPadding, top: 6, bottom: 6 },
+            hover: {
+                ...text(layer, "sans", "default", { size: "xs" }),
+                background: background(layer, "hovered"),
+                border: border(layer, "active"),
+            },
+        },
+        dismissButton: {
+            color: foreground(layer),
+            iconWidth: 8,
+            iconHeight: 8,
+            buttonWidth: 8,
+            buttonHeight: 8,
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+    }
 }

styles/src/styleTree/statusBar.ts 🔗

@@ -1,118 +1,125 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, foreground, text } from "./components"
 
 export default function statusBar(colorScheme: ColorScheme) {
-  let layer = colorScheme.lowest;
+    let layer = colorScheme.lowest
 
-  const statusContainer = {
-    cornerRadius: 6,
-    padding: { top: 3, bottom: 3, left: 6, right: 6 },
-  };
-
-  const diagnosticStatusContainer = {
-    cornerRadius: 6,
-    padding: { top: 1, bottom: 1, left: 6, right: 6 },
-  };
+    const statusContainer = {
+        cornerRadius: 6,
+        padding: { top: 3, bottom: 3, left: 6, right: 6 },
+    }
 
-  return {
-    height: 30,
-    itemSpacing: 8,
-    padding: {
-      top: 1,
-      bottom: 1,
-      left: 6,
-      right: 6,
-    },
-    border: border(layer, { top: true, overlay: true }),
-    cursorPosition: text(layer, "sans", "variant"),
-    autoUpdateProgressMessage: text(layer, "sans", "variant"),
-    autoUpdateDoneMessage: text(layer, "sans", "variant"),
-    lspStatus: {
-      ...diagnosticStatusContainer,
-      iconSpacing: 4,
-      iconWidth: 14,
-      height: 18,
-      message: text(layer, "sans"),
-      iconColor: foreground(layer),
-      hover: {
-        message: text(layer, "sans"),
-        iconColor: foreground(layer),
-        background: background(layer),
-      },
-    },
-    diagnosticMessage: {
-      ...text(layer, "sans"),
-      hover: text(layer, "sans", "hovered"),
-    },
-    feedback: {
-      ...text(layer, "sans", "variant"),
-      hover: text(layer, "sans", "hovered"),
-    },
-    diagnosticSummary: {
-      height: 20,
-      iconWidth: 16,
-      iconSpacing: 2,
-      summarySpacing: 6,
-      text: text(layer, "sans", { size: "sm" }),
-      iconColorOk: foreground(layer, "variant"),
-      iconColorWarning: foreground(layer, "warning"),
-      iconColorError: foreground(layer, "negative"),
-      containerOk: {
+    const diagnosticStatusContainer = {
         cornerRadius: 6,
-        padding: { top: 3, bottom: 3, left: 7, right: 7 },
-      },
-      containerWarning: {
-        ...diagnosticStatusContainer,
-        background: background(layer, "warning"),
-        border: border(layer, "warning"),
-      },
-      containerError: {
-        ...diagnosticStatusContainer,
-        background: background(layer, "negative"),
-        border: border(layer, "negative"),
-      },
-      hover: {
-        iconColorOk: foreground(layer, "on"),
-        containerOk: {
-          cornerRadius: 6,
-          padding: { top: 3, bottom: 3, left: 7, right: 7 },
-          background: background(layer, "on", "hovered"),
+        padding: { top: 1, bottom: 1, left: 6, right: 6 },
+    }
+
+    return {
+        height: 30,
+        itemSpacing: 8,
+        padding: {
+            top: 1,
+            bottom: 1,
+            left: 6,
+            right: 6,
+        },
+        border: border(layer, { top: true, overlay: true }),
+        cursorPosition: text(layer, "sans", "variant"),
+        activeLanguage: {
+            padding: { left: 6, right: 6 },
+            ...text(layer, "sans", "variant"),
+            hover: {
+                ...text(layer, "sans", "on"),
+            },
         },
-        containerWarning: {
-          ...diagnosticStatusContainer,
-          background: background(layer, "warning", "hovered"),
-          border: border(layer, "warning", "hovered"),
+        autoUpdateProgressMessage: text(layer, "sans", "variant"),
+        autoUpdateDoneMessage: text(layer, "sans", "variant"),
+        lspStatus: {
+            ...diagnosticStatusContainer,
+            iconSpacing: 4,
+            iconWidth: 14,
+            height: 18,
+            message: text(layer, "sans"),
+            iconColor: foreground(layer),
+            hover: {
+                message: text(layer, "sans"),
+                iconColor: foreground(layer),
+                background: background(layer, "hovered"),
+            },
         },
-        containerError: {
-          ...diagnosticStatusContainer,
-          background: background(layer, "negative", "hovered"),
-          border: border(layer, "negative", "hovered"),
+        diagnosticMessage: {
+            ...text(layer, "sans"),
+            hover: text(layer, "sans", "hovered"),
         },
-      },
-    },
-    sidebarButtons: {
-      groupLeft: {},
-      groupRight: {},
-      item: {
-        ...statusContainer,
-        iconSize: 16,
-        iconColor: foreground(layer, "variant"),
-        hover: {
-          iconColor: foreground(layer, "hovered"),
-          background: background(layer, "variant"),
+        diagnosticSummary: {
+            height: 20,
+            iconWidth: 16,
+            iconSpacing: 2,
+            summarySpacing: 6,
+            text: text(layer, "sans", { size: "sm" }),
+            iconColorOk: foreground(layer, "variant"),
+            iconColorWarning: foreground(layer, "warning"),
+            iconColorError: foreground(layer, "negative"),
+            containerOk: {
+                cornerRadius: 6,
+                padding: { top: 3, bottom: 3, left: 7, right: 7 },
+            },
+            containerWarning: {
+                ...diagnosticStatusContainer,
+                background: background(layer, "warning"),
+                border: border(layer, "warning"),
+            },
+            containerError: {
+                ...diagnosticStatusContainer,
+                background: background(layer, "negative"),
+                border: border(layer, "negative"),
+            },
+            hover: {
+                iconColorOk: foreground(layer, "on"),
+                containerOk: {
+                    cornerRadius: 6,
+                    padding: { top: 3, bottom: 3, left: 7, right: 7 },
+                    background: background(layer, "on", "hovered"),
+                },
+                containerWarning: {
+                    ...diagnosticStatusContainer,
+                    background: background(layer, "warning", "hovered"),
+                    border: border(layer, "warning", "hovered"),
+                },
+                containerError: {
+                    ...diagnosticStatusContainer,
+                    background: background(layer, "negative", "hovered"),
+                    border: border(layer, "negative", "hovered"),
+                },
+            },
         },
-        active: {
-          iconColor: foreground(layer, "active"),
-          background: background(layer, "active"),
+        sidebarButtons: {
+            groupLeft: {},
+            groupRight: {},
+            item: {
+                ...statusContainer,
+                iconSize: 16,
+                iconColor: foreground(layer, "variant"),
+                label: {
+                    margin: { left: 6 },
+                    ...text(layer, "sans", { size: "sm" }),
+                },
+                hover: {
+                    iconColor: foreground(layer, "hovered"),
+                    background: background(layer, "variant"),
+                },
+                active: {
+                    iconColor: foreground(layer, "active"),
+                    background: background(layer, "active"),
+                },
+            },
+            badge: {
+                cornerRadius: 3,
+                padding: 2,
+                margin: { bottom: -1, right: -1 },
+                border: border(layer),
+                background: background(layer, "accent"),
+            },
         },
-      },
-      badge: {
-        cornerRadius: 3,
-        padding: 2,
-        margin: { bottom: -1, right: -1 },
-        border: border(layer),
-        background: background(layer, "accent"),
-      },
-    },
-  };
+    }
 }

styles/src/styleTree/tabBar.ts 🔗

@@ -1,103 +1,106 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import { text, border, background, foreground } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { text, border, background, foreground } from "./components"
 
 export default function tabBar(colorScheme: ColorScheme) {
-  const height = 32;
+    const height = 32
 
-  let activeLayer = colorScheme.highest;
-  let layer = colorScheme.middle;
+    let activeLayer = colorScheme.highest
+    let layer = colorScheme.middle
 
-  const tab = {
-    height,
-    text: text(layer, "sans", "variant", { size: "sm" }),
-    background: background(layer),
-    border: border(layer, {
-      right: true,
-      bottom: true,
-      overlay: true,
-    }),
-    padding: {
-      left: 8,
-      right: 12,
-    },
-    spacing: 8,
+    const tab = {
+        height,
+        text: text(layer, "sans", "variant", { size: "sm" }),
+        background: background(layer),
+        border: border(layer, {
+            right: true,
+            bottom: true,
+            overlay: true,
+        }),
+        padding: {
+            left: 8,
+            right: 12,
+        },
+        spacing: 8,
 
-    // Close icons
-    iconWidth: 8,
-    iconClose: foreground(layer, "variant"),
-    iconCloseActive: foreground(layer, "hovered"),
+        // Tab type icons (e.g. Project Search)
+        typeIconWidth: 14,
 
-    // Indicators
-    iconConflict: foreground(layer, "warning"),
-    iconDirty: foreground(layer, "accent"),
+        // Close icons
+        closeIconWidth: 8,
+        iconClose: foreground(layer, "variant"),
+        iconCloseActive: foreground(layer, "hovered"),
 
-    // When two tabs of the same name are open, a label appears next to them
-    description: {
-      margin: { left: 8 },
-      ...text(layer, "sans", "disabled", { size: "2xs" }),
-    },
-  };
+        // Indicators
+        iconConflict: foreground(layer, "warning"),
+        iconDirty: foreground(layer, "accent"),
 
-  const activePaneActiveTab = {
-    ...tab,
-    background: background(activeLayer),
-    text: text(activeLayer, "sans", "active", { size: "sm" }),
-    border: {
-      ...tab.border,
-      bottom: false,
-    },
-  };
+        // When two tabs of the same name are open, a label appears next to them
+        description: {
+            margin: { left: 8 },
+            ...text(layer, "sans", "disabled", { size: "2xs" }),
+        },
+    }
 
-  const inactivePaneInactiveTab = {
-    ...tab,
-    background: background(layer),
-    text: text(layer, "sans", "variant", { size: "sm" }),
-  };
+    const activePaneActiveTab = {
+        ...tab,
+        background: background(activeLayer),
+        text: text(activeLayer, "sans", "active", { size: "sm" }),
+        border: {
+            ...tab.border,
+            bottom: false,
+        },
+    }
 
-  const inactivePaneActiveTab = {
-    ...tab,
-    background: background(activeLayer),
-    text: text(layer, "sans", "variant", { size: "sm" }),
-    border: {
-      ...tab.border,
-      bottom: false,
-    },
-  };
+    const inactivePaneInactiveTab = {
+        ...tab,
+        background: background(layer),
+        text: text(layer, "sans", "variant", { size: "sm" }),
+    }
 
-  const draggedTab = {
-    ...activePaneActiveTab,
-    background: withOpacity(tab.background, 0.9),
-    border: undefined as any,
-    shadow: colorScheme.popoverShadow,
-  };
+    const inactivePaneActiveTab = {
+        ...tab,
+        background: background(activeLayer),
+        text: text(layer, "sans", "variant", { size: "sm" }),
+        border: {
+            ...tab.border,
+            bottom: false,
+        },
+    }
 
-  return {
-    height,
-    background: background(layer),
-    activePane: {
-      activeTab: activePaneActiveTab,
-      inactiveTab: tab,
-    },
-    inactivePane: {
-      activeTab: inactivePaneActiveTab,
-      inactiveTab: inactivePaneInactiveTab,
-    },
-    draggedTab,
-    paneButton: {
-      color: foreground(layer, "variant"),
-      iconWidth: 12,
-      buttonWidth: activePaneActiveTab.height,
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-    paneButtonContainer: {
-      background: tab.background,
-      border: {
-        ...tab.border,
-        right: false,
-      },
-    },
-  };
+    const draggedTab = {
+        ...activePaneActiveTab,
+        background: withOpacity(tab.background, 0.9),
+        border: undefined as any,
+        shadow: colorScheme.popoverShadow,
+    }
+
+    return {
+        height,
+        background: background(layer),
+        activePane: {
+            activeTab: activePaneActiveTab,
+            inactiveTab: tab,
+        },
+        inactivePane: {
+            activeTab: inactivePaneActiveTab,
+            inactiveTab: inactivePaneInactiveTab,
+        },
+        draggedTab,
+        paneButton: {
+            color: foreground(layer, "variant"),
+            iconWidth: 12,
+            buttonWidth: activePaneActiveTab.height,
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+        paneButtonContainer: {
+            background: tab.background,
+            border: {
+                ...tab.border,
+                right: false,
+            },
+        },
+    }
 }

styles/src/styleTree/terminal.ts 🔗

@@ -1,52 +1,52 @@
-import { ColorScheme } from "../themes/common/colorScheme";
+import { ColorScheme } from "../themes/common/colorScheme"
 
 export default function terminal(colorScheme: ColorScheme) {
-  /**
-   * Colors are controlled per-cell in the terminal grid.
-   * Cells can be set to any of these more 'theme-capable' colors
-   * or can be set directly with RGB values.
-   * Here are the common interpretations of these names:
-   * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
-   */
-  return {
-    black: colorScheme.ramps.neutral(0).hex(),
-    red: colorScheme.ramps.red(0.5).hex(),
-    green: colorScheme.ramps.green(0.5).hex(),
-    yellow: colorScheme.ramps.yellow(0.5).hex(),
-    blue: colorScheme.ramps.blue(0.5).hex(),
-    magenta: colorScheme.ramps.magenta(0.5).hex(),
-    cyan: colorScheme.ramps.cyan(0.5).hex(),
-    white: colorScheme.ramps.neutral(1).hex(),
-    brightBlack: colorScheme.ramps.neutral(0.4).hex(),
-    brightRed: colorScheme.ramps.red(0.25).hex(),
-    brightGreen: colorScheme.ramps.green(0.25).hex(),
-    brightYellow: colorScheme.ramps.yellow(0.25).hex(),
-    brightBlue: colorScheme.ramps.blue(0.25).hex(),
-    brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
-    brightCyan: colorScheme.ramps.cyan(0.25).hex(),
-    brightWhite: colorScheme.ramps.neutral(1).hex(),
     /**
-     * Default color for characters
+     * Colors are controlled per-cell in the terminal grid.
+     * Cells can be set to any of these more 'theme-capable' colors
+     * or can be set directly with RGB values.
+     * Here are the common interpretations of these names:
+     * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
      */
-    foreground: colorScheme.ramps.neutral(1).hex(),
-    /**
-     * Default color for the rectangle background of a cell
-     */
-    background: colorScheme.ramps.neutral(0).hex(),
-    modalBackground: colorScheme.ramps.neutral(0.1).hex(),
-    /**
-     * Default color for the cursor
-     */
-    cursor: colorScheme.players[0].cursor,
-    dimBlack: colorScheme.ramps.neutral(1).hex(),
-    dimRed: colorScheme.ramps.red(0.75).hex(),
-    dimGreen: colorScheme.ramps.green(0.75).hex(),
-    dimYellow: colorScheme.ramps.yellow(0.75).hex(),
-    dimBlue: colorScheme.ramps.blue(0.75).hex(),
-    dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
-    dimCyan: colorScheme.ramps.cyan(0.75).hex(),
-    dimWhite: colorScheme.ramps.neutral(0.6).hex(),
-    brightForeground: colorScheme.ramps.neutral(1).hex(),
-    dimForeground: colorScheme.ramps.neutral(0).hex(),
-  };
+    return {
+        black: colorScheme.ramps.neutral(0).hex(),
+        red: colorScheme.ramps.red(0.5).hex(),
+        green: colorScheme.ramps.green(0.5).hex(),
+        yellow: colorScheme.ramps.yellow(0.5).hex(),
+        blue: colorScheme.ramps.blue(0.5).hex(),
+        magenta: colorScheme.ramps.magenta(0.5).hex(),
+        cyan: colorScheme.ramps.cyan(0.5).hex(),
+        white: colorScheme.ramps.neutral(1).hex(),
+        brightBlack: colorScheme.ramps.neutral(0.4).hex(),
+        brightRed: colorScheme.ramps.red(0.25).hex(),
+        brightGreen: colorScheme.ramps.green(0.25).hex(),
+        brightYellow: colorScheme.ramps.yellow(0.25).hex(),
+        brightBlue: colorScheme.ramps.blue(0.25).hex(),
+        brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
+        brightCyan: colorScheme.ramps.cyan(0.25).hex(),
+        brightWhite: colorScheme.ramps.neutral(1).hex(),
+        /**
+         * Default color for characters
+         */
+        foreground: colorScheme.ramps.neutral(1).hex(),
+        /**
+         * Default color for the rectangle background of a cell
+         */
+        background: colorScheme.ramps.neutral(0).hex(),
+        modalBackground: colorScheme.ramps.neutral(0.1).hex(),
+        /**
+         * Default color for the cursor
+         */
+        cursor: colorScheme.players[0].cursor,
+        dimBlack: colorScheme.ramps.neutral(1).hex(),
+        dimRed: colorScheme.ramps.red(0.75).hex(),
+        dimGreen: colorScheme.ramps.green(0.75).hex(),
+        dimYellow: colorScheme.ramps.yellow(0.75).hex(),
+        dimBlue: colorScheme.ramps.blue(0.75).hex(),
+        dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
+        dimCyan: colorScheme.ramps.cyan(0.75).hex(),
+        dimWhite: colorScheme.ramps.neutral(0.6).hex(),
+        brightForeground: colorScheme.ramps.neutral(1).hex(),
+        dimForeground: colorScheme.ramps.neutral(0).hex(),
+    }
 }

styles/src/styleTree/tooltip.ts 🔗

@@ -1,23 +1,23 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { background, border, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { background, border, text } from "./components"
 
 export default function tooltip(colorScheme: ColorScheme) {
-  let layer = colorScheme.middle;
-  return {
-    background: background(layer),
-    border: border(layer),
-    padding: { top: 4, bottom: 4, left: 8, right: 8 },
-    margin: { top: 6, left: 6 },
-    shadow: colorScheme.popoverShadow,
-    cornerRadius: 6,
-    text: text(layer, "sans", { size: "xs" }),
-    keystroke: {
-      background: background(layer, "on"),
-      cornerRadius: 4,
-      margin: { left: 6 },
-      padding: { left: 4, right: 4 },
-      ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
-    },
-    maxTextWidth: 200,
-  };
+    let layer = colorScheme.middle
+    return {
+        background: background(layer),
+        border: border(layer),
+        padding: { top: 4, bottom: 4, left: 8, right: 8 },
+        margin: { top: 6, left: 6 },
+        shadow: colorScheme.popoverShadow,
+        cornerRadius: 6,
+        text: text(layer, "sans", { size: "xs" }),
+        keystroke: {
+            background: background(layer, "on"),
+            cornerRadius: 4,
+            margin: { left: 6 },
+            padding: { left: 4, right: 4 },
+            ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
+        },
+        maxTextWidth: 200,
+    }
 }

styles/src/styleTree/updateNotification.ts 🔗

@@ -1,31 +1,31 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { foreground, text } from "./components";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { foreground, text } from "./components"
 
-const headerPadding = 8;
+const headerPadding = 8
 
 export default function updateNotification(colorScheme: ColorScheme): Object {
-  let layer = colorScheme.middle;
-  return {
-    message: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, right: headerPadding },
-    },
-    actionMessage: {
-      ...text(layer, "sans", { size: "xs" }),
-      margin: { left: headerPadding, top: 6, bottom: 6 },
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-    dismissButton: {
-      color: foreground(layer),
-      iconWidth: 8,
-      iconHeight: 8,
-      buttonWidth: 8,
-      buttonHeight: 8,
-      hover: {
-        color: foreground(layer, "hovered"),
-      },
-    },
-  };
+    let layer = colorScheme.middle
+    return {
+        message: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: headerPadding, right: headerPadding },
+        },
+        actionMessage: {
+            ...text(layer, "sans", { size: "xs" }),
+            margin: { left: headerPadding, top: 6, bottom: 6 },
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+        dismissButton: {
+            color: foreground(layer),
+            iconWidth: 8,
+            iconHeight: 8,
+            buttonWidth: 8,
+            buttonHeight: 8,
+            hover: {
+                color: foreground(layer, "hovered"),
+            },
+        },
+    }
 }

styles/src/styleTree/welcome.ts 🔗

@@ -0,0 +1,129 @@
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import {
+    border,
+    background,
+    foreground,
+    text,
+    TextProperties,
+    svg,
+} from "./components"
+
+export default function welcome(colorScheme: ColorScheme) {
+    let layer = colorScheme.highest
+
+    let checkboxBase = {
+        cornerRadius: 4,
+        padding: {
+            left: 3,
+            right: 3,
+            top: 3,
+            bottom: 3,
+        },
+        // shadow: colorScheme.popoverShadow,
+        border: border(layer),
+        margin: {
+            right: 8,
+            top: 5,
+            bottom: 5,
+        },
+    }
+
+    let interactive_text_size: TextProperties = { size: "sm" }
+
+    return {
+        pageWidth: 320,
+        logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64),
+        logoSubheading: {
+            ...text(layer, "sans", "variant", { size: "md" }),
+            margin: {
+                top: 10,
+                bottom: 7,
+            },
+        },
+        buttonGroup: {
+            margin: {
+                top: 8,
+                bottom: 16,
+            },
+        },
+        headingGroup: {
+            margin: {
+                top: 8,
+                bottom: 12,
+            },
+        },
+        checkboxGroup: {
+            border: border(layer, "variant"),
+            background: withOpacity(background(layer, "hovered"), 0.25),
+            cornerRadius: 4,
+            padding: {
+                left: 12,
+                top: 2,
+                bottom: 2,
+            },
+        },
+        button: {
+            background: background(layer),
+            border: border(layer, "active"),
+            cornerRadius: 4,
+            margin: {
+                top: 4,
+                bottom: 4,
+            },
+            padding: {
+                top: 3,
+                bottom: 3,
+                left: 7,
+                right: 7,
+            },
+            ...text(layer, "sans", "default", interactive_text_size),
+            hover: {
+                ...text(layer, "sans", "default", interactive_text_size),
+                background: background(layer, "hovered"),
+                border: border(layer, "active"),
+            },
+        },
+        usageNote: {
+            ...text(layer, "sans", "variant", { size: "2xs" }),
+            padding: {
+                top: -4,
+            },
+        },
+        checkboxContainer: {
+            margin: {
+                top: 4,
+            },
+            padding: {
+                bottom: 8,
+            },
+        },
+        checkbox: {
+            label: {
+                ...text(layer, "sans", interactive_text_size),
+                // Also supports margin, container, border, etc.
+            },
+            icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12),
+            default: {
+                ...checkboxBase,
+                background: background(layer, "default"),
+                border: border(layer, "active"),
+            },
+            checked: {
+                ...checkboxBase,
+                background: background(layer, "hovered"),
+                border: border(layer, "active"),
+            },
+            hovered: {
+                ...checkboxBase,
+                background: background(layer, "hovered"),
+                border: border(layer, "active"),
+            },
+            hoveredAndChecked: {
+                ...checkboxBase,
+                background: background(layer, "hovered"),
+                border: border(layer, "active"),
+            },
+        },
+    }
+}

styles/src/styleTree/workspace.ts 🔗

@@ -1,235 +1,314 @@
-import { ColorScheme } from "../themes/common/colorScheme";
-import { withOpacity } from "../utils/color";
-import {
-  background,
-  border,
-  borderColor,
-  foreground,
-  text,
-} from "./components";
-import statusBar from "./statusBar";
-import tabBar from "./tabBar";
+import { ColorScheme } from "../themes/common/colorScheme"
+import { withOpacity } from "../utils/color"
+import { background, border, borderColor, foreground, svg, text } from "./components"
+import statusBar from "./statusBar"
+import tabBar from "./tabBar"
 
 export default function workspace(colorScheme: ColorScheme) {
-  const layer = colorScheme.lowest;
-  const titlebarPadding = 6;
-  const titlebarButton = {
-    cornerRadius: 6,
-    padding: {
-      top: 1,
-      bottom: 1,
-      left: 8,
-      right: 8,
-    },
-    ...text(layer, "sans", "variant", { size: "xs" }),
-    background: background(layer, "variant"),
-    border: border(layer),
-    hover: {
-      ...text(layer, "sans", "variant", "hovered", { size: "xs" }),
-      background: background(layer, "variant", "hovered"),
-      border: border(layer, "variant", "hovered"),
-    },
-  };
-  const avatarWidth = 18;
+    const layer = colorScheme.lowest
+    const itemSpacing = 8
+    const titlebarButton = {
+        cornerRadius: 6,
+        padding: {
+            top: 1,
+            bottom: 1,
+            left: 8,
+            right: 8,
+        },
+        ...text(layer, "sans", "variant", { size: "xs" }),
+        background: background(layer, "variant"),
+        border: border(layer),
+        hover: {
+            ...text(layer, "sans", "variant", "hovered", { size: "xs" }),
+            background: background(layer, "variant", "hovered"),
+            border: border(layer, "variant", "hovered"),
+        },
+        clicked: {
+            ...text(layer, "sans", "variant", "pressed", { size: "xs" }),
+            background: background(layer, "variant", "pressed"),
+            border: border(layer, "variant", "pressed"),
+        },
+        active: {
+            ...text(layer, "sans", "variant", "active", { size: "xs" }),
+            background: background(layer, "variant", "active"),
+            border: border(layer, "variant", "active"),
+        },
+    }
+    const avatarWidth = 18
+    const avatarOuterWidth = avatarWidth + 4
+    const followerAvatarWidth = 14
+    const followerAvatarOuterWidth = followerAvatarWidth + 4
 
-  return {
-    background: background(layer),
-    joiningProjectAvatar: {
-      cornerRadius: 40,
-      width: 80,
-    },
-    joiningProjectMessage: {
-      padding: 12,
-      ...text(layer, "sans", { size: "lg" }),
-    },
-    externalLocationMessage: {
-      background: background(colorScheme.middle, "accent"),
-      border: border(colorScheme.middle, "accent"),
-      cornerRadius: 6,
-      padding: 12,
-      margin: { bottom: 8, right: 8 },
-      ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
-    },
-    leaderBorderOpacity: 0.7,
-    leaderBorderWidth: 2.0,
-    tabBar: tabBar(colorScheme),
-    modal: {
-      margin: {
-        bottom: 52,
-        top: 52,
-      },
-      cursor: "Arrow",
-    },
-    sidebar: {
-      initialSize: 240,
-      border: border(layer, { left: true, right: true }),
-    },
-    paneDivider: {
-      color: borderColor(layer),
-      width: 1,
-    },
-    statusBar: statusBar(colorScheme),
-    titlebar: {
-      avatarWidth,
-      avatarMargin: 8,
-      height: 33, // 32px + 1px for overlaid border
-      background: background(layer),
-      border: border(layer, { bottom: true, overlay: true }),
-      padding: {
-        left: 80,
-        right: titlebarPadding,
-      },
+    return {
+        background: background(colorScheme.lowest),
+        blankPane: {
+            logoContainer: {
+                width: 256,
+                height: 256,
+            },
+            logo: svg(withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), "icons/logo_96.svg", 256, 256),
 
-      // Project
-      title: text(layer, "sans", "variant"),
+            logoShadow: svg(withOpacity(
+                colorScheme.isLight
+                    ? "#FFFFFF"
+                    : colorScheme.lowest.base.default.background,
+                colorScheme.isLight ? 1 : 0.6
+            ), "icons/logo_96.svg", 256, 256),
+            keyboardHints: {
+                margin: {
+                    top: 96,
+                },
+                cornerRadius: 4,
+            },
+            keyboardHint: {
+                ...text(layer, "sans", "variant", { size: "sm" }),
+                padding: {
+                    top: 3,
+                    left: 8,
+                    right: 8,
+                    bottom: 3,
+                },
+                cornerRadius: 8,
+                hover: {
+                    ...text(layer, "sans", "active", { size: "sm" }),
+                },
+            },
+            keyboardHintWidth: 320,
+        },
+        joiningProjectAvatar: {
+            cornerRadius: 40,
+            width: 80,
+        },
+        joiningProjectMessage: {
+            padding: 12,
+            ...text(layer, "sans", { size: "lg" }),
+        },
+        externalLocationMessage: {
+            background: background(colorScheme.middle, "accent"),
+            border: border(colorScheme.middle, "accent"),
+            cornerRadius: 6,
+            padding: 12,
+            margin: { bottom: 8, right: 8 },
+            ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
+        },
+        leaderBorderOpacity: 0.7,
+        leaderBorderWidth: 2.0,
+        tabBar: tabBar(colorScheme),
+        modal: {
+            margin: {
+                bottom: 52,
+                top: 52,
+            },
+            cursor: "Arrow",
+        },
+        sidebar: {
+            initialSize: 240,
+            border: border(layer, { left: true, right: true }),
+        },
+        paneDivider: {
+            color: borderColor(layer),
+            width: 1,
+        },
+        statusBar: statusBar(colorScheme),
+        titlebar: {
+            itemSpacing,
+            facePileSpacing: 2,
+            height: 33, // 32px + 1px border. It's important the content area of the titlebar is evenly sized to vertically center avatar images.
+            background: background(layer),
+            border: border(layer, { bottom: true }),
+            padding: {
+                left: 80,
+                right: itemSpacing,
+            },
 
-      // Collaborators
-      avatar: {
-        cornerRadius: avatarWidth / 2,
-        border: {
-          color: "#00000088",
-          width: 1,
-        },
-      },
-      inactiveAvatar: {
-        cornerRadius: avatarWidth / 2,
-        border: {
-          color: "#00000088",
-          width: 1,
-        },
-        grayscale: true,
-      },
-      avatarRibbon: {
-        height: 3,
-        width: 12,
-        // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
-      },
+            // Project
+            title: text(layer, "sans", "variant"),
 
-      // Sign in buttom
-      // FlatButton, Variant
-      signInPrompt: {
-        ...titlebarButton
-      },
+            // Collaborators
+            leaderAvatar: {
+                width: avatarWidth,
+                outerWidth: avatarOuterWidth,
+                cornerRadius: avatarWidth / 2,
+                outerCornerRadius: avatarOuterWidth / 2,
+            },
+            followerAvatar: {
+                width: followerAvatarWidth,
+                outerWidth: followerAvatarOuterWidth,
+                cornerRadius: followerAvatarWidth / 2,
+                outerCornerRadius: followerAvatarOuterWidth / 2,
+            },
+            inactiveAvatarGrayscale: true,
+            followerAvatarOverlap: 8,
+            leaderSelection: {
+                margin: {
+                    top: 4,
+                    bottom: 4,
+                },
+                padding: {
+                    left: 2,
+                    right: 2,
+                    top: 2,
+                    bottom: 2,
+                },
+                cornerRadius: 6,
+            },
+            avatarRibbon: {
+                height: 3,
+                width: 12,
+                // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
+            },
 
-      // Offline Indicator
-      offlineIcon: {
-        color: foreground(layer, "variant"),
-        width: 16,
-        margin: {
-          left: titlebarPadding,
-        },
-        padding: {
-          right: 4,
+            // Sign in buttom
+            // FlatButton, Variant
+            signInPrompt: {
+                margin: {
+                    left: itemSpacing,
+                },
+                ...titlebarButton,
+            },
+
+            // Offline Indicator
+            offlineIcon: {
+                color: foreground(layer, "variant"),
+                width: 16,
+                margin: {
+                    left: itemSpacing,
+                },
+                padding: {
+                    right: 4,
+                },
+            },
+
+            // Notice that the collaboration server is out of date
+            outdatedWarning: {
+                ...text(layer, "sans", "warning", { size: "xs" }),
+                background: withOpacity(background(layer, "warning"), 0.3),
+                border: border(layer, "warning"),
+                margin: {
+                    left: itemSpacing,
+                },
+                padding: {
+                    left: 8,
+                    right: 8,
+                },
+                cornerRadius: 6,
+            },
+            callControl: {
+                cornerRadius: 6,
+                color: foreground(layer, "variant"),
+                iconWidth: 12,
+                buttonWidth: 20,
+                hover: {
+                    background: background(layer, "variant", "hovered"),
+                    color: foreground(layer, "variant", "hovered"),
+                },
+            },
+            toggleContactsButton: {
+                margin: { left: itemSpacing },
+                cornerRadius: 6,
+                color: foreground(layer, "variant"),
+                iconWidth: 14,
+                buttonWidth: 20,
+                active: {
+                    background: background(layer, "variant", "active"),
+                    color: foreground(layer, "variant", "active"),
+                },
+                clicked: {
+                    background: background(layer, "variant", "pressed"),
+                    color: foreground(layer, "variant", "pressed"),
+                },
+                hover: {
+                    background: background(layer, "variant", "hovered"),
+                    color: foreground(layer, "variant", "hovered"),
+                },
+            },
+            userMenuButton: {
+                buttonWidth: 20,
+                iconWidth: 12,
+                ...titlebarButton,
+            },
+            toggleContactsBadge: {
+                cornerRadius: 3,
+                padding: 2,
+                margin: { top: 3, left: 3 },
+                border: border(layer),
+                background: foreground(layer, "accent"),
+            },
+            shareButton: {
+                ...titlebarButton,
+            },
         },
-      },
 
-      // Notice that the collaboration server is out of date
-      outdatedWarning: {
-        ...text(layer, "sans", "warning", { size: "xs" }),
-        background: withOpacity(background(layer, "warning"), 0.3),
-        border: border(layer, "warning"),
-        margin: {
-          left: titlebarPadding,
+        toolbar: {
+            height: 34,
+            background: background(colorScheme.highest),
+            border: border(colorScheme.highest, { bottom: true }),
+            itemSpacing: 8,
+            navButton: {
+                color: foreground(colorScheme.highest, "on"),
+                iconWidth: 12,
+                buttonWidth: 24,
+                cornerRadius: 6,
+                hover: {
+                    color: foreground(colorScheme.highest, "on", "hovered"),
+                    background: background(
+                        colorScheme.highest,
+                        "on",
+                        "hovered"
+                    ),
+                },
+                disabled: {
+                    color: foreground(colorScheme.highest, "on", "disabled"),
+                },
+            },
+            padding: { left: 8, right: 8, top: 4, bottom: 4 },
         },
-        padding: {
-          left: 8,
-          right: 8,
+        breadcrumbHeight: 24,
+        breadcrumbs: {
+            ...text(colorScheme.highest, "sans", "variant"),
+            cornerRadius: 6,
+            padding: {
+                left: 6,
+                right: 6,
+            },
+            hover: {
+                color: foreground(colorScheme.highest, "on", "hovered"),
+                background: background(
+                    colorScheme.highest,
+                    "on",
+                    "hovered"
+                ),
+            },
         },
-        cornerRadius: 6,
-      },
-      callControl: {
-        cornerRadius: 6,
-        color: foreground(layer, "variant"),
-        iconWidth: 12,
-        buttonWidth: 20,
-        hover: {
-          background: background(layer, "variant", "hovered"),
-          color: foreground(layer, "variant", "hovered"),
+        disconnectedOverlay: {
+            ...text(layer, "sans"),
+            background: withOpacity(background(layer), 0.8),
         },
-      },
-      toggleContactsButton: {
-        margin: { left: 6 },
-        cornerRadius: 6,
-        color: foreground(layer, "variant"),
-        iconWidth: 8,
-        buttonWidth: 20,
-        active: {
-          background: background(layer, "variant", "active"),
-          color: foreground(layer, "variant", "active"),
+        notification: {
+            margin: { top: 10 },
+            background: background(colorScheme.middle),
+            cornerRadius: 6,
+            padding: 12,
+            border: border(colorScheme.middle),
+            shadow: colorScheme.popoverShadow,
         },
-        hover: {
-          background: background(layer, "variant", "hovered"),
-          color: foreground(layer, "variant", "hovered"),
-        },
-      },
-      toggleContactsBadge: {
-        cornerRadius: 3,
-        padding: 2,
-        margin: { top: 3, left: 3 },
-        border: border(layer),
-        background: foreground(layer, "accent"),
-      },
-      shareButton: {
-        ...titlebarButton
-      }
-    },
-
-    toolbar: {
-      height: 34,
-      background: background(colorScheme.highest),
-      border: border(colorScheme.highest, { bottom: true }),
-      itemSpacing: 8,
-      navButton: {
-        color: foreground(colorScheme.highest, "on"),
-        iconWidth: 12,
-        buttonWidth: 24,
-        cornerRadius: 6,
-        hover: {
-          color: foreground(colorScheme.highest, "on", "hovered"),
-          background: background(colorScheme.highest, "on", "hovered"),
-        },
-        disabled: {
-          color: foreground(colorScheme.highest, "on", "disabled"),
-        },
-      },
-      padding: { left: 8, right: 8, top: 4, bottom: 4 },
-    },
-    breadcrumbs: {
-      ...text(layer, "mono", "variant"),
-      padding: { left: 6 },
-    },
-    disconnectedOverlay: {
-      ...text(layer, "sans"),
-      background: withOpacity(background(layer), 0.8),
-    },
-    notification: {
-      margin: { top: 10 },
-      background: background(colorScheme.middle),
-      cornerRadius: 6,
-      padding: 12,
-      border: border(colorScheme.middle),
-      shadow: colorScheme.popoverShadow,
-    },
-    notifications: {
-      width: 400,
-      margin: { right: 10, bottom: 10 },
-    },
-    dock: {
-      initialSizeRight: 640,
-      initialSizeBottom: 480,
-      wash_color: withOpacity(background(colorScheme.highest), 0.5),
-      panel: {
-        border: border(colorScheme.middle),
-      },
-      maximized: {
-        margin: 32,
-        border: border(colorScheme.highest, { overlay: true }),
-        shadow: colorScheme.modalShadow,
-      },
-    },
-    dropTargetOverlayColor: withOpacity(
-      foreground(layer, "variant"),
-      0.5
-    ),
-  };
+        notifications: {
+            width: 400,
+            margin: { right: 10, bottom: 10 },
+        },
+        dock: {
+            initialSizeRight: 640,
+            initialSizeBottom: 304,
+            wash_color: withOpacity(background(colorScheme.highest), 0.5),
+            panel: {
+                border: border(colorScheme.middle),
+            },
+            maximized: {
+                margin: 32,
+                border: border(colorScheme.highest, { overlay: true }),
+                shadow: colorScheme.modalShadow,
+            },
+        },
+        dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
+    }
 }

styles/src/system/lib/convert.ts 🔗

@@ -0,0 +1,11 @@
+/** Converts a percentage scale value (0-100) to normalized scale (0-1) value. */
+export function percentageToNormalized(value: number) {
+    const normalized = value / 100
+    return normalized
+}
+
+/** Converts a normalized scale (0-1) value to a percentage scale (0-100) value. */
+export function normalizedToPercetage(value: number) {
+    const percentage = value * 100
+    return percentage
+}

styles/src/system/lib/curve.ts 🔗

@@ -0,0 +1,26 @@
+import bezier from "bezier-easing"
+import { Curve } from "../ref/curves"
+
+/**
+ * Formats our Curve data structure into a bezier easing function.
+ * @param {Curve} curve - The curve to format.
+ * @param {Boolean} inverted - Whether or not to invert the curve.
+ * @returns {EasingFunction} The formatted easing function.
+ */
+export function curve(curve: Curve, inverted?: Boolean) {
+    if (inverted) {
+        return bezier(
+            curve.value[3],
+            curve.value[2],
+            curve.value[1],
+            curve.value[0]
+        )
+    }
+
+    return bezier(
+        curve.value[0],
+        curve.value[1],
+        curve.value[2],
+        curve.value[3]
+    )
+}

styles/src/system/lib/generate.ts 🔗

@@ -0,0 +1,159 @@
+import bezier from "bezier-easing"
+import chroma from "chroma-js"
+import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types"
+import { percentageToNormalized } from "./convert"
+import { curve } from "./curve"
+
+// Re-export interface in a more standard format
+export type EasingFunction = bezier.EasingFunction
+
+/**
+ * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata.
+ *
+ * @param {EasingFunction} hueEasing - An easing function for the hue component of the color.
+ * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color.
+ * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color.
+ * @param {ColorFamilyConfig} family - Configuration for the color family.
+ * @param {number} step - The current step.
+ * @param {number} steps - The total number of steps in the color scale.
+ *
+ * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark.
+ */
+function generateColor(
+    hueEasing: EasingFunction,
+    saturationEasing: EasingFunction,
+    lightnessEasing: EasingFunction,
+    family: ColorFamilyConfig,
+    step: number,
+    steps: number
+) {
+    const { hue, saturation, lightness } = family.color
+
+    const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start
+    const stepSaturation =
+        saturationEasing(step / steps) * (saturation.end - saturation.start) +
+        saturation.start
+    const stepLightness =
+        lightnessEasing(step / steps) * (lightness.end - lightness.start) +
+        lightness.start
+
+    const color = chroma.hsl(
+        stepHue,
+        percentageToNormalized(stepSaturation),
+        percentageToNormalized(stepLightness)
+    )
+
+    const contrast = {
+        black: {
+            value: chroma.contrast(color, "black"),
+            aaPass: chroma.contrast(color, "black") >= 4.5,
+            aaaPass: chroma.contrast(color, "black") >= 7,
+        },
+        white: {
+            value: chroma.contrast(color, "white"),
+            aaPass: chroma.contrast(color, "white") >= 4.5,
+            aaaPass: chroma.contrast(color, "white") >= 7,
+        },
+    }
+
+    const lch = color.lch()
+    const rgba = color.rgba()
+    const hex = color.hex()
+
+    // 55 is a magic number. It's the lightness value at which we consider a color to be "light".
+    // It was picked by eye with some testing. We might want to use a more scientific approach in the future.
+    const isLight = lch[0] > 55
+
+    const result: Color = {
+        step,
+        lch,
+        hex,
+        rgba,
+        contrast,
+        isLight,
+    }
+
+    return result
+}
+
+/**
+ * Generates a color scale based on a color family configuration.
+ *
+ * @param {ColorFamilyConfig} config - The configuration for the color family.
+ * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
+ *
+ * @returns {ColorScale} The generated color scale.
+ *
+ * @example
+ * ```ts
+ * const colorScale = generateColorScale({
+ *   name: "blue",
+ *   color: {
+ *     hue: {
+ *       start: 210,
+ *       end: 240,
+ *       curve: "easeInOut"
+ *     },
+ *     saturation: {
+ *       start: 100,
+ *       end: 100,
+ *       curve: "easeInOut"
+ *     },
+ *     lightness: {
+ *       start: 50,
+ *       end: 50,
+ *       curve: "easeInOut"
+ *     }
+ *   }
+ * });
+ * ```
+ */
+
+export function generateColorScale(
+    config: ColorFamilyConfig,
+    inverted: Boolean = false
+) {
+    const { hue, saturation, lightness } = config.color
+
+    // 101 steps means we get values from 0-100
+    const NUM_STEPS = 101
+
+    const hueEasing = curve(hue.curve, inverted)
+    const saturationEasing = curve(saturation.curve, inverted)
+    const lightnessEasing = curve(lightness.curve, inverted)
+
+    let scale: ColorScale = {
+        colors: [],
+        values: [],
+    }
+
+    for (let i = 0; i < NUM_STEPS; i++) {
+        const color = generateColor(
+            hueEasing,
+            saturationEasing,
+            lightnessEasing,
+            config,
+            i,
+            NUM_STEPS
+        )
+
+        scale.colors.push(color)
+        scale.values.push(color.hex)
+    }
+
+    return scale
+}
+
+/** Generates a color family with a scale and an inverted scale. */
+export function generateColorFamily(config: ColorFamilyConfig) {
+    const scale = generateColorScale(config, false)
+    const invertedScale = generateColorScale(config, true)
+
+    const family: ColorFamily = {
+        name: config.name,
+        scale,
+        invertedScale,
+    }
+
+    return family
+}

styles/src/system/ref/color.ts 🔗

@@ -0,0 +1,445 @@
+import { generateColorFamily } from "../lib/generate"
+import { curve } from "./curves"
+
+// These are the source colors for the color scales in the system.
+// These should never directly be used directly in components or themes as they generate thousands of lines of code.
+// Instead, use the outputs from the reference palette which exports a smaller subset of colors.
+
+// Token or user-facing colors should use short, clear names and a 100-900 scale to match the font weight scale.
+
+// Light Gray ======================================== //
+
+export const lightgray = generateColorFamily({
+    name: "lightgray",
+    color: {
+        hue: {
+            start: 210,
+            end: 210,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 10,
+            end: 15,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 50,
+            curve: curve.linear,
+        },
+    },
+})
+
+// Light Dark ======================================== //
+
+export const darkgray = generateColorFamily({
+    name: "darkgray",
+    color: {
+        hue: {
+            start: 210,
+            end: 210,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 15,
+            end: 20,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 55,
+            end: 8,
+            curve: curve.linear,
+        },
+    },
+})
+
+// Red ======================================== //
+
+export const red = generateColorFamily({
+    name: "red",
+    color: {
+        hue: {
+            start: 0,
+            end: 0,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 95,
+            end: 75,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 25,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Sunset ======================================== //
+
+export const sunset = generateColorFamily({
+    name: "sunset",
+    color: {
+        hue: {
+            start: 15,
+            end: 15,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 100,
+            end: 90,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 25,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Orange ======================================== //
+
+export const orange = generateColorFamily({
+    name: "orange",
+    color: {
+        hue: {
+            start: 25,
+            end: 25,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 100,
+            end: 95,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Amber ======================================== //
+
+export const amber = generateColorFamily({
+    name: "amber",
+    color: {
+        hue: {
+            start: 38,
+            end: 38,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 100,
+            end: 100,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 18,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Yellow ======================================== //
+
+export const yellow = generateColorFamily({
+    name: "yellow",
+    color: {
+        hue: {
+            start: 48,
+            end: 48,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 90,
+            end: 100,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Lemon ======================================== //
+
+export const lemon = generateColorFamily({
+    name: "lemon",
+    color: {
+        hue: {
+            start: 55,
+            end: 55,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 95,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Citron ======================================== //
+
+export const citron = generateColorFamily({
+    name: "citron",
+    color: {
+        hue: {
+            start: 70,
+            end: 70,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 90,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Lime ======================================== //
+
+export const lime = generateColorFamily({
+    name: "lime",
+    color: {
+        hue: {
+            start: 85,
+            end: 85,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 80,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 18,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Green ======================================== //
+
+export const green = generateColorFamily({
+    name: "green",
+    color: {
+        hue: {
+            start: 108,
+            end: 108,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 60,
+            end: 70,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 18,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Mint ======================================== //
+
+export const mint = generateColorFamily({
+    name: "mint",
+    color: {
+        hue: {
+            start: 142,
+            end: 142,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 60,
+            end: 75,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Cyan ======================================== //
+
+export const cyan = generateColorFamily({
+    name: "cyan",
+    color: {
+        hue: {
+            start: 179,
+            end: 179,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 70,
+            end: 80,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Sky ======================================== //
+
+export const sky = generateColorFamily({
+    name: "sky",
+    color: {
+        hue: {
+            start: 195,
+            end: 205,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 90,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Blue ======================================== //
+
+export const blue = generateColorFamily({
+    name: "blue",
+    color: {
+        hue: {
+            start: 218,
+            end: 218,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 85,
+            end: 70,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 15,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Indigo ======================================== //
+
+export const indigo = generateColorFamily({
+    name: "indigo",
+    color: {
+        hue: {
+            start: 245,
+            end: 245,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 60,
+            end: 50,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 22,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Purple ======================================== //
+
+export const purple = generateColorFamily({
+    name: "purple",
+    color: {
+        hue: {
+            start: 260,
+            end: 270,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 65,
+            end: 55,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 20,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Pink ======================================== //
+
+export const pink = generateColorFamily({
+    name: "pink",
+    color: {
+        hue: {
+            start: 320,
+            end: 330,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 70,
+            end: 65,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 32,
+            curve: curve.lightness,
+        },
+    },
+})
+
+// Rose ======================================== //
+
+export const rose = generateColorFamily({
+    name: "rose",
+    color: {
+        hue: {
+            start: 345,
+            end: 345,
+            curve: curve.linear,
+        },
+        saturation: {
+            start: 90,
+            end: 70,
+            curve: curve.saturation,
+        },
+        lightness: {
+            start: 97,
+            end: 32,
+            curve: curve.lightness,
+        },
+    },
+})

styles/src/system/ref/curves.ts 🔗

@@ -0,0 +1,25 @@
+export interface Curve {
+    name: string
+    value: number[]
+}
+
+export interface Curves {
+    lightness: Curve
+    saturation: Curve
+    linear: Curve
+}
+
+export const curve: Curves = {
+    lightness: {
+        name: "lightnessCurve",
+        value: [0.2, 0, 0.75, 1.0],
+    },
+    saturation: {
+        name: "saturationCurve",
+        value: [0.67, 0.6, 0.55, 1.0],
+    },
+    linear: {
+        name: "linear",
+        value: [0.5, 0.5, 0.5, 0.5],
+    },
+}

styles/src/system/system.ts 🔗

@@ -0,0 +1,32 @@
+import chroma from "chroma-js"
+import * as colorFamily from "./ref/color"
+
+const color = {
+    lightgray: chroma
+        .scale(colorFamily.lightgray.scale.values)
+        .mode("lch")
+        .colors(9),
+    darkgray: chroma
+        .scale(colorFamily.darkgray.scale.values)
+        .mode("lch")
+        .colors(9),
+    red: chroma.scale(colorFamily.red.scale.values).mode("lch").colors(9),
+    sunset: chroma.scale(colorFamily.sunset.scale.values).mode("lch").colors(9),
+    orange: chroma.scale(colorFamily.orange.scale.values).mode("lch").colors(9),
+    amber: chroma.scale(colorFamily.amber.scale.values).mode("lch").colors(9),
+    yellow: chroma.scale(colorFamily.yellow.scale.values).mode("lch").colors(9),
+    lemon: chroma.scale(colorFamily.lemon.scale.values).mode("lch").colors(9),
+    citron: chroma.scale(colorFamily.citron.scale.values).mode("lch").colors(9),
+    lime: chroma.scale(colorFamily.lime.scale.values).mode("lch").colors(9),
+    green: chroma.scale(colorFamily.green.scale.values).mode("lch").colors(9),
+    mint: chroma.scale(colorFamily.mint.scale.values).mode("lch").colors(9),
+    cyan: chroma.scale(colorFamily.cyan.scale.values).mode("lch").colors(9),
+    sky: chroma.scale(colorFamily.sky.scale.values).mode("lch").colors(9),
+    blue: chroma.scale(colorFamily.blue.scale.values).mode("lch").colors(9),
+    indigo: chroma.scale(colorFamily.indigo.scale.values).mode("lch").colors(9),
+    purple: chroma.scale(colorFamily.purple.scale.values).mode("lch").colors(9),
+    pink: chroma.scale(colorFamily.pink.scale.values).mode("lch").colors(9),
+    rose: chroma.scale(colorFamily.rose.scale.values).mode("lch").colors(9),
+}
+
+export { color }

styles/src/system/types.ts 🔗

@@ -0,0 +1,66 @@
+import { Curve } from "./ref/curves"
+
+export interface ColorAccessiblityValue {
+    value: number
+    aaPass: boolean
+    aaaPass: boolean
+}
+
+/**
+ * Calculates the color contrast between a specified color and its corresponding background and foreground colors.
+ *
+ * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette.
+ * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information.
+ */
+export interface ColorAccessiblity {
+    black: ColorAccessiblityValue
+    white: ColorAccessiblityValue
+}
+
+export type Color = {
+    step: number
+    contrast: ColorAccessiblity
+    hex: string
+    lch: number[]
+    rgba: number[]
+    isLight: boolean
+}
+
+export interface ColorScale {
+    colors: Color[]
+    // An array of hex values for each color in the scale
+    values: string[]
+}
+
+export type ColorFamily = {
+    name: string
+    scale: ColorScale
+    invertedScale: ColorScale
+}
+
+export interface ColorFamilyHue {
+    start: number
+    end: number
+    curve: Curve
+}
+
+export interface ColorFamilySaturation {
+    start: number
+    end: number
+    curve: Curve
+}
+
+export interface ColorFamilyLightness {
+    start: number
+    end: number
+    curve: Curve
+}
+
+export interface ColorFamilyConfig {
+    name: string
+    color: {
+        hue: ColorFamilyHue
+        saturation: ColorFamilySaturation
+        lightness: ColorFamilyLightness
+    }
+}

styles/src/themes/andromeda.ts 🔗

@@ -1,41 +1,45 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Andromeda";
+const name = "Andromeda"
 
 const ramps = {
-  neutral: chroma
-    .scale([
-      "#1E2025",
-      "#23262E",
-      "#292E38",
-      "#2E323C",
-      "#ACA8AE",
-      "#CBC9CF",
-      "#E1DDE4",
-      "#F7F7F8",
-    ])
-    .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]),
-  red: colorRamp(chroma("#F92672")),
-  orange: colorRamp(chroma("#F39C12")),
-  yellow: colorRamp(chroma("#FFE66D")),
-  green: colorRamp(chroma("#96E072")),
-  cyan: colorRamp(chroma("#00E8C6")),
-  blue: colorRamp(chroma("#0CA793")),
-  violet: colorRamp(chroma("#8A3FA6")),
-  magenta: colorRamp(chroma("#C74DED")),
-};
+    neutral: chroma
+        .scale([
+            "#1E2025",
+            "#23262E",
+            "#292E38",
+            "#2E323C",
+            "#ACA8AE",
+            "#CBC9CF",
+            "#E1DDE4",
+            "#F7F7F8",
+        ])
+        .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]),
+    red: colorRamp(chroma("#F92672")),
+    orange: colorRamp(chroma("#F39C12")),
+    yellow: colorRamp(chroma("#FFE66D")),
+    green: colorRamp(chroma("#96E072")),
+    cyan: colorRamp(chroma("#00E8C6")),
+    blue: colorRamp(chroma("#0CA793")),
+    violet: colorRamp(chroma("#8A3FA6")),
+    magenta: colorRamp(chroma("#C74DED")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(name, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "EliverLara",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md",
-    license_checksum: "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89"
-  },
-  url: "https://github.com/EliverLara/Andromeda"
-}
+    name,
+    author: "EliverLara",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md",
+            license_checksum:
+                "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89",
+        },
+    },
+    url: "https://github.com/EliverLara/Andromeda",
+}

styles/src/themes/atelier-cave-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Cave Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/",
+    },
+    colors: {
+        base00: "#19171c",
+        base01: "#26232a",
+        base02: "#585260",
+        base03: "#655f6d",
+        base04: "#7e7887",
+        base05: "#8b8792",
+        base06: "#e2dfe7",
+        base07: "#efecf4",
+        base08: "#be4678",
+        base09: "#aa573c",
+        base0A: "#a06e3b",
+        base0B: "#2a9292",
+        base0C: "#398bc6",
+        base0D: "#576ddb",
+        base0E: "#955ae7",
+        base0F: "#bf40bf",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-cave-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Cave Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/",
+    },
+    colors: {
+        base00: "#efecf4",
+        base01: "#e2dfe7",
+        base02: "#8b8792",
+        base03: "#7e7887",
+        base04: "#655f6d",
+        base05: "#585260",
+        base06: "#26232a",
+        base07: "#19171c",
+        base08: "#be4678",
+        base09: "#aa573c",
+        base0A: "#a06e3b",
+        base0B: "#2a9292",
+        base0C: "#398bc6",
+        base0D: "#576ddb",
+        base0E: "#955ae7",
+        base0F: "#bf40bf",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-cave.ts 🔗

@@ -1,63 +0,0 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
-
-const name = "Atelier Cave";
-
-export const dark = createColorScheme(`${name} Dark`, false, {
-  neutral: chroma
-    .scale([
-      "#19171c",
-      "#26232a",
-      "#585260",
-      "#655f6d",
-      "#7e7887",
-      "#8b8792",
-      "#e2dfe7",
-      "#efecf4",
-    ])
-    .domain([0, 0.15, 0.45, 0.6, 0.65, 0.7, 0.85, 1]),
-  red: colorRamp(chroma("#be4678")),
-  orange: colorRamp(chroma("#aa573c")),
-  yellow: colorRamp(chroma("#a06e3b")),
-  green: colorRamp(chroma("#2a9292")),
-  cyan: colorRamp(chroma("#398bc6")),
-  blue: colorRamp(chroma("#576ddb")),
-  violet: colorRamp(chroma("#955ae7")),
-  magenta: colorRamp(chroma("#bf40bf")),
-});
-
-export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma
-    .scale([
-      "#19171c",
-      "#26232a",
-      "#585260",
-      "#655f6d",
-      "#7e7887",
-      "#8b8792",
-      "#e2dfe7",
-      "#efecf4",
-    ])
-    .correctLightness(),
-  red: colorRamp(chroma("#be4678")),
-  orange: colorRamp(chroma("#aa573c")),
-  yellow: colorRamp(chroma("#a06e3b")),
-  green: colorRamp(chroma("#2a9292")),
-  cyan: colorRamp(chroma("#398bc6")),
-  blue: colorRamp(chroma("#576ddb")),
-  violet: colorRamp(chroma("#955ae7")),
-  magenta: colorRamp(chroma("#bf40bf")),
-});
-
-
-export const meta: Meta = {
-  name,
-  author: "atelierbram",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://atelierbram.mit-license.org/license.txt",
-    license_checksum: "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5"
-  },
-  url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/"
-}

styles/src/themes/atelier-dune-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Dune Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/",
+    },
+    colors: {
+        base00: "#20201d",
+        base01: "#292824",
+        base02: "#6e6b5e",
+        base03: "#7d7a68",
+        base04: "#999580",
+        base05: "#a6a28c",
+        base06: "#e8e4cf",
+        base07: "#fefbec",
+        base08: "#d73737",
+        base09: "#b65611",
+        base0A: "#ae9513",
+        base0B: "#60ac39",
+        base0C: "#1fad83",
+        base0D: "#6684e1",
+        base0E: "#b854d4",
+        base0F: "#d43552",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-dune-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Dune Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/",
+    },
+    colors: {
+        base00: "#fefbec",
+        base01: "#e8e4cf",
+        base02: "#a6a28c",
+        base03: "#999580",
+        base04: "#7d7a68",
+        base05: "#6e6b5e",
+        base06: "#292824",
+        base07: "#20201d",
+        base08: "#d73737",
+        base09: "#b65611",
+        base0A: "#ae9513",
+        base0B: "#60ac39",
+        base0C: "#1fad83",
+        base0D: "#6684e1",
+        base0E: "#b854d4",
+        base0F: "#d43552",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-estuary-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Estuary Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary/",
+    },
+    colors: {
+        base00: "#22221b",
+        base01: "#302f27",
+        base02: "#5f5e4e",
+        base03: "#6c6b5a",
+        base04: "#878573",
+        base05: "#929181",
+        base06: "#e7e6df",
+        base07: "#f4f3ec",
+        base08: "#ba6236",
+        base09: "#ae7313",
+        base0A: "#a5980d",
+        base0B: "#7d9726",
+        base0C: "#5b9d48",
+        base0D: "#36a166",
+        base0E: "#5f9182",
+        base0F: "#9d6c7c",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-estuary-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Estuary Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary/",
+    },
+    colors: {
+        base00: "#f4f3ec",
+        base01: "#e7e6df",
+        base02: "#929181",
+        base03: "#878573",
+        base04: "#6c6b5a",
+        base05: "#5f5e4e",
+        base06: "#302f27",
+        base07: "#22221b",
+        base08: "#ba6236",
+        base09: "#ae7313",
+        base0A: "#a5980d",
+        base0B: "#7d9726",
+        base0C: "#5b9d48",
+        base0D: "#36a166",
+        base0E: "#5f9182",
+        base0F: "#9d6c7c",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-forest-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Forest Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest/",
+    },
+    colors: {
+        base00: "#1b1918",
+        base01: "#2c2421",
+        base02: "#68615e",
+        base03: "#766e6b",
+        base04: "#9c9491",
+        base05: "#a8a19f",
+        base06: "#e6e2e0",
+        base07: "#f1efee",
+        base08: "#f22c40",
+        base09: "#df5320",
+        base0A: "#c38418",
+        base0B: "#7b9726",
+        base0C: "#3d97b8",
+        base0D: "#407ee7",
+        base0E: "#6666ea",
+        base0F: "#c33ff3",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-forest-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Forest Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest/",
+    },
+    colors: {
+        base00: "#f1efee",
+        base01: "#e6e2e0",
+        base02: "#a8a19f",
+        base03: "#9c9491",
+        base04: "#766e6b",
+        base05: "#68615e",
+        base06: "#2c2421",
+        base07: "#1b1918",
+        base08: "#f22c40",
+        base09: "#df5320",
+        base0A: "#c38418",
+        base0B: "#7b9726",
+        base0C: "#3d97b8",
+        base0D: "#407ee7",
+        base0E: "#6666ea",
+        base0F: "#c33ff3",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-heath-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Heath Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/",
+    },
+    colors: {
+        base00: "#1b181b",
+        base01: "#292329",
+        base02: "#695d69",
+        base03: "#776977",
+        base04: "#9e8f9e",
+        base05: "#ab9bab",
+        base06: "#d8cad8",
+        base07: "#f7f3f7",
+        base08: "#ca402b",
+        base09: "#a65926",
+        base0A: "#bb8a35",
+        base0B: "#918b3b",
+        base0C: "#159393",
+        base0D: "#516aec",
+        base0E: "#7b59c0",
+        base0F: "#cc33cc",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-heath-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Heath Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/",
+    },
+    colors: {
+        base00: "#f7f3f7",
+        base01: "#d8cad8",
+        base02: "#ab9bab",
+        base03: "#9e8f9e",
+        base04: "#776977",
+        base05: "#695d69",
+        base06: "#292329",
+        base07: "#1b181b",
+        base08: "#ca402b",
+        base09: "#a65926",
+        base0A: "#bb8a35",
+        base0B: "#918b3b",
+        base0C: "#159393",
+        base0D: "#516aec",
+        base0E: "#7b59c0",
+        base0F: "#cc33cc",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-lakeside-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Lakeside Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/",
+    },
+    colors: {
+        base00: "#161b1d",
+        base01: "#1f292e",
+        base02: "#516d7b",
+        base03: "#5a7b8c",
+        base04: "#7195a8",
+        base05: "#7ea2b4",
+        base06: "#c1e4f6",
+        base07: "#ebf8ff",
+        base08: "#d22d72",
+        base09: "#935c25",
+        base0A: "#8a8a0f",
+        base0B: "#568c3b",
+        base0C: "#2d8f6f",
+        base0D: "#257fad",
+        base0E: "#6b6bb8",
+        base0F: "#b72dd2",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-lakeside-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Lakeside Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/",
+    },
+    colors: {
+        base00: "#ebf8ff",
+        base01: "#c1e4f6",
+        base02: "#7ea2b4",
+        base03: "#7195a8",
+        base04: "#5a7b8c",
+        base05: "#516d7b",
+        base06: "#1f292e",
+        base07: "#161b1d",
+        base08: "#d22d72",
+        base09: "#935c25",
+        base0A: "#8a8a0f",
+        base0B: "#568c3b",
+        base0C: "#2d8f6f",
+        base0D: "#257fad",
+        base0E: "#6b6bb8",
+        base0F: "#b72dd2",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-plateau-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Plateau Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau/",
+    },
+    colors: {
+        base00: "#1b1818",
+        base01: "#292424",
+        base02: "#585050",
+        base03: "#655d5d",
+        base04: "#7e7777",
+        base05: "#8a8585",
+        base06: "#e7dfdf",
+        base07: "#f4ecec",
+        base08: "#ca4949",
+        base09: "#b45a3c",
+        base0A: "#a06e3b",
+        base0B: "#4b8b8b",
+        base0C: "#5485b6",
+        base0D: "#7272ca",
+        base0E: "#8464c4",
+        base0F: "#bd5187",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-plateau-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Plateau Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau/",
+    },
+    colors: {
+        base00: "#f4ecec",
+        base01: "#e7dfdf",
+        base02: "#8a8585",
+        base03: "#7e7777",
+        base04: "#655d5d",
+        base05: "#585050",
+        base06: "#292424",
+        base07: "#1b1818",
+        base08: "#ca4949",
+        base09: "#b45a3c",
+        base0A: "#a06e3b",
+        base0B: "#4b8b8b",
+        base0C: "#5485b6",
+        base0D: "#7272ca",
+        base0E: "#8464c4",
+        base0F: "#bd5187",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-savanna-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Savanna Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna/",
+    },
+    colors: {
+        base00: "#171c19",
+        base01: "#232a25",
+        base02: "#526057",
+        base03: "#5f6d64",
+        base04: "#78877d",
+        base05: "#87928a",
+        base06: "#dfe7e2",
+        base07: "#ecf4ee",
+        base08: "#b16139",
+        base09: "#9f713c",
+        base0A: "#a07e3b",
+        base0B: "#489963",
+        base0C: "#1c9aa0",
+        base0D: "#478c90",
+        base0E: "#55859b",
+        base0F: "#867469",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-savanna-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Savanna Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna/",
+    },
+    colors: {
+        base00: "#ecf4ee",
+        base01: "#dfe7e2",
+        base02: "#87928a",
+        base03: "#78877d",
+        base04: "#5f6d64",
+        base05: "#526057",
+        base06: "#232a25",
+        base07: "#171c19",
+        base08: "#b16139",
+        base09: "#9f713c",
+        base0A: "#a07e3b",
+        base0B: "#489963",
+        base0C: "#1c9aa0",
+        base0D: "#478c90",
+        base0E: "#55859b",
+        base0F: "#867469",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-seaside-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Seaside Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/",
+    },
+    colors: {
+        base00: "#131513",
+        base01: "#242924",
+        base02: "#5e6e5e",
+        base03: "#687d68",
+        base04: "#809980",
+        base05: "#8ca68c",
+        base06: "#cfe8cf",
+        base07: "#f4fbf4",
+        base08: "#e6193c",
+        base09: "#87711d",
+        base0A: "#98981b",
+        base0B: "#29a329",
+        base0C: "#1999b3",
+        base0D: "#3d62f5",
+        base0E: "#ad2bee",
+        base0F: "#e619c3",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-seaside-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Seaside Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/",
+    },
+    colors: {
+        base00: "#f4fbf4",
+        base01: "#cfe8cf",
+        base02: "#8ca68c",
+        base03: "#809980",
+        base04: "#687d68",
+        base05: "#5e6e5e",
+        base06: "#242924",
+        base07: "#131513",
+        base08: "#e6193c",
+        base09: "#87711d",
+        base0A: "#98981b",
+        base0B: "#29a329",
+        base0C: "#1999b3",
+        base0D: "#3d62f5",
+        base0E: "#ad2bee",
+        base0F: "#e619c3",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-sulphurpool-dark.ts 🔗

@@ -0,0 +1,66 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Sulphurpool Dark`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/",
+    },
+    colors: {
+        base00: "#202746",
+        base01: "#293256",
+        base02: "#5e6687",
+        base03: "#6b7394",
+        base04: "#898ea4",
+        base05: "#979db4",
+        base06: "#dfe2f1",
+        base07: "#f5f7ff",
+        base08: "#c94922",
+        base09: "#c76b29",
+        base0A: "#c08b30",
+        base0B: "#ac9739",
+        base0C: "#22a2c9",
+        base0D: "#3d8fd1",
+        base0E: "#6679cc",
+        base0F: "#9c637a",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        false,
+        {
+            neutral: chroma.scale([
+                colors.base00,
+                colors.base01,
+                colors.base02,
+                colors.base03,
+                colors.base04,
+                colors.base05,
+                colors.base06,
+                colors.base07,
+            ]),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-sulphurpool-light.ts 🔗

@@ -0,0 +1,68 @@
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+
+const variant: Variant = {
+    meta: {
+        name: `${name} Sulphurpool Light`,
+        ...metaCommon,
+        url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/",
+    },
+    colors: {
+        base00: "#f5f7ff",
+        base01: "#dfe2f1",
+        base02: "#979db4",
+        base03: "#898ea4",
+        base04: "#6b7394",
+        base05: "#5e6687",
+        base06: "#293256",
+        base07: "#202746",
+        base08: "#c94922",
+        base09: "#c76b29",
+        base0A: "#c08b30",
+        base0B: "#ac9739",
+        base0C: "#22a2c9",
+        base0D: "#3d8fd1",
+        base0E: "#6679cc",
+        base0F: "#9c637a",
+    },
+}
+
+const syntax = buildSyntax(variant)
+
+const theme = (variant: Variant) => {
+    const { meta, colors } = variant
+
+    return createColorScheme(
+        meta.name,
+        true,
+        {
+            neutral: chroma.scale(
+                [
+                    colors.base00,
+                    colors.base01,
+                    colors.base02,
+                    colors.base03,
+                    colors.base04,
+                    colors.base05,
+                    colors.base06,
+                    colors.base07,
+                ].reverse()
+            ),
+            red: colorRamp(chroma(colors.base08)),
+            orange: colorRamp(chroma(colors.base09)),
+            yellow: colorRamp(chroma(colors.base0A)),
+            green: colorRamp(chroma(colors.base0B)),
+            cyan: colorRamp(chroma(colors.base0C)),
+            blue: colorRamp(chroma(colors.base0D)),
+            violet: colorRamp(chroma(colors.base0E)),
+            magenta: colorRamp(chroma(colors.base0F)),
+        },
+        syntax
+    )
+}
+
+export const dark = theme(variant)
+
+export const meta: Meta = variant.meta

styles/src/themes/atelier-sulphurpool.ts 🔗

@@ -1,42 +0,0 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
-
-const name = "Atelier Sulphurpool";
-
-const ramps = {
-  neutral: chroma
-    .scale([
-      "#202746",
-      "#293256",
-      "#5e6687",
-      "#6b7394",
-      "#898ea4",
-      "#979db4",
-      "#dfe2f1",
-      "#f5f7ff",
-    ])
-    .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
-  red: colorRamp(chroma("#c94922")),
-  orange: colorRamp(chroma("#c76b29")),
-  yellow: colorRamp(chroma("#c08b30")),
-  green: colorRamp(chroma("#ac9739")),
-  cyan: colorRamp(chroma("#22a2c9")),
-  blue: colorRamp(chroma("#3d8fd1")),
-  violet: colorRamp(chroma("#6679cc")),
-  magenta: colorRamp(chroma("#9c637a")),
-};
-
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);
-
-export const meta: Meta = {
-  name,
-  author: "atelierbram",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://atelierbram.mit-license.org/license.txt",
-    license_checksum: "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5"
-  },
-  url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/"
-}

styles/src/themes/common/atelier-common.ts 🔗

@@ -0,0 +1,66 @@
+import { License, Meta, ThemeSyntax } from "./colorScheme"
+
+export interface Variant {
+    meta: Meta
+    colors: {
+        base00: string
+        base01: string
+        base02: string
+        base03: string
+        base04: string
+        base05: string
+        base06: string
+        base07: string
+        base08: string
+        base09: string
+        base0A: string
+        base0B: string
+        base0C: string
+        base0D: string
+        base0E: string
+        base0F: string
+    }
+}
+
+export const metaCommon: {
+    author: string
+    license: License
+} = {
+    author: "Bram de Haan (http://atelierbramdehaan.nl)",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url: "https://atelierbram.mit-license.org/license.txt",
+            license_checksum:
+                "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5",
+        },
+    },
+}
+
+export const buildSyntax = (variant: Variant): ThemeSyntax => {
+    const { colors } = variant
+    return {
+        primary: { color: colors.base06 },
+        comment: { color: colors.base03 },
+        "punctuation.delimiter": { color: colors.base05 },
+        "punctuation.bracket": { color: colors.base05 },
+        "punctuation.special": { color: colors.base0F },
+        "string.special.symbol": { color: colors.base0B },
+        operator: { color: colors.base05 },
+        function: { color: colors.base0D },
+        "function.method": { color: colors.base0D },
+        "function.special.definition": { color: colors.base0A },
+        string: { color: colors.base0B },
+        "string.special": { color: colors.base0F },
+        "string.regex": { color: colors.base0C },
+        type: { color: colors.base0A },
+        number: { color: colors.base09 },
+        property: { color: colors.base08 },
+        variable: { color: colors.base06 },
+        "variable.special": { color: colors.base0E },
+        variant: { color: colors.base0A },
+        keyword: { color: colors.base0E },
+    }
+}
+
+export const name = "Atelier"

styles/src/themes/common/base16.ts 🔗

@@ -1,296 +0,0 @@
-// NOTE – This should be removed
-// I (Nate) need to come back and check if we are still using this anywhere
-
-import chroma, { Color, Scale } from "chroma-js";
-import { fontWeights } from "../../common";
-import { withOpacity } from "../../utils/color";
-import Theme, { buildPlayer, Syntax } from "./theme";
-
-export function colorRamp(color: Color): Scale {
-  let hue = color.hsl()[0];
-  let endColor = chroma.hsl(hue, 0.88, 0.96);
-  let startColor = chroma.hsl(hue, 0.68, 0.12);
-  return chroma.scale([startColor, color, endColor]).mode("hsl");
-}
-
-export function createTheme(
-  name: string,
-  isLight: boolean,
-  color_ramps: { [rampName: string]: Scale }
-): Theme {
-  let ramps: typeof color_ramps = {};
-  // Chromajs mutates the underlying ramp when you call domain. This causes problems because
-  // we now store the ramps object in the theme so that we can pull colors out of them.
-  // So instead of calling domain and storing the result, we have to construct new ramps for each
-  // theme so that we don't modify the passed in ramps.
-  // This combined with an error in the type definitions for chroma js means we have to cast the colors
-  // function to any in order to get the colors back out from the original ramps.
-  if (isLight) {
-    for (var rampName in color_ramps) {
-      ramps[rampName] = chroma
-        .scale((color_ramps[rampName].colors as any)())
-        .domain([1, 0]);
-    }
-    ramps.neutral = chroma
-      .scale((color_ramps.neutral.colors as any)())
-      .domain([7, 0]);
-  } else {
-    for (var rampName in color_ramps) {
-      ramps[rampName] = chroma
-        .scale((color_ramps[rampName].colors as any)())
-        .domain([0, 1]);
-    }
-    ramps.neutral = chroma
-      .scale((color_ramps.neutral.colors as any)())
-      .domain([0, 7]);
-  }
-
-  let blend = isLight ? 0.12 : 0.24;
-
-  function sample(ramp: Scale, index: number): string {
-    return ramp(index).hex();
-  }
-  const darkest = ramps.neutral(isLight ? 7 : 0).hex();
-
-  const backgroundColor = {
-    // Title bar
-    100: {
-      base: sample(ramps.neutral, 1.25),
-      hovered: sample(ramps.neutral, 1.5),
-      active: sample(ramps.neutral, 1.75),
-    },
-    // Midground (panels, etc)
-    300: {
-      base: sample(ramps.neutral, 1),
-      hovered: sample(ramps.neutral, 1.25),
-      active: sample(ramps.neutral, 1.5),
-    },
-    // Editor
-    500: {
-      base: sample(ramps.neutral, 0),
-      hovered: sample(ramps.neutral, 0.25),
-      active: sample(ramps.neutral, 0.5),
-    },
-    on300: {
-      base: sample(ramps.neutral, 0),
-      hovered: sample(ramps.neutral, 0.5),
-      active: sample(ramps.neutral, 1),
-    },
-    on500: {
-      base: sample(ramps.neutral, 1),
-      hovered: sample(ramps.neutral, 1.5),
-      active: sample(ramps.neutral, 2),
-    },
-    ok: {
-      base: withOpacity(sample(ramps.green, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
-      active: withOpacity(sample(ramps.green, 0.5), 0.25),
-    },
-    error: {
-      base: withOpacity(sample(ramps.red, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
-      active: withOpacity(sample(ramps.red, 0.5), 0.25),
-    },
-    on500Error: {
-      base: sample(ramps.red, 0.05),
-      hovered: sample(ramps.red, 0.1),
-      active: sample(ramps.red, 0.15),
-    },
-    warning: {
-      base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
-      active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
-    },
-    on500Warning: {
-      base: sample(ramps.yellow, 0.05),
-      hovered: sample(ramps.yellow, 0.1),
-      active: sample(ramps.yellow, 0.15),
-    },
-    info: {
-      base: withOpacity(sample(ramps.blue, 0.5), 0.15),
-      hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
-      active: withOpacity(sample(ramps.blue, 0.5), 0.25),
-    },
-    on500Info: {
-      base: sample(ramps.blue, 0.05),
-      hovered: sample(ramps.blue, 0.1),
-      active: sample(ramps.blue, 0.15),
-    },
-    on500Ok: {
-      base: sample(ramps.green, 0.05),
-      hovered: sample(ramps.green, 0.1),
-      active: sample(ramps.green, 0.15),
-    },
-  };
-
-  const borderColor = {
-    primary: sample(ramps.neutral, isLight ? 1.5 : 0),
-    secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
-    muted: sample(ramps.neutral, isLight ? 1.25 : 3),
-    active: sample(ramps.neutral, isLight ? 4 : 3),
-    onMedia: withOpacity(darkest, 0.1),
-    ok: sample(ramps.green, 0.3),
-    error: sample(ramps.red, 0.3),
-    warning: sample(ramps.yellow, 0.3),
-    info: sample(ramps.blue, 0.3),
-  };
-
-  const textColor = {
-    primary: sample(ramps.neutral, 6),
-    secondary: sample(ramps.neutral, 5),
-    muted: sample(ramps.neutral, 4),
-    placeholder: sample(ramps.neutral, 3),
-    active: sample(ramps.neutral, 7),
-    feature: sample(ramps.blue, 0.5),
-    ok: sample(ramps.green, 0.5),
-    error: sample(ramps.red, 0.5),
-    warning: sample(ramps.yellow, 0.5),
-    info: sample(ramps.blue, 0.5),
-    onMedia: darkest,
-  };
-
-  const player = {
-    1: buildPlayer(sample(ramps.blue, 0.5)),
-    2: buildPlayer(sample(ramps.green, 0.5)),
-    3: buildPlayer(sample(ramps.magenta, 0.5)),
-    4: buildPlayer(sample(ramps.orange, 0.5)),
-    5: buildPlayer(sample(ramps.violet, 0.5)),
-    6: buildPlayer(sample(ramps.cyan, 0.5)),
-    7: buildPlayer(sample(ramps.red, 0.5)),
-    8: buildPlayer(sample(ramps.yellow, 0.5)),
-  };
-
-  const editor = {
-    background: backgroundColor[500].base,
-    indent_guide: borderColor.muted,
-    indent_guide_active: borderColor.secondary,
-    line: {
-      active: sample(ramps.neutral, 1),
-      highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
-    },
-    highlight: {
-      selection: player[1].selectionColor,
-      occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
-      activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
-      matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
-      match: sample(ramps.violet, 0.15),
-      activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
-      related: backgroundColor[500].hovered,
-    },
-    gutter: {
-      primary: textColor.placeholder,
-      active: textColor.active,
-    },
-  };
-
-  const syntax: Syntax = {
-    primary: {
-      color: sample(ramps.neutral, 7),
-      weight: fontWeights.normal,
-    },
-    "variable.special": {
-      color: sample(ramps.blue, 0.8),
-      weight: fontWeights.normal,
-    },
-    comment: {
-      color: sample(ramps.neutral, 5),
-      weight: fontWeights.normal,
-    },
-    punctuation: {
-      color: sample(ramps.neutral, 6),
-      weight: fontWeights.normal,
-    },
-    constant: {
-      color: sample(ramps.neutral, 4),
-      weight: fontWeights.normal,
-    },
-    keyword: {
-      color: sample(ramps.blue, 0.5),
-      weight: fontWeights.normal,
-    },
-    function: {
-      color: sample(ramps.yellow, 0.5),
-      weight: fontWeights.normal,
-    },
-    type: {
-      color: sample(ramps.cyan, 0.5),
-      weight: fontWeights.normal,
-    },
-    constructor: {
-      color: sample(ramps.cyan, 0.5),
-      weight: fontWeights.normal,
-    },
-    property: {
-      color: sample(ramps.blue, 0.6),
-      weight: fontWeights.normal,
-    },
-    enum: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-    },
-    operator: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-    },
-    string: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-    },
-    number: {
-      color: sample(ramps.green, 0.5),
-      weight: fontWeights.normal,
-    },
-    boolean: {
-      color: sample(ramps.green, 0.5),
-      weight: fontWeights.normal,
-    },
-    predictive: {
-      color: textColor.muted,
-      weight: fontWeights.normal,
-    },
-    title: {
-      color: sample(ramps.yellow, 0.5),
-      weight: fontWeights.bold,
-    },
-    emphasis: {
-      color: textColor.feature,
-      weight: fontWeights.normal,
-    },
-    "emphasis.strong": {
-      color: textColor.feature,
-      weight: fontWeights.bold,
-    },
-    linkUri: {
-      color: sample(ramps.green, 0.5),
-      weight: fontWeights.normal,
-      underline: true,
-    },
-    linkText: {
-      color: sample(ramps.orange, 0.5),
-      weight: fontWeights.normal,
-      italic: true,
-    },
-  };
-
-  const shadow = withOpacity(
-    ramps
-      .neutral(isLight ? 7 : 0)
-      .darken()
-      .hex(),
-    blend
-  );
-
-  return {
-    name,
-    isLight,
-    backgroundColor,
-    borderColor,
-    textColor,
-    iconColor: textColor,
-    editor,
-    syntax,
-    player,
-    shadow,
-    ramps,
-  };
-}

styles/src/themes/common/colorScheme.ts 🔗

@@ -1,100 +1,107 @@
-import { Scale } from "chroma-js";
+import { Scale } from "chroma-js"
+import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
+export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
 
 export interface ColorScheme {
-  name: string;
-  isLight: boolean;
+    name: string
+    isLight: boolean
 
-  lowest: Layer;
-  middle: Layer;
-  highest: Layer;
+    lowest: Layer
+    middle: Layer
+    highest: Layer
 
-  ramps: RampSet;
+    ramps: RampSet
 
-  popoverShadow: Shadow;
-  modalShadow: Shadow;
+    popoverShadow: Shadow
+    modalShadow: Shadow
 
-  players: Players;
+    players: Players
+    syntax?: Partial<ThemeSyntax>
 }
 
 export interface Meta {
-  name: string,
-  author: string,
-  url: string,
-  license: License
+    name: string
+    author: string
+    url: string
+    license: License
 }
 
 export interface License {
-  SPDX: SPDXExpression,
-  /// A url where we can download the license's text
-  https_url: string,
-  license_checksum: string
+    SPDX: SPDXExpression
+    /// A url where we can download the license's text
+    license_text: Verification | string
+}
+
+export interface Verification {
+    https_url: string
+    license_checksum: string
 }
 
 // License name -> License text
 export interface Licenses {
-  [key: string]: string
+    [key: string]: string
 }
 
 // FIXME: Add support for the SPDX expression syntax
-export type SPDXExpression = "MIT";
+export type SPDXExpression = "MIT"
 
 export interface Player {
-  cursor: string;
-  selection: string;
+    cursor: string
+    selection: string
 }
 
 export interface Players {
-  "0": Player;
-  "1": Player;
-  "2": Player;
-  "3": Player;
-  "4": Player;
-  "5": Player;
-  "6": Player;
-  "7": Player;
+    "0": Player
+    "1": Player
+    "2": Player
+    "3": Player
+    "4": Player
+    "5": Player
+    "6": Player
+    "7": Player
 }
 
 export interface Shadow {
-  blur: number;
-  color: string;
-  offset: number[];
+    blur: number
+    color: string
+    offset: number[]
 }
 
-export type StyleSets = keyof Layer;
+export type StyleSets = keyof Layer
 export interface Layer {
-  base: StyleSet;
-  variant: StyleSet;
-  on: StyleSet;
-  accent: StyleSet;
-  positive: StyleSet;
-  warning: StyleSet;
-  negative: StyleSet;
+    base: StyleSet
+    variant: StyleSet
+    on: StyleSet
+    accent: StyleSet
+    positive: StyleSet
+    warning: StyleSet
+    negative: StyleSet
 }
 
 export interface RampSet {
-  neutral: Scale;
-  red: Scale;
-  orange: Scale;
-  yellow: Scale;
-  green: Scale;
-  cyan: Scale;
-  blue: Scale;
-  violet: Scale;
-  magenta: Scale;
+    neutral: Scale
+    red: Scale
+    orange: Scale
+    yellow: Scale
+    green: Scale
+    cyan: Scale
+    blue: Scale
+    violet: Scale
+    magenta: Scale
 }
 
-export type Styles = keyof StyleSet;
+export type Styles = keyof StyleSet
 export interface StyleSet {
-  default: Style;
-  active: Style;
-  disabled: Style;
-  hovered: Style;
-  pressed: Style;
-  inverted: Style;
+    default: Style
+    active: Style
+    disabled: Style
+    hovered: Style
+    pressed: Style
+    inverted: Style
 }
 
 export interface Style {
-  background: string;
-  border: string;
-  foreground: string;
+    background: string
+    border: string
+    foreground: string
 }

styles/src/themes/common/ramps.ts 🔗

@@ -1,210 +1,215 @@
-import chroma, { Color, Scale } from "chroma-js";
+import chroma, { Color, Scale } from "chroma-js"
 import {
-  ColorScheme,
-  Layer,
-  Player,
-  RampSet,
-  Style,
-  Styles,
-  StyleSet,
-} from "./colorScheme";
+    ColorScheme,
+    Layer,
+    Player,
+    RampSet,
+    Style,
+    Styles,
+    StyleSet,
+    ThemeSyntax,
+} from "./colorScheme"
 
 export function colorRamp(color: Color): Scale {
-  let endColor = color.desaturate(1).brighten(5);
-  let startColor = color.desaturate(1).darken(4);
-  return chroma.scale([startColor, color, endColor]).mode("lab");
+    let endColor = color.desaturate(1).brighten(5)
+    let startColor = color.desaturate(1).darken(4)
+    return chroma.scale([startColor, color, endColor]).mode("lab")
 }
 
 export function createColorScheme(
-  name: string,
-  isLight: boolean,
-  colorRamps: { [rampName: string]: Scale }
+    name: string,
+    isLight: boolean,
+    colorRamps: { [rampName: string]: Scale },
+    syntax?: ThemeSyntax
 ): ColorScheme {
-  // Chromajs scales from 0 to 1 flipped if isLight is true
-  let ramps: RampSet = {} as any;
-
-  // Chromajs mutates the underlying ramp when you call domain. This causes problems because
-  // we now store the ramps object in the theme so that we can pull colors out of them.
-  // So instead of calling domain and storing the result, we have to construct new ramps for each
-  // theme so that we don't modify the passed in ramps.
-  // This combined with an error in the type definitions for chroma js means we have to cast the colors
-  // function to any in order to get the colors back out from the original ramps.
-  if (isLight) {
-    for (var rampName in colorRamps) {
-      (ramps as any)[rampName] = chroma.scale(
-        colorRamps[rampName].colors(100).reverse()
-      );
+    // Chromajs scales from 0 to 1 flipped if isLight is true
+    let ramps: RampSet = {} as any
+
+    // Chromajs mutates the underlying ramp when you call domain. This causes problems because
+    // we now store the ramps object in the theme so that we can pull colors out of them.
+    // So instead of calling domain and storing the result, we have to construct new ramps for each
+    // theme so that we don't modify the passed in ramps.
+    // This combined with an error in the type definitions for chroma js means we have to cast the colors
+    // function to any in order to get the colors back out from the original ramps.
+    if (isLight) {
+        for (var rampName in colorRamps) {
+            ;(ramps as any)[rampName] = chroma.scale(
+                colorRamps[rampName].colors(100).reverse()
+            )
+        }
+        ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
+    } else {
+        for (var rampName in colorRamps) {
+            ;(ramps as any)[rampName] = chroma.scale(
+                colorRamps[rampName].colors(100)
+            )
+        }
+        ramps.neutral = chroma.scale(colorRamps.neutral.colors(100))
+    }
+
+    let lowest = lowestLayer(ramps)
+    let middle = middleLayer(ramps)
+    let highest = highestLayer(ramps)
+
+    let popoverShadow = {
+        blur: 4,
+        color: ramps
+            .neutral(isLight ? 7 : 0)
+            .darken()
+            .alpha(0.2)
+            .hex(), // TODO used blend previously. Replace with something else
+        offset: [1, 2],
+    }
+
+    let modalShadow = {
+        blur: 16,
+        color: ramps
+            .neutral(isLight ? 7 : 0)
+            .darken()
+            .alpha(0.2)
+            .hex(), // TODO used blend previously. Replace with something else
+        offset: [0, 2],
+    }
+
+    let players = {
+        "0": player(ramps.blue),
+        "1": player(ramps.green),
+        "2": player(ramps.magenta),
+        "3": player(ramps.orange),
+        "4": player(ramps.violet),
+        "5": player(ramps.cyan),
+        "6": player(ramps.red),
+        "7": player(ramps.yellow),
     }
-    ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse());
-  } else {
-    for (var rampName in colorRamps) {
-      (ramps as any)[rampName] = chroma.scale(colorRamps[rampName].colors(100));
+
+    return {
+        name,
+        isLight,
+
+        ramps,
+
+        lowest,
+        middle,
+        highest,
+
+        popoverShadow,
+        modalShadow,
+
+        players,
+        syntax,
     }
-    ramps.neutral = chroma.scale(colorRamps.neutral.colors(100));
-  }
-
-  let lowest = lowestLayer(ramps);
-  let middle = middleLayer(ramps);
-  let highest = highestLayer(ramps);
-
-  let popoverShadow = {
-    blur: 4,
-    color: ramps
-      .neutral(isLight ? 7 : 0)
-      .darken()
-      .alpha(0.2)
-      .hex(), // TODO used blend previously. Replace with something else
-    offset: [1, 2],
-  };
-
-  let modalShadow = {
-    blur: 16,
-    color: ramps
-      .neutral(isLight ? 7 : 0)
-      .darken()
-      .alpha(0.2)
-      .hex(), // TODO used blend previously. Replace with something else
-    offset: [0, 2],
-  };
-
-  let players = {
-    "0": player(ramps.blue),
-    "1": player(ramps.green),
-    "2": player(ramps.magenta),
-    "3": player(ramps.orange),
-    "4": player(ramps.violet),
-    "5": player(ramps.cyan),
-    "6": player(ramps.red),
-    "7": player(ramps.yellow),
-  };
-
-  return {
-    name,
-    isLight,
-
-    ramps,
-
-    lowest,
-    middle,
-    highest,
-
-    popoverShadow,
-    modalShadow,
-
-    players,
-  };
 }
 
 function player(ramp: Scale): Player {
-  return {
-    selection: ramp(0.5).alpha(0.24).hex(),
-    cursor: ramp(0.5).hex(),
-  };
+    return {
+        selection: ramp(0.5).alpha(0.24).hex(),
+        cursor: ramp(0.5).hex(),
+    }
 }
 
 function lowestLayer(ramps: RampSet): Layer {
-  return {
-    base: buildStyleSet(ramps.neutral, 0.2, 1),
-    variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
-    on: buildStyleSet(ramps.neutral, 0.1, 1),
-    accent: buildStyleSet(ramps.blue, 0.1, 0.5),
-    positive: buildStyleSet(ramps.green, 0.1, 0.5),
-    warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
-    negative: buildStyleSet(ramps.red, 0.1, 0.5),
-  };
+    return {
+        base: buildStyleSet(ramps.neutral, 0.2, 1),
+        variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
+        on: buildStyleSet(ramps.neutral, 0.1, 1),
+        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
+        positive: buildStyleSet(ramps.green, 0.1, 0.5),
+        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
+        negative: buildStyleSet(ramps.red, 0.1, 0.5),
+    }
 }
 
 function middleLayer(ramps: RampSet): Layer {
-  return {
-    base: buildStyleSet(ramps.neutral, 0.1, 1),
-    variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
-    on: buildStyleSet(ramps.neutral, 0, 1),
-    accent: buildStyleSet(ramps.blue, 0.1, 0.5),
-    positive: buildStyleSet(ramps.green, 0.1, 0.5),
-    warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
-    negative: buildStyleSet(ramps.red, 0.1, 0.5),
-  };
+    return {
+        base: buildStyleSet(ramps.neutral, 0.1, 1),
+        variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
+        on: buildStyleSet(ramps.neutral, 0, 1),
+        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
+        positive: buildStyleSet(ramps.green, 0.1, 0.5),
+        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
+        negative: buildStyleSet(ramps.red, 0.1, 0.5),
+    }
 }
 
 function highestLayer(ramps: RampSet): Layer {
-  return {
-    base: buildStyleSet(ramps.neutral, 0, 1),
-    variant: buildStyleSet(ramps.neutral, 0, 0.7),
-    on: buildStyleSet(ramps.neutral, 0.1, 1),
-    accent: buildStyleSet(ramps.blue, 0.1, 0.5),
-    positive: buildStyleSet(ramps.green, 0.1, 0.5),
-    warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
-    negative: buildStyleSet(ramps.red, 0.1, 0.5),
-  };
+    return {
+        base: buildStyleSet(ramps.neutral, 0, 1),
+        variant: buildStyleSet(ramps.neutral, 0, 0.7),
+        on: buildStyleSet(ramps.neutral, 0.1, 1),
+        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
+        positive: buildStyleSet(ramps.green, 0.1, 0.5),
+        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
+        negative: buildStyleSet(ramps.red, 0.1, 0.5),
+    }
 }
 
 function buildStyleSet(
-  ramp: Scale,
-  backgroundBase: number,
-  foregroundBase: number,
-  step: number = 0.08
+    ramp: Scale,
+    backgroundBase: number,
+    foregroundBase: number,
+    step: number = 0.08
 ): StyleSet {
-  let styleDefinitions = buildStyleDefinition(
-    backgroundBase,
-    foregroundBase,
-    step
-  );
-
-  function colorString(indexOrColor: number | Color): string {
-    if (typeof indexOrColor === "number") {
-      return ramp(indexOrColor).hex();
-    } else {
-      return indexOrColor.hex();
+    let styleDefinitions = buildStyleDefinition(
+        backgroundBase,
+        foregroundBase,
+        step
+    )
+
+    function colorString(indexOrColor: number | Color): string {
+        if (typeof indexOrColor === "number") {
+            return ramp(indexOrColor).hex()
+        } else {
+            return indexOrColor.hex()
+        }
+    }
+
+    function buildStyle(style: Styles): Style {
+        return {
+            background: colorString(styleDefinitions.background[style]),
+            border: colorString(styleDefinitions.border[style]),
+            foreground: colorString(styleDefinitions.foreground[style]),
+        }
     }
-  }
 
-  function buildStyle(style: Styles): Style {
     return {
-      background: colorString(styleDefinitions.background[style]),
-      border: colorString(styleDefinitions.border[style]),
-      foreground: colorString(styleDefinitions.foreground[style]),
-    };
-  }
-
-  return {
-    default: buildStyle("default"),
-    hovered: buildStyle("hovered"),
-    pressed: buildStyle("pressed"),
-    active: buildStyle("active"),
-    disabled: buildStyle("disabled"),
-    inverted: buildStyle("inverted"),
-  };
+        default: buildStyle("default"),
+        hovered: buildStyle("hovered"),
+        pressed: buildStyle("pressed"),
+        active: buildStyle("active"),
+        disabled: buildStyle("disabled"),
+        inverted: buildStyle("inverted"),
+    }
 }
 
 function buildStyleDefinition(
-  bgBase: number,
-  fgBase: number,
-  step: number = 0.08
+    bgBase: number,
+    fgBase: number,
+    step: number = 0.08
 ) {
-  return {
-    background: {
-      default: bgBase,
-      hovered: bgBase + step,
-      pressed: bgBase + step * 1.5,
-      active: bgBase + step * 2.2,
-      disabled: bgBase,
-      inverted: fgBase + step * 6,
-    },
-    border: {
-      default: bgBase + step * 1,
-      hovered: bgBase + step,
-      pressed: bgBase + step,
-      active: bgBase + step * 3,
-      disabled: bgBase + step * 0.5,
-      inverted: bgBase - step * 3,
-    },
-    foreground: {
-      default: fgBase,
-      hovered: fgBase,
-      pressed: fgBase,
-      active: fgBase + step * 6,
-      disabled: bgBase + step * 4,
-      inverted: bgBase + step * 2,
-    },
-  };
+    return {
+        background: {
+            default: bgBase,
+            hovered: bgBase + step,
+            pressed: bgBase + step * 1.5,
+            active: bgBase + step * 2.2,
+            disabled: bgBase,
+            inverted: fgBase + step * 6,
+        },
+        border: {
+            default: bgBase + step * 1,
+            hovered: bgBase + step,
+            pressed: bgBase + step,
+            active: bgBase + step * 3,
+            disabled: bgBase + step * 0.5,
+            inverted: bgBase - step * 3,
+        },
+        foreground: {
+            default: fgBase,
+            hovered: fgBase,
+            pressed: fgBase,
+            active: fgBase + step * 6,
+            disabled: bgBase + step * 4,
+            inverted: bgBase + step * 2,
+        },
+    }
 }

styles/src/themes/common/syntax.ts 🔗

@@ -0,0 +1,304 @@
+import deepmerge from "deepmerge"
+import { FontWeight, fontWeights } from "../../common"
+import { ColorScheme } from "./colorScheme"
+
+export interface SyntaxHighlightStyle {
+    color: string
+    weight?: FontWeight
+    underline?: boolean
+    italic?: boolean
+}
+
+export interface Syntax {
+    // == Text Styles ====== /
+    comment: SyntaxHighlightStyle
+    // elixir: doc comment
+    "comment.doc": SyntaxHighlightStyle
+    primary: SyntaxHighlightStyle
+    predictive: SyntaxHighlightStyle
+
+    // === Formatted Text ====== /
+    emphasis: SyntaxHighlightStyle
+    "emphasis.strong": SyntaxHighlightStyle
+    title: SyntaxHighlightStyle
+    linkUri: SyntaxHighlightStyle
+    linkText: SyntaxHighlightStyle
+    /** md: indented_code_block, fenced_code_block, code_span */
+    "text.literal": SyntaxHighlightStyle
+
+    // == Punctuation ====== /
+    punctuation: SyntaxHighlightStyle
+    /** Example: `(`, `[`, `{`...*/
+    "punctuation.bracket": SyntaxHighlightStyle
+    /**., ;*/
+    "punctuation.delimiter": SyntaxHighlightStyle
+    // js, ts: ${, } in a template literal
+    // yaml: *, &, ---, ...
+    "punctuation.special": SyntaxHighlightStyle
+    // md: list_marker_plus, list_marker_dot, etc
+    "punctuation.list_marker": SyntaxHighlightStyle
+
+    // == Strings ====== /
+
+    string: SyntaxHighlightStyle
+    // css: color_value
+    // js: this, super
+    // toml: offset_date_time, local_date_time...
+    "string.special": SyntaxHighlightStyle
+    // elixir: atom, quoted_atom, keyword, quoted_keyword
+    // ruby: simple_symbol, delimited_symbol...
+    "string.special.symbol"?: SyntaxHighlightStyle
+    // elixir, python, yaml...: escape_sequence
+    "string.escape"?: SyntaxHighlightStyle
+    // Regular expressions
+    "string.regex"?: SyntaxHighlightStyle
+
+    // == Types ====== /
+    // We allow Function here because all JS objects literals have this property
+    constructor: SyntaxHighlightStyle | Function
+    variant: SyntaxHighlightStyle
+    type: SyntaxHighlightStyle
+    // js: predefined_type
+    "type.builtin"?: SyntaxHighlightStyle
+
+    // == Values
+    variable: SyntaxHighlightStyle
+    // this, ...
+    // css: -- (var(--foo))
+    // lua: self
+    "variable.special"?: SyntaxHighlightStyle
+    // c: statement_identifier,
+    label: SyntaxHighlightStyle
+    // css: tag_name, nesting_selector, universal_selector...
+    tag: SyntaxHighlightStyle
+    // css: attribute, pseudo_element_selector (tag_name),
+    attribute: SyntaxHighlightStyle
+    // css: class_name, property_name, namespace_name...
+    property: SyntaxHighlightStyle
+    // true, false, null, nullptr
+    constant: SyntaxHighlightStyle
+    // css: @media, @import, @supports...
+    // js: declare, implements, interface, keyof, public...
+    keyword: SyntaxHighlightStyle
+    // note: js enum is currently defined as a keyword
+    enum: SyntaxHighlightStyle
+    // -, --, ->, !=, &&, ||, <=...
+    operator: SyntaxHighlightStyle
+    number: SyntaxHighlightStyle
+    boolean: SyntaxHighlightStyle
+    // elixir: __MODULE__, __DIR__, __ENV__, etc
+    // go: nil, iota
+    "constant.builtin"?: SyntaxHighlightStyle
+
+    // == Functions ====== /
+
+    function: SyntaxHighlightStyle
+    // lua: assert, error, loadfile, tostring, unpack...
+    "function.builtin"?: SyntaxHighlightStyle
+    // go: call_expression, method_declaration
+    // js: call_expression, method_definition, pair (key, arrow function)
+    // rust: function_item name: (identifier)
+    "function.definition"?: SyntaxHighlightStyle
+    // rust: macro_definition name: (identifier)
+    "function.special.definition"?: SyntaxHighlightStyle
+    "function.method"?: SyntaxHighlightStyle
+    // ruby: identifier/"defined?" // Nate note: I don't fully understand this one.
+    "function.method.builtin"?: SyntaxHighlightStyle
+
+    // == Unsorted ====== /
+    // lua: hash_bang_line
+    preproc: SyntaxHighlightStyle
+    // elixir, python: interpolation (ex: foo in ${foo})
+    // js: template_substitution
+    embedded: SyntaxHighlightStyle
+}
+
+export type ThemeSyntax = Partial<Syntax>
+
+const defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
+    weight: fontWeights.normal,
+    underline: false,
+    italic: false,
+}
+
+function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
+    // Make a temporary object that is allowed to be missing
+    // the "color" property for each style
+    const syntax: {
+        [key: string]: Omit<SyntaxHighlightStyle, "color">
+    } = {}
+
+    // then spread the default to each style
+    for (const key of Object.keys({} as Syntax)) {
+        syntax[key as keyof Syntax] = {
+            ...defaultSyntaxHighlightStyle,
+        }
+    }
+
+    const color = {
+        primary: colorScheme.ramps.neutral(1).hex(),
+        comment: colorScheme.ramps.neutral(0.71).hex(),
+        punctuation: colorScheme.ramps.neutral(0.86).hex(),
+        predictive: colorScheme.ramps.neutral(0.57).hex(),
+        emphasis: colorScheme.ramps.blue(0.5).hex(),
+        string: colorScheme.ramps.orange(0.5).hex(),
+        function: colorScheme.ramps.yellow(0.5).hex(),
+        type: colorScheme.ramps.cyan(0.5).hex(),
+        constructor: colorScheme.ramps.blue(0.5).hex(),
+        variant: colorScheme.ramps.blue(0.5).hex(),
+        property: colorScheme.ramps.blue(0.5).hex(),
+        enum: colorScheme.ramps.orange(0.5).hex(),
+        operator: colorScheme.ramps.orange(0.5).hex(),
+        number: colorScheme.ramps.green(0.5).hex(),
+        boolean: colorScheme.ramps.green(0.5).hex(),
+        constant: colorScheme.ramps.green(0.5).hex(),
+        keyword: colorScheme.ramps.blue(0.5).hex(),
+    }
+
+    // Then assign colors and use Syntax to enforce each style getting it's own color
+    const defaultSyntax: Syntax = {
+        ...syntax,
+        comment: {
+            color: color.comment,
+        },
+        "comment.doc": {
+            color: color.comment,
+        },
+        primary: {
+            color: color.primary,
+        },
+        predictive: {
+            color: color.predictive,
+        },
+        emphasis: {
+            color: color.emphasis,
+        },
+        "emphasis.strong": {
+            color: color.emphasis,
+            weight: fontWeights.bold,
+        },
+        title: {
+            color: color.primary,
+            weight: fontWeights.bold,
+        },
+        linkUri: {
+            color: colorScheme.ramps.green(0.5).hex(),
+            underline: true,
+        },
+        linkText: {
+            color: colorScheme.ramps.orange(0.5).hex(),
+            italic: true,
+        },
+        "text.literal": {
+            color: color.string,
+        },
+        punctuation: {
+            color: color.punctuation,
+        },
+        "punctuation.bracket": {
+            color: color.punctuation,
+        },
+        "punctuation.delimiter": {
+            color: color.punctuation,
+        },
+        "punctuation.special": {
+            color: colorScheme.ramps.neutral(0.86).hex(),
+        },
+        "punctuation.list_marker": {
+            color: color.punctuation,
+        },
+        string: {
+            color: color.string,
+        },
+        "string.special": {
+            color: color.string,
+        },
+        "string.special.symbol": {
+            color: color.string,
+        },
+        "string.escape": {
+            color: color.comment,
+        },
+        "string.regex": {
+            color: color.string,
+        },
+        constructor: {
+            color: colorScheme.ramps.blue(0.5).hex(),
+        },
+        variant: {
+            color: colorScheme.ramps.blue(0.5).hex(),
+        },
+        type: {
+            color: color.type,
+        },
+        variable: {
+            color: color.primary,
+        },
+        label: {
+            color: colorScheme.ramps.blue(0.5).hex(),
+        },
+        tag: {
+            color: colorScheme.ramps.blue(0.5).hex(),
+        },
+        attribute: {
+            color: colorScheme.ramps.blue(0.5).hex(),
+        },
+        property: {
+            color: colorScheme.ramps.blue(0.5).hex(),
+        },
+        constant: {
+            color: color.constant,
+        },
+        keyword: {
+            color: color.keyword,
+        },
+        enum: {
+            color: color.enum,
+        },
+        operator: {
+            color: color.operator,
+        },
+        number: {
+            color: color.number,
+        },
+        boolean: {
+            color: color.boolean,
+        },
+        function: {
+            color: color.function,
+        },
+        preproc: {
+            color: color.primary,
+        },
+        embedded: {
+            color: color.primary,
+        },
+    }
+
+    return defaultSyntax
+}
+
+function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
+    if (!colorScheme.syntax) {
+        return defaultSyntax
+    }
+
+    return deepmerge<Syntax, Partial<ThemeSyntax>>(
+        defaultSyntax,
+        colorScheme.syntax,
+        {
+            arrayMerge: (destinationArray, sourceArray) => [
+                ...destinationArray,
+                ...sourceArray,
+            ],
+        }
+    )
+}
+
+export function buildSyntax(colorScheme: ColorScheme): Syntax {
+    const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
+
+    const syntax = mergeSyntax(defaultSyntax, colorScheme)
+
+    return syntax
+}

styles/src/themes/common/theme.ts 🔗

@@ -1,165 +0,0 @@
-import { Scale } from "chroma-js";
-import { FontWeight } from "../../common";
-import { withOpacity } from "../../utils/color";
-
-export interface SyntaxHighlightStyle {
-  color: string;
-  weight?: FontWeight;
-  underline?: boolean;
-  italic?: boolean;
-}
-
-export interface Player {
-  baseColor: string;
-  cursorColor: string;
-  selectionColor: string;
-  borderColor: string;
-}
-export function buildPlayer(
-  color: string,
-  cursorOpacity?: number,
-  selectionOpacity?: number,
-  borderOpacity?: number
-) {
-  return {
-    baseColor: color,
-    cursorColor: withOpacity(color, cursorOpacity || 1.0),
-    selectionColor: withOpacity(color, selectionOpacity || 0.24),
-    borderColor: withOpacity(color, borderOpacity || 0.8),
-  };
-}
-
-export interface BackgroundColorSet {
-  base: string;
-  hovered: string;
-  active: string;
-}
-
-export interface Syntax {
-  primary: SyntaxHighlightStyle;
-  comment: SyntaxHighlightStyle;
-  punctuation: SyntaxHighlightStyle;
-  constant: SyntaxHighlightStyle;
-  keyword: SyntaxHighlightStyle;
-  function: SyntaxHighlightStyle;
-  type: SyntaxHighlightStyle;
-  constructor: SyntaxHighlightStyle;
-  property: SyntaxHighlightStyle;
-  enum: SyntaxHighlightStyle;
-  operator: SyntaxHighlightStyle;
-  string: SyntaxHighlightStyle;
-  number: SyntaxHighlightStyle;
-  boolean: SyntaxHighlightStyle;
-  predictive: SyntaxHighlightStyle;
-  title: SyntaxHighlightStyle;
-  emphasis: SyntaxHighlightStyle;
-  linkUri: SyntaxHighlightStyle;
-  linkText: SyntaxHighlightStyle;
-
-  [key: string]: SyntaxHighlightStyle;
-}
-
-export default interface Theme {
-  name: string;
-  isLight: boolean;
-  backgroundColor: {
-    // Basically just Title Bar
-    // Lowest background level
-    100: BackgroundColorSet;
-    // Tab bars, panels, popovers
-    // Mid-ground
-    300: BackgroundColorSet;
-    // The editor
-    // Foreground
-    500: BackgroundColorSet;
-    // Hacks for elements on top of the midground
-    // Buttons in a panel, tab bar, or panel
-    on300: BackgroundColorSet;
-    // Hacks for elements on top of the editor
-    on500: BackgroundColorSet;
-    ok: BackgroundColorSet;
-    on500Ok: BackgroundColorSet;
-    error: BackgroundColorSet;
-    on500Error: BackgroundColorSet;
-    warning: BackgroundColorSet;
-    on500Warning: BackgroundColorSet;
-    info: BackgroundColorSet;
-    on500Info: BackgroundColorSet;
-  };
-  borderColor: {
-    primary: string;
-    secondary: string;
-    muted: string;
-    active: string;
-    /**
-     * Used for rendering borders on top of media like avatars, images, video, etc.
-     */
-    onMedia: string;
-    ok: string;
-    error: string;
-    warning: string;
-    info: string;
-  };
-  textColor: {
-    primary: string;
-    secondary: string;
-    muted: string;
-    placeholder: string;
-    active: string;
-    feature: string;
-    ok: string;
-    error: string;
-    warning: string;
-    info: string;
-    onMedia: string;
-  };
-  iconColor: {
-    primary: string;
-    secondary: string;
-    muted: string;
-    placeholder: string;
-    active: string;
-    feature: string;
-    ok: string;
-    error: string;
-    warning: string;
-    info: string;
-  };
-  editor: {
-    background: string;
-    indent_guide: string;
-    indent_guide_active: string;
-    line: {
-      active: string;
-      highlighted: string;
-    };
-    highlight: {
-      selection: string;
-      occurrence: string;
-      activeOccurrence: string;
-      matchingBracket: string;
-      match: string;
-      activeMatch: string;
-      related: string;
-    };
-    gutter: {
-      primary: string;
-      active: string;
-    };
-  };
-
-  syntax: Syntax;
-
-  player: {
-    1: Player;
-    2: Player;
-    3: Player;
-    4: Player;
-    5: Player;
-    6: Player;
-    7: Player;
-    8: Player;
-  };
-  shadow: string;
-  ramps: { [rampName: string]: Scale };
-}

styles/src/themes/gruvbox-common.ts 🔗

@@ -0,0 +1,256 @@
+import chroma from "chroma-js"
+import { Meta, ThemeSyntax } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
+
+const name = "Gruvbox"
+
+const color = {
+    dark0_hard: "#1d2021",
+    dark0: "#282828",
+    dark0_soft: "#32302f",
+    dark1: "#3c3836",
+    dark2: "#504945",
+    dark3: "#665c54",
+    dark4: "#7c6f64",
+    dark4_256: "#7c6f64",
+
+    gray_245: "#928374",
+    gray_244: "#928374",
+
+    light0_hard: "#f9f5d7",
+    light0: "#fbf1c7",
+    light0_soft: "#f2e5bc",
+    light1: "#ebdbb2",
+    light2: "#d5c4a1",
+    light3: "#bdae93",
+    light4: "#a89984",
+    light4_256: "#a89984",
+
+    bright_red: "#fb4934",
+    bright_green: "#b8bb26",
+    bright_yellow: "#fabd2f",
+    bright_blue: "#83a598",
+    bright_purple: "#d3869b",
+    bright_aqua: "#8ec07c",
+    bright_orange: "#fe8019",
+
+    neutral_red: "#cc241d",
+    neutral_green: "#98971a",
+    neutral_yellow: "#d79921",
+    neutral_blue: "#458588",
+    neutral_purple: "#b16286",
+    neutral_aqua: "#689d6a",
+    neutral_orange: "#d65d0e",
+
+    faded_red: "#9d0006",
+    faded_green: "#79740e",
+    faded_yellow: "#b57614",
+    faded_blue: "#076678",
+    faded_purple: "#8f3f71",
+    faded_aqua: "#427b58",
+    faded_orange: "#af3a03",
+}
+
+interface ThemeColors {
+    red: string
+    green: string
+    yellow: string
+    blue: string
+    purple: string
+    aqua: string
+    orange: string
+    gray: string
+}
+
+const darkNeutrals = [
+    color.dark1,
+    color.dark2,
+    color.dark3,
+    color.dark4,
+    color.light4,
+    color.light3,
+    color.light2,
+    color.light1,
+    color.light0,
+]
+
+const dark: ThemeColors = {
+    red: color.bright_red,
+    green: color.bright_green,
+    yellow: color.bright_yellow,
+    blue: color.bright_blue,
+    purple: color.bright_purple,
+    aqua: color.bright_aqua,
+    orange: color.bright_orange,
+    gray: color.light4,
+}
+
+const lightNeutrals = [
+    color.light1,
+    color.light2,
+    color.light3,
+    color.light4,
+    color.dark4,
+    color.dark3,
+    color.dark2,
+    color.dark1,
+    color.dark0,
+]
+
+const light: ThemeColors = {
+    red: color.faded_red,
+    green: color.faded_green,
+    yellow: color.faded_yellow,
+    blue: color.faded_blue,
+    purple: color.faded_purple,
+    aqua: color.faded_aqua,
+    orange: color.faded_orange,
+    gray: color.dark4,
+}
+
+const darkHardNeutral = [color.dark0_hard, ...darkNeutrals]
+const darkNeutral = [color.dark0, ...darkNeutrals]
+const darkSoftNeutral = [color.dark0_soft, ...darkNeutrals]
+
+const lightHardNeutral = [color.light0_hard, ...lightNeutrals]
+const lightNeutral = [color.light0, ...lightNeutrals]
+const lightSoftNeutral = [color.light0_soft, ...lightNeutrals]
+
+interface Variant {
+    name: string
+    appearance: "light" | "dark"
+    colors: ThemeColors
+}
+
+const variant: Variant[] = [
+    {
+        name: "Dark Hard",
+        appearance: "dark",
+        colors: dark,
+    },
+    {
+        name: "Dark",
+        appearance: "dark",
+        colors: dark,
+    },
+    {
+        name: "Dark Soft",
+        appearance: "dark",
+        colors: dark,
+    },
+    {
+        name: "Light Hard",
+        appearance: "light",
+        colors: light,
+    },
+    {
+        name: "Light",
+        appearance: "light",
+
+        colors: light,
+    },
+    {
+        name: "Light Soft",
+        appearance: "light",
+        colors: light,
+    },
+]
+
+const buildVariant = (variant: Variant) => {
+    const { colors } = variant
+
+    const name = `Gruvbox ${variant.name}`
+
+    const isLight = variant.appearance === "light"
+
+    let neutral: string[] = []
+
+    switch (variant.name) {
+        case "Dark Hard": {
+            neutral = darkHardNeutral
+            break
+        }
+        case "Dark": {
+            neutral = darkNeutral
+            break
+        }
+        case "Dark Soft": {
+            neutral = darkSoftNeutral
+            break
+        }
+        case "Light Hard": {
+            neutral = lightHardNeutral
+            break
+        }
+        case "Light": {
+            neutral = lightNeutral
+            break
+        }
+        case "Light Soft": {
+            neutral = lightSoftNeutral
+            break
+        }
+    }
+
+    const ramps = {
+        neutral: chroma.scale(isLight ? neutral.reverse() : neutral),
+        red: colorRamp(chroma(variant.colors.red)),
+        orange: colorRamp(chroma(variant.colors.orange)),
+        yellow: colorRamp(chroma(variant.colors.yellow)),
+        green: colorRamp(chroma(variant.colors.green)),
+        cyan: colorRamp(chroma(variant.colors.aqua)),
+        blue: colorRamp(chroma(variant.colors.blue)),
+        violet: colorRamp(chroma(variant.colors.purple)),
+        magenta: colorRamp(chroma(variant.colors.gray)),
+    }
+
+    const syntax: ThemeSyntax = {
+        primary: { color: neutral[isLight ? 0 : 8] },
+        "text.literal": { color: colors.blue },
+        comment: { color: colors.gray },
+        punctuation: { color: neutral[isLight ? 1 : 7] },
+        "punctuation.bracket": { color: neutral[isLight ? 3 : 5] },
+        "punctuation.list_marker": { color: neutral[isLight ? 0 : 8] },
+        operator: { color: colors.aqua },
+        boolean: { color: colors.purple },
+        number: { color: colors.purple },
+        string: { color: colors.green },
+        "string.special": { color: colors.purple },
+        "string.special.symbol": { color: colors.aqua },
+        "string.regex": { color: colors.orange },
+        type: { color: colors.yellow },
+        enum: { color: colors.orange },
+        tag: { color: colors.aqua },
+        constant: { color: colors.yellow },
+        keyword: { color: colors.red },
+        function: { color: colors.green },
+        "function.builtin": { color: colors.red },
+        variable: { color: colors.blue },
+        property: { color: neutral[isLight ? 0 : 8] },
+        embedded: { color: colors.aqua },
+        linkText: { color: colors.aqua },
+        linkUri: { color: colors.purple },
+        title: { color: colors.green },
+    }
+
+    return createColorScheme(name, isLight, ramps, syntax)
+}
+
+// Variants
+export const darkHard = buildVariant(variant[0])
+export const darkDefault = buildVariant(variant[1])
+export const darkSoft = buildVariant(variant[2])
+export const lightHard = buildVariant(variant[3])
+export const lightDefault = buildVariant(variant[4])
+export const lightSoft = buildVariant(variant[5])
+
+export const meta: Meta = {
+    name,
+    license: {
+        SPDX: "MIT", // "MIT/X11"
+        license_text:

styles/src/themes/gruvbox-dark-hard.ts 🔗

@@ -0,0 +1,6 @@
+import { darkHard as dark, meta as commonMeta } from "./gruvbox-common"
+
+let meta = { ...commonMeta }
+meta.name = `${commonMeta.name} Dark Hard`
+
+export { dark, meta }

styles/src/themes/gruvbox-dark-soft.ts 🔗

@@ -0,0 +1,6 @@
+import { darkSoft as dark, meta as commonMeta } from "./gruvbox-common"
+
+let meta = { ...commonMeta }
+meta.name = `${commonMeta.name} Dark Soft`
+
+export { dark, meta }

styles/src/themes/gruvbox-dark.ts 🔗

@@ -0,0 +1,6 @@
+import { darkDefault as dark, meta as commonMeta } from "./gruvbox-common"
+
+let meta = { ...commonMeta }
+meta.name = `${commonMeta.name} Dark`
+
+export { dark, meta }

styles/src/themes/gruvbox-light-hard.ts 🔗

@@ -0,0 +1,6 @@
+import { lightHard as light, meta as commonMeta } from "./gruvbox-common"
+
+let meta = { ...commonMeta }
+meta.name = `${commonMeta.name} Dark Soft`
+
+export { light, meta }

styles/src/themes/gruvbox-light-soft.ts 🔗

@@ -0,0 +1,6 @@
+import { lightSoft as light, meta as commonMeta } from "./gruvbox-common"
+
+let meta = { ...commonMeta }
+meta.name = `${commonMeta.name} Light Soft`
+
+export { light, meta }

styles/src/themes/gruvbox-light.ts 🔗

@@ -0,0 +1,6 @@
+import { lightDefault as light, meta as commonMeta } from "./gruvbox-common"
+
+let meta = { ...commonMeta }
+meta.name = `${commonMeta.name} Light`
+
+export { light, meta }

styles/src/themes/one-dark.ts 🔗

@@ -1,40 +1,85 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { fontWeights } from "../common"
+import { Meta, ThemeSyntax } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "One Dark";
+const name = "One Dark"
 
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma
-    .scale([
-      "#282c34",
-      "#353b45",
-      "#3e4451",
-      "#545862",
-      "#565c64",
-      "#abb2bf",
-      "#b6bdca",
-      "#c8ccd4",
-    ])
-    .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+const color = {
+    white: "#ACB2BE",
+    grey: "#5D636F",
+    red: "#D07277",
+    darkRed: "#B1574B",
+    orange: "#C0966B",
+    yellow: "#DFC184",
+    green: "#A1C181",
+    teal: "#6FB4C0",
+    blue: "#74ADE9",
+    purple: "#B478CF",
+}
+
+const ramps = {
+    neutral: chroma
+        .scale([
+            "#282c34",
+            "#353b45",
+            "#3e4451",
+            "#545862",
+            "#565c64",
+            "#abb2bf",
+            "#b6bdca",
+            "#c8ccd4",
+        ])
+        .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+    red: colorRamp(chroma(color.red)),
+    orange: colorRamp(chroma(color.orange)),
+    yellow: colorRamp(chroma(color.yellow)),
+    green: colorRamp(chroma(color.green)),
+    cyan: colorRamp(chroma(color.teal)),
+    blue: colorRamp(chroma(color.blue)),
+    violet: colorRamp(chroma(color.purple)),
+    magenta: colorRamp(chroma("#be5046")),
+}
+
+const syntax: ThemeSyntax = {
+    boolean: { color: color.orange },
+    comment: { color: color.grey },
+    enum: { color: color.red },
+    "emphasis.strong": { color: color.orange },
+    function: { color: color.blue },
+    keyword: { color: color.purple },
+    linkText: { color: color.blue, italic: false },
+    linkUri: { color: color.teal },
+    number: { color: color.orange },
+    constant: { color: color.yellow },
+    operator: { color: color.teal },
+    primary: { color: color.white },
+    property: { color: color.red },
+    punctuation: { color: color.white },
+    "punctuation.list_marker": { color: color.red },
+    "punctuation.special": { color: color.darkRed },
+    string: { color: color.green },
+    title: { color: color.red, weight: fontWeights.normal },
+    "text.literal": { color: color.green },
+    type: { color: color.teal },
+    "variable.special": { color: color.orange },
+    variant: { color: color.blue },
+    constructor: { color: color.blue },
+}
 
-  red: colorRamp(chroma("#e06c75")),
-  orange: colorRamp(chroma("#d19a66")),
-  yellow: colorRamp(chroma("#e5c07b")),
-  green: colorRamp(chroma("#98c379")),
-  cyan: colorRamp(chroma("#56b6c2")),
-  blue: colorRamp(chroma("#61afef")),
-  violet: colorRamp(chroma("#c678dd")),
-  magenta: colorRamp(chroma("#be5046")),
-});
+export const dark = createColorScheme(name, false, ramps, syntax)
 
 export const meta: Meta = {
-  name,
-  author: "simurai",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
-    license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8"
-  },
-  url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui"
+    name,
+    author: "simurai",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
+            license_checksum:
+                "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8",
+        },
+    },
+    url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
 }

styles/src/themes/one-light.ts 🔗

@@ -1,39 +1,84 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { fontWeights } from "../common"
+import { Meta, ThemeSyntax } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "One Light";
+const name = "One Light"
 
-export const light = createColorScheme(`${name}`, true, {
-  neutral: chroma.scale([
-    "#090a0b",
-    "#202227",
-    "#383a42",
-    "#696c77",
-    "#a0a1a7",
-    "#e5e5e6",
-    "#f0f0f1",
-    "#fafafa",
-  ])
-    .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+const color = {
+    black: "#383A41",
+    grey: "#A2A3A7",
+    red: "#D36050",
+    darkRed: "#B92C46",
+    orange: "#AD6F26",
+    yellow: "#DFC184",
+    green: "#659F58",
+    teal: "#3982B7",
+    blue: "#5B79E3",
+    purple: "#A449AB",
+    magenta: "#994EA6",
+}
+
+const ramps = {
+    neutral: chroma
+        .scale([
+            "#383A41",
+            "#535456",
+            "#696c77",
+            "#9D9D9F",
+            "#A9A9A9",
+            "#DBDBDC",
+            "#EAEAEB",
+            "#FAFAFA",
+        ])
+        .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
+    red: colorRamp(chroma(color.red)),
+    orange: colorRamp(chroma(color.orange)),
+    yellow: colorRamp(chroma(color.yellow)),
+    green: colorRamp(chroma(color.green)),
+    cyan: colorRamp(chroma(color.teal)),
+    blue: colorRamp(chroma(color.blue)),
+    violet: colorRamp(chroma(color.purple)),
+    magenta: colorRamp(chroma(color.magenta)),
+}
+
+const syntax: ThemeSyntax = {
+    boolean: { color: color.orange },
+    comment: { color: color.grey },
+    enum: { color: color.red },
+    "emphasis.strong": { color: color.orange },
+    function: { color: color.blue },
+    keyword: { color: color.purple },
+    linkText: { color: color.blue },
+    linkUri: { color: color.teal },
+    number: { color: color.orange },
+    operator: { color: color.teal },
+    primary: { color: color.black },
+    property: { color: color.red },
+    punctuation: { color: color.black },
+    "punctuation.list_marker": { color: color.red },
+    "punctuation.special": { color: color.darkRed },
+    string: { color: color.green },
+    title: { color: color.red, weight: fontWeights.normal },
+    "text.literal": { color: color.green },
+    type: { color: color.teal },
+    "variable.special": { color: color.orange },
+    variant: { color: color.blue },
+}
 
-  red: colorRamp(chroma("#ca1243")),
-  orange: colorRamp(chroma("#d75f00")),
-  yellow: colorRamp(chroma("#c18401")),
-  green: colorRamp(chroma("#50a14f")),
-  cyan: colorRamp(chroma("#0184bc")),
-  blue: colorRamp(chroma("#4078f2")),
-  violet: colorRamp(chroma("#a626a4")),
-  magenta: colorRamp(chroma("#986801")),
-});
+export const light = createColorScheme(name, true, ramps, syntax)
 
 export const meta: Meta = {
-  name,
-  author: "simurai",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
-    license_checksum: "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8"
-  },
-  url: "https://github.com/atom/atom/tree/master/packages/one-light-ui"
+    name,
+    author: "simurai",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
+            license_checksum:
+                "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8",
+        },
+    },
+    url: "https://github.com/atom/atom/tree/master/packages/one-light-ui",
 }

styles/src/themes/rose-pine-dawn.ts 🔗

@@ -1,41 +1,45 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Rosé Pine Dawn";
+const name = "Rosé Pine Dawn"
 
 const ramps = {
-  neutral: chroma
-    .scale([
-      "#575279",
-      "#797593",
-      "#9893A5",
-      "#B5AFB8",
-      "#D3CCCC",
-      "#F2E9E1",
-      "#FFFAF3",
-      "#FAF4ED",
-    ])
-    .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
-  red: colorRamp(chroma("#B4637A")),
-  orange: colorRamp(chroma("#D7827E")),
-  yellow: colorRamp(chroma("#EA9D34")),
-  green: colorRamp(chroma("#679967")),
-  cyan: colorRamp(chroma("#286983")),
-  blue: colorRamp(chroma("#56949F")),
-  violet: colorRamp(chroma("#907AA9")),
-  magenta: colorRamp(chroma("#79549F")),
-};
+    neutral: chroma
+        .scale([
+            "#575279",
+            "#797593",
+            "#9893A5",
+            "#B5AFB8",
+            "#D3CCCC",
+            "#F2E9E1",
+            "#FFFAF3",
+            "#FAF4ED",
+        ])
+        .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
+    red: colorRamp(chroma("#B4637A")),
+    orange: colorRamp(chroma("#D7827E")),
+    yellow: colorRamp(chroma("#EA9D34")),
+    green: colorRamp(chroma("#679967")),
+    cyan: colorRamp(chroma("#286983")),
+    blue: colorRamp(chroma("#56949F")),
+    violet: colorRamp(chroma("#907AA9")),
+    magenta: colorRamp(chroma("#79549F")),
+}
 
-export const light = createColorScheme(`${name}`, true, ramps);
+export const light = createColorScheme(name, true, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "edunfelt",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-    license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a"
-  },
-  url: "https://github.com/edunfelt/base16-rose-pine-scheme"
-}
+    name,
+    author: "edunfelt",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
+            license_checksum:
+                "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
+        },
+    },
+    url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+}

styles/src/themes/rose-pine-moon.ts 🔗

@@ -1,41 +1,45 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Rosé Pine Moon";
+const name = "Rosé Pine Moon"
 
 const ramps = {
-  neutral: chroma
-    .scale([
-      "#232136",
-      "#2A273F",
-      "#393552",
-      "#3E3A53",
-      "#56526C",
-      "#6E6A86",
-      "#908CAA",
-      "#E0DEF4",
-    ])
-    .domain([0, 0.3, 0.55, 1]),
-  red: colorRamp(chroma("#EB6F92")),
-  orange: colorRamp(chroma("#EBBCBA")),
-  yellow: colorRamp(chroma("#F6C177")),
-  green: colorRamp(chroma("#8DBD8D")),
-  cyan: colorRamp(chroma("#409BBE")),
-  blue: colorRamp(chroma("#9CCFD8")),
-  violet: colorRamp(chroma("#C4A7E7")),
-  magenta: colorRamp(chroma("#AB6FE9")),
-};
+    neutral: chroma
+        .scale([
+            "#232136",
+            "#2A273F",
+            "#393552",
+            "#3E3A53",
+            "#56526C",
+            "#6E6A86",
+            "#908CAA",
+            "#E0DEF4",
+        ])
+        .domain([0, 0.3, 0.55, 1]),
+    red: colorRamp(chroma("#EB6F92")),
+    orange: colorRamp(chroma("#EBBCBA")),
+    yellow: colorRamp(chroma("#F6C177")),
+    green: colorRamp(chroma("#8DBD8D")),
+    cyan: colorRamp(chroma("#409BBE")),
+    blue: colorRamp(chroma("#9CCFD8")),
+    violet: colorRamp(chroma("#C4A7E7")),
+    magenta: colorRamp(chroma("#AB6FE9")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(name, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "edunfelt",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-    license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a"
-  },
-  url: "https://github.com/edunfelt/base16-rose-pine-scheme"
-}
+    name,
+    author: "edunfelt",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
+            license_checksum:
+                "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
+        },
+    },
+    url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+}

styles/src/themes/rose-pine.ts 🔗

@@ -1,39 +1,43 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Rosé Pine";
+const name = "Rosé Pine"
 
 const ramps = {
-  neutral: chroma.scale([
-    "#191724",
-    "#1f1d2e",
-    "#26233A",
-    "#3E3A53",
-    "#56526C",
-    "#6E6A86",
-    "#908CAA",
-    "#E0DEF4",
-  ]),
-  red: colorRamp(chroma("#EB6F92")),
-  orange: colorRamp(chroma("#EBBCBA")),
-  yellow: colorRamp(chroma("#F6C177")),
-  green: colorRamp(chroma("#8DBD8D")),
-  cyan: colorRamp(chroma("#409BBE")),
-  blue: colorRamp(chroma("#9CCFD8")),
-  violet: colorRamp(chroma("#C4A7E7")),
-  magenta: colorRamp(chroma("#AB6FE9")),
-};
+    neutral: chroma.scale([
+        "#191724",
+        "#1f1d2e",
+        "#26233A",
+        "#3E3A53",
+        "#56526C",
+        "#6E6A86",
+        "#908CAA",
+        "#E0DEF4",
+    ]),
+    red: colorRamp(chroma("#EB6F92")),
+    orange: colorRamp(chroma("#EBBCBA")),
+    yellow: colorRamp(chroma("#F6C177")),
+    green: colorRamp(chroma("#8DBD8D")),
+    cyan: colorRamp(chroma("#409BBE")),
+    blue: colorRamp(chroma("#9CCFD8")),
+    violet: colorRamp(chroma("#C4A7E7")),
+    magenta: colorRamp(chroma("#AB6FE9")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(name, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "edunfelt",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-    license_checksum: "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a"
-  },
-  url: "https://github.com/edunfelt/base16-rose-pine-scheme"
-}
+    name,
+    author: "edunfelt",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
+            license_checksum:
+                "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
+        },
+    },
+    url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+}

styles/src/themes/sandcastle.ts 🔗

@@ -1,40 +1,43 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Sandcastle";
+const name = "Sandcastle"
 
 const ramps = {
-  neutral: chroma.scale([
-    "#282c34",
-    "#2c323b",
-    "#3e4451",
-    "#665c54",
-    "#928374",
-    "#a89984",
-    "#d5c4a1",
-    "#fdf4c1",
-  ]),
-  red: colorRamp(chroma("#B4637A")),
-  orange: colorRamp(chroma("#a07e3b")),
-  yellow: colorRamp(chroma("#a07e3b")),
-  green: colorRamp(chroma("#83a598")),
-  cyan: colorRamp(chroma("#83a598")),
-  blue: colorRamp(chroma("#528b8b")),
-  violet: colorRamp(chroma("#d75f5f")),
-  magenta: colorRamp(chroma("#a87322")),
-};
+    neutral: chroma.scale([
+        "#282c34",
+        "#2c323b",
+        "#3e4451",
+        "#665c54",
+        "#928374",
+        "#a89984",
+        "#d5c4a1",
+        "#fdf4c1",
+    ]),
+    red: colorRamp(chroma("#B4637A")),
+    orange: colorRamp(chroma("#a07e3b")),
+    yellow: colorRamp(chroma("#a07e3b")),
+    green: colorRamp(chroma("#83a598")),
+    cyan: colorRamp(chroma("#83a598")),
+    blue: colorRamp(chroma("#528b8b")),
+    violet: colorRamp(chroma("#d75f5f")),
+    magenta: colorRamp(chroma("#a87322")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(name, false, ramps)
 
 export const meta: Meta = {
-  name,
-  author: "gessig",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE",
-    license_checksum: "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc"
-  },
-  url: "https://github.com/gessig/base16-sandcastle-scheme"
+    name,
+    author: "gessig",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE",
+            license_checksum:
+                "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc",
+        },
+    },
+    url: "https://github.com/gessig/base16-sandcastle-scheme",
 }
-

styles/src/themes/solarized.ts 🔗

@@ -1,43 +1,46 @@
-import chroma from "chroma-js";
-import { Meta as Metadata } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta as Metadata } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Solarized";
+const name = "Solarized"
 
 const ramps = {
-  neutral: chroma
-    .scale([
-      "#002b36",
-      "#073642",
-      "#586e75",
-      "#657b83",
-      "#839496",
-      "#93a1a1",
-      "#eee8d5",
-      "#fdf6e3",
-    ])
-    .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
-  red: colorRamp(chroma("#dc322f")),
-  orange: colorRamp(chroma("#cb4b16")),
-  yellow: colorRamp(chroma("#b58900")),
-  green: colorRamp(chroma("#859900")),
-  cyan: colorRamp(chroma("#2aa198")),
-  blue: colorRamp(chroma("#268bd2")),
-  violet: colorRamp(chroma("#6c71c4")),
-  magenta: colorRamp(chroma("#d33682")),
-};
+    neutral: chroma
+        .scale([
+            "#002b36",
+            "#073642",
+            "#586e75",
+            "#657b83",
+            "#839496",
+            "#93a1a1",
+            "#eee8d5",
+            "#fdf6e3",
+        ])
+        .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
+    red: colorRamp(chroma("#dc322f")),
+    orange: colorRamp(chroma("#cb4b16")),
+    yellow: colorRamp(chroma("#b58900")),
+    green: colorRamp(chroma("#859900")),
+    cyan: colorRamp(chroma("#2aa198")),
+    blue: colorRamp(chroma("#268bd2")),
+    violet: colorRamp(chroma("#6c71c4")),
+    magenta: colorRamp(chroma("#d33682")),
+}
 
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);
+export const dark = createColorScheme(`${name} Dark`, false, ramps)
+export const light = createColorScheme(`${name} Light`, true, ramps)
 
 export const meta: Metadata = {
-  name,
-  author: "Ethan Schoonover",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE",
-    license_checksum: "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6"
-  },
-  url: "https://github.com/altercation/solarized"
+    name,
+    author: "Ethan Schoonover",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE",
+            license_checksum:
+                "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6",
+        },
+    },
+    url: "https://github.com/altercation/solarized",
 }
-

styles/src/themes/staff/abruzzo.ts 🔗

@@ -1,31 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Abruzzo";
-const author = "slightknack <hey@isaac.sh>";
-const url = "https://github.com/slightknack";
-const license = {
-  type: "",
-  url: ""
-}
-
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#1b0d05",
-    "#2c1e18",
-    "#654035",
-    "#9d5e4a",
-    "#b37354",
-    "#c1825a",
-    "#dda66e",
-    "#fbf3e2",
-  ]),
-  red: colorRamp(chroma("#e594c4")),
-  orange: colorRamp(chroma("#d9e87e")),
-  yellow: colorRamp(chroma("#fd9d83")),
-  green: colorRamp(chroma("#96adf7")),
-  cyan: colorRamp(chroma("#fc798f")),
-  blue: colorRamp(chroma("#BCD0F5")),
-  violet: colorRamp(chroma("#dac5eb")),
-  magenta: colorRamp(chroma("#c1a3ef")),
-});

styles/src/themes/staff/atelier-dune.ts 🔗

@@ -1,34 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Atelier Dune";
-const author = "atelierbram";
-const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/";
-const license = {
-  type: "MIT",
-  url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
-};
-
-const ramps = {
-  neutral: chroma.scale([
-    "#20201d",
-    "#292824",
-    "#6e6b5e",
-    "#7d7a68",
-    "#999580",
-    "#a6a28c",
-    "#e8e4cf",
-    "#fefbec",
-  ]),
-  red: colorRamp(chroma("#d73737")),
-  orange: colorRamp(chroma("#b65611")),
-  yellow: colorRamp(chroma("#ae9513")),
-  green: colorRamp(chroma("#60ac39")),
-  cyan: colorRamp(chroma("#1fad83")),
-  blue: colorRamp(chroma("#6684e1")),
-  violet: colorRamp(chroma("#b854d4")),
-  magenta: colorRamp(chroma("#d43552")),
-};
-
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);

styles/src/themes/staff/atelier-heath.ts 🔗

@@ -1,53 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Atelier Heath";
-const author = "atelierbram";
-const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/";
-const license = {
-  type: "MIT",
-  url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name} Dark`, false, {
-  neutral: chroma.scale([
-    "#1b181b",
-    "#292329",
-    "#695d69",
-    "#776977",
-    "#9e8f9e",
-    "#ab9bab",
-    "#d8cad8",
-    "#f7f3f7",
-  ]),
-  red: colorRamp(chroma("#ca402b")),
-  orange: colorRamp(chroma("#a65926")),
-  yellow: colorRamp(chroma("#bb8a35")),
-  green: colorRamp(chroma("#918b3b")),
-  cyan: colorRamp(chroma("#159393")),
-  blue: colorRamp(chroma("#516aec")),
-  violet: colorRamp(chroma("#7b59c0")),
-  magenta: colorRamp(chroma("#cc33cc")),
-});
-
-export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma.scale([
-    "#161b1d",
-    "#1f292e",
-    "#516d7b",
-    "#5a7b8c",
-    "#7195a8",
-    "#7ea2b4",
-    "#c1e4f6",
-    "#ebf8ff",
-  ]),
-  red: colorRamp(chroma("#d22d72")),
-  orange: colorRamp(chroma("#935c25")),
-  yellow: colorRamp(chroma("#8a8a0f")),
-  green: colorRamp(chroma("#568c3b")),
-  cyan: colorRamp(chroma("#2d8f6f")),
-  blue: colorRamp(chroma("#257fad")),
-  violet: colorRamp(chroma("#6b6bb8")),
-  magenta: colorRamp(chroma("#b72dd2")),
-});

styles/src/themes/staff/atelier-seaside.ts 🔗

@@ -1,34 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Atelier Seaside";
-const author = "atelierbram";
-const url = "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/";
-const license = {
-  type: "MIT",
-  url: "https://github.com/atelierbram/syntax-highlighting/blob/master/LICENSE",
-};
-
-const ramps = {
-  neutral: chroma.scale([
-    "#131513",
-    "#242924",
-    "#5e6e5e",
-    "#687d68",
-    "#809980",
-    "#8ca68c",
-    "#cfe8cf",
-    "#f4fbf4",
-  ]),
-  red: colorRamp(chroma("#e6193c")),
-  orange: colorRamp(chroma("#87711d")),
-  yellow: colorRamp(chroma("#98981b")),
-  green: colorRamp(chroma("#29a329")),
-  cyan: colorRamp(chroma("#1999b3")),
-  blue: colorRamp(chroma("#3d62f5")),
-  violet: colorRamp(chroma("#ad2bee")),
-  magenta: colorRamp(chroma("#e619c3")),
-};
-
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);

styles/src/themes/staff/ayu-mirage.ts 🔗

@@ -1,31 +1,31 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Ayu";
-const author = "Konstantin Pschera <me@kons.ch>";
-const url = "https://github.com/ayu-theme/ayu-colors";
+const name = "Ayu"
+const author = "Konstantin Pschera <me@kons.ch>"
+const url = "https://github.com/ayu-theme/ayu-colors"
 const license = {
-  type: "MIT",
-  url: "https://github.com/ayu-theme/ayu-colors/blob/master/license"
+    type: "MIT",
+    url: "https://github.com/ayu-theme/ayu-colors/blob/master/license",
 }
 
 export const dark = createColorScheme(`${name} Mirage`, false, {
-  neutral: chroma.scale([
-    "#171B24",
-    "#1F2430",
-    "#242936",
-    "#707A8C",
-    "#8A9199",
-    "#CCCAC2",
-    "#D9D7CE",
-    "#F3F4F5",
-  ]),
-  red: colorRamp(chroma("#F28779")),
-  orange: colorRamp(chroma("#FFAD66")),
-  yellow: colorRamp(chroma("#FFD173")),
-  green: colorRamp(chroma("#D5FF80")),
-  cyan: colorRamp(chroma("#95E6CB")),
-  blue: colorRamp(chroma("#5CCFE6")),
-  violet: colorRamp(chroma("#D4BFFF")),
-  magenta: colorRamp(chroma("#F29E74")),
-});
+    neutral: chroma.scale([
+        "#171B24",
+        "#1F2430",
+        "#242936",
+        "#707A8C",
+        "#8A9199",
+        "#CCCAC2",
+        "#D9D7CE",
+        "#F3F4F5",
+    ]),
+    red: colorRamp(chroma("#F28779")),
+    orange: colorRamp(chroma("#FFAD66")),
+    yellow: colorRamp(chroma("#FFD173")),
+    green: colorRamp(chroma("#D5FF80")),
+    cyan: colorRamp(chroma("#95E6CB")),
+    blue: colorRamp(chroma("#5CCFE6")),
+    violet: colorRamp(chroma("#D4BFFF")),
+    magenta: colorRamp(chroma("#F29E74")),
+})

styles/src/themes/staff/ayu.ts 🔗

@@ -1,52 +1,52 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
+import chroma from "chroma-js"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
-const name = "Ayu";
-const author = "Konstantin Pschera <me@kons.ch>";
-const url = "https://github.com/ayu-theme/ayu-colors";
+const name = "Ayu"
+const author = "Konstantin Pschera <me@kons.ch>"
+const url = "https://github.com/ayu-theme/ayu-colors"
 const license = {
-  type: "MIT",
-  url: "https://github.com/ayu-theme/ayu-colors/blob/master/license"
+    type: "MIT",
+    url: "https://github.com/ayu-theme/ayu-colors/blob/master/license",
 }
 
 export const dark = createColorScheme(`${name} Dark`, false, {
-  neutral: chroma.scale([
-    "#0F1419",
-    "#131721",
-    "#272D38",
-    "#3E4B59",
-    "#BFBDB6",
-    "#E6E1CF",
-    "#E6E1CF",
-    "#F3F4F5",
-  ]),
-  red: colorRamp(chroma("#F07178")),
-  orange: colorRamp(chroma("#FF8F40")),
-  yellow: colorRamp(chroma("#FFB454")),
-  green: colorRamp(chroma("#B8CC52")),
-  cyan: colorRamp(chroma("#95E6CB")),
-  blue: colorRamp(chroma("#59C2FF")),
-  violet: colorRamp(chroma("#D2A6FF")),
-  magenta: colorRamp(chroma("#E6B673")),
-});
+    neutral: chroma.scale([
+        "#0F1419",
+        "#131721",
+        "#272D38",
+        "#3E4B59",
+        "#BFBDB6",
+        "#E6E1CF",
+        "#E6E1CF",
+        "#F3F4F5",
+    ]),
+    red: colorRamp(chroma("#F07178")),
+    orange: colorRamp(chroma("#FF8F40")),
+    yellow: colorRamp(chroma("#FFB454")),
+    green: colorRamp(chroma("#B8CC52")),
+    cyan: colorRamp(chroma("#95E6CB")),
+    blue: colorRamp(chroma("#59C2FF")),
+    violet: colorRamp(chroma("#D2A6FF")),
+    magenta: colorRamp(chroma("#E6B673")),
+})
 
 export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma.scale([
-    "#1A1F29",
-    "#242936",
-    "#5C6773",
-    "#828C99",
-    "#ABB0B6",
-    "#F8F9FA",
-    "#F3F4F5",
-    "#FAFAFA",
-  ]),
-  red: colorRamp(chroma("#F07178")),
-  orange: colorRamp(chroma("#FA8D3E")),
-  yellow: colorRamp(chroma("#F2AE49")),
-  green: colorRamp(chroma("#86B300")),
-  cyan: colorRamp(chroma("#4CBF99")),
-  blue: colorRamp(chroma("#36A3D9")),
-  violet: colorRamp(chroma("#A37ACC")),
-  magenta: colorRamp(chroma("#E6BA7E")),
-});
+    neutral: chroma.scale([
+        "#1A1F29",
+        "#242936",
+        "#5C6773",
+        "#828C99",
+        "#ABB0B6",
+        "#F8F9FA",
+        "#F3F4F5",
+        "#FAFAFA",
+    ]),
+    red: colorRamp(chroma("#F07178")),
+    orange: colorRamp(chroma("#FA8D3E")),
+    yellow: colorRamp(chroma("#F2AE49")),
+    green: colorRamp(chroma("#86B300")),
+    cyan: colorRamp(chroma("#4CBF99")),
+    blue: colorRamp(chroma("#36A3D9")),
+    violet: colorRamp(chroma("#A37ACC")),
+    magenta: colorRamp(chroma("#E6BA7E")),
+})

styles/src/themes/staff/brushtrees.ts 🔗

@@ -1,73 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Brush Trees";
-const author = "Abraham White <abelincoln.white@gmail.com>";
-const url = "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme";
-const license = {
-  type: "MIT",
-  url: "https://github.com/WhiteAbeLincoln/base16-brushtrees-scheme/blob/master/LICENSE"
-}
-
-export const dark = createColorScheme(`${name} Dark`, false, {
-  neutral: chroma.scale([
-    "#485867",
-    "#5A6D7A",
-    "#6D828E",
-    "#8299A1",
-    "#98AFB5",
-    "#B0C5C8",
-    "#C9DBDC",
-    "#E3EFEF",
-  ]),
-  red: colorRamp(chroma("#b38686")),
-  orange: colorRamp(chroma("#d8bba2")),
-  yellow: colorRamp(chroma("#aab386")),
-  green: colorRamp(chroma("#87b386")),
-  cyan: colorRamp(chroma("#86b3b3")),
-  blue: colorRamp(chroma("#868cb3")),
-  violet: colorRamp(chroma("#b386b2")),
-  magenta: colorRamp(chroma("#b39f9f")),
-});
-
-export const mirage = createColorScheme(`${name} Mirage`, false, {
-  neutral: chroma.scale([
-    "#485867",
-    "#5A6D7A",
-    "#6D828E",
-    "#8299A1",
-    "#98AFB5",
-    "#B0C5C8",
-    "#C9DBDC",
-    "#E3EFEF",
-  ]),
-  red: colorRamp(chroma("#F28779")),
-  orange: colorRamp(chroma("#FFAD66")),
-  yellow: colorRamp(chroma("#FFD173")),
-  green: colorRamp(chroma("#D5FF80")),
-  cyan: colorRamp(chroma("#95E6CB")),
-  blue: colorRamp(chroma("#5CCFE6")),
-  violet: colorRamp(chroma("#D4BFFF")),
-  magenta: colorRamp(chroma("#F29E74")),
-});
-
-export const light = createColorScheme(`${name} Light`, true, {
-  neutral: chroma.scale([
-    "#1A1F29",
-    "#242936",
-    "#5C6773",
-    "#828C99",
-    "#ABB0B6",
-    "#F8F9FA",
-    "#F3F4F5",
-    "#FAFAFA",
-  ]),
-  red: colorRamp(chroma("#b38686")),
-  orange: colorRamp(chroma("#d8bba2")),
-  yellow: colorRamp(chroma("#aab386")),
-  green: colorRamp(chroma("#87b386")),
-  cyan: colorRamp(chroma("#86b3b3")),
-  blue: colorRamp(chroma("#868cb3")),
-  violet: colorRamp(chroma("#b386b2")),
-  magenta: colorRamp(chroma("#b39f9f")),
-});

styles/src/themes/staff/dracula.ts 🔗

@@ -1,31 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Dracula";
-const author = "zenorocha";
-const url = "https://github.com/dracula/dracula-theme";
-const license = {
-  type: "MIT",
-  url: "https://github.com/dracula/dracula-theme/blob/master/LICENSE",
-};
-
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#282A36",
-    "#3a3c4e",
-    "#4d4f68",
-    "#626483",
-    "#62d6e8",
-    "#e9e9f4",
-    "#f1f2f8",
-    "#f8f8f2",
-  ]),
-  red: colorRamp(chroma("#ff5555")),
-  orange: colorRamp(chroma("#ffb86c")),
-  yellow: colorRamp(chroma("#f1fa8c")),
-  green: colorRamp(chroma("#50fa7b")),
-  cyan: colorRamp(chroma("#8be9fd")),
-  blue: colorRamp(chroma("#6272a4")),
-  violet: colorRamp(chroma("#bd93f9")),
-  magenta: colorRamp(chroma("#00f769")),
-});

styles/src/themes/staff/gruvbox-medium.ts 🔗

@@ -1,138 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Gruvbox";
-const author = "Dawid Kurek (dawikur@gmail.com)";
-const url = "https://github.com/morhetz/gruvbox";
-const license = {
-  type: "MIT/X11",
-  url: "https://en.wikipedia.org/wiki/MIT_License",
-};
-
-export const dark = createColorScheme(`${name} Dark Medium`, false, {
-  neutral: chroma.scale([
-    "#282828",
-    "#3c3836",
-    "#504945",
-    "#665c54",
-    "#7C6F64",
-    "#928374",
-    "#A89984",
-    "#BDAE93",
-    "#D5C4A1",
-    "#EBDBB2",
-    "#FBF1C7",
-  ]),
-  red: chroma.scale([
-    "#4D150F",
-    "#7D241A",
-    "#A31C17",
-    "#CC241D",
-    "#C83A29",
-    "#FB4934",
-    "#F06D61",
-    "#E6928E",
-    "#FFFFFF",
-  ]),
-  orange: chroma.scale([
-    "#462307",
-    "#7F400C",
-    "#AB4A0B",
-    "#D65D0E",
-    "#CB6614",
-    "#FE8019",
-    "#F49750",
-    "#EBAE87",
-    "#FFFFFF",
-  ]),
-  yellow: chroma.scale([
-    "#3D2C05",
-    "#7D5E17",
-    "#AC7A1A",
-    "#D79921",
-    "#E8AB28",
-    "#FABD2F",
-    "#F2C45F",
-    "#EBCC90",
-    "#FFFFFF",
-  ]),
-  green: chroma.scale([
-    "#32330A",
-    "#5C5D13",
-    "#797814",
-    "#98971A",
-    "#93951E",
-    "#B8BB26",
-    "#C2C359",
-    "#CCCB8D",
-    "#FFFFFF",
-  ]),
-  cyan: chroma.scale([
-    "#283D20",
-    "#47603E",
-    "#537D54",
-    "#689D6A",
-    "#719963",
-    "#8EC07C",
-    "#A1C798",
-    "#B4CEB5",
-    "#FFFFFF",
-  ]),
-  blue: chroma.scale([
-    "#103738",
-    "#214C4D",
-    "#376A6C",
-    "#458588",
-    "#688479",
-    "#83A598",
-    "#92B3AE",
-    "#A2C2C4",
-    "#FFFFFF",
-  ]),
-  violet: chroma.scale([
-    "#392228",
-    "#69434D",
-    "#8D4E6B",
-    "#B16286",
-    "#A86B7C",
-    "#D3869B",
-    "#D59BAF",
-    "#D8B1C3",
-    "#FFFFFF",
-  ]),
-  magenta: chroma.scale([
-    "#48402C",
-    "#756D59",
-    "#867A69",
-    "#A89984",
-    "#BCAF8E",
-    "#EBDBB2",
-    "#DFD3BA",
-    "#D4CCC2",
-    "#FFFFFF",
-  ]),
-});
-
-export const light = createColorScheme(`${name} Light Medium`, true, {
-  neutral: chroma.scale([
-    "#282828",
-    "#3c3836",
-    "#504945",
-    "#665c54",
-    "#7C6F64",
-    "#928374",
-    "#A89984",
-    "#BDAE93",
-    "#D5C4A1",
-    "#EBDBB2",
-    "#FBF1C7",
-  ]),
-  red: colorRamp(chroma("#9d0006")),
-  orange: colorRamp(chroma("#af3a03")),
-  yellow: colorRamp(chroma("#b57614")),
-  green: colorRamp(chroma("#79740e")),
-  cyan: colorRamp(chroma("#427b58")),
-  blue: colorRamp(chroma("#076678")),
-  violet: colorRamp(chroma("#8f3f71")),
-  magenta: colorRamp(chroma("#d65d0e")),
-});

styles/src/themes/staff/monokai.ts 🔗

@@ -1,32 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Monokai";
-const author = "Wimer Hazenberg (http://www.monokai.nl)";
-const url = "https://base16.netlify.app/previews/base16-monokai.html";
-const license = {
-  type: "?",
-  url: "?",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#272822",
-    "#383830",
-    "#49483e",
-    "#75715e",
-    "#a59f85",
-    "#f8f8f2",
-    "#f5f4f1",
-    "#f9f8f5",
-  ]),
-  red: colorRamp(chroma("#f92672")),
-  orange: colorRamp(chroma("#fd971f")),
-  yellow: colorRamp(chroma("#f4bf75")),
-  green: colorRamp(chroma("#a6e22e")),
-  cyan: colorRamp(chroma("#a1efe4")),
-  blue: colorRamp(chroma("#66d9ef")),
-  violet: colorRamp(chroma("#ae81ff")),
-  magenta: colorRamp(chroma("#cc6633")),
-});

styles/src/themes/staff/nord.ts 🔗

@@ -1,32 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Nord";
-const author = "arcticicestudio";
-const url = "https://www.nordtheme.com/";
-const license = {
-  type: "MIT",
-  url: "https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#2E3440",
-    "#3B4252",
-    "#434C5E",
-    "#4C566A",
-    "#D8DEE9",
-    "#E5E9F0",
-    "#ECEFF4",
-    "#8FBCBB",
-  ]),
-  red: colorRamp(chroma("#88C0D0")),
-  orange: colorRamp(chroma("#81A1C1")),
-  yellow: colorRamp(chroma("#5E81AC")),
-  green: colorRamp(chroma("#BF616A")),
-  cyan: colorRamp(chroma("#D08770")),
-  blue: colorRamp(chroma("#EBCB8B")),
-  violet: colorRamp(chroma("#A3BE8C")),
-  magenta: colorRamp(chroma("#B48EAD")),
-});

styles/src/themes/staff/seti-ui.ts 🔗

@@ -1,32 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Seti UI";
-const author = "jesseweed";
-const url = "https://github.com/jesseweed/seti-ui";
-const license = {
-  type: "MIT",
-  url: "https://github.com/jesseweed/seti-ui/blob/master/LICENSE.md",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#151718",
-    "#262B30",
-    "#1E2326",
-    "#41535B",
-    "#43a5d5",
-    "#d6d6d6",
-    "#eeeeee",
-    "#ffffff",
-  ]),
-  red: colorRamp(chroma("#Cd3f45")),
-  orange: colorRamp(chroma("#db7b55")),
-  yellow: colorRamp(chroma("#e6cd69")),
-  green: colorRamp(chroma("#9fca56")),
-  cyan: colorRamp(chroma("#55dbbe")),
-  blue: colorRamp(chroma("#55b5db")),
-  violet: colorRamp(chroma("#a074c4")),
-  magenta: colorRamp(chroma("#8a553f")),
-});

styles/src/themes/staff/tokyo-night-storm.ts 🔗

@@ -1,32 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Tokyo Night Storm";
-const author = "folke";
-const url = "https://github.com/folke/tokyonight.nvim";
-const license = {
-  type: "MIT",
-  url: "https://github.com/ghifarit53/tokyonight-vim/blob/master/LICENSE",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#24283B",
-    "#16161E",
-    "#343A52",
-    "#444B6A",
-    "#787C99",
-    "#A9B1D6",
-    "#CBCCD1",
-    "#D5D6DB",
-  ]),
-  red: colorRamp(chroma("#C0CAF5")),
-  orange: colorRamp(chroma("#A9B1D6")),
-  yellow: colorRamp(chroma("#0DB9D7")),
-  green: colorRamp(chroma("#9ECE6A")),
-  cyan: colorRamp(chroma("#B4F9F8")),
-  blue: colorRamp(chroma("#2AC3DE")),
-  violet: colorRamp(chroma("#BB9AF7")),
-  magenta: colorRamp(chroma("#F7768E")),
-});

styles/src/themes/staff/tokyo-night.ts 🔗

@@ -1,53 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Tokyo";
-const author = "folke";
-const url = "https://github.com/folke/tokyonight.nvim";
-const license = {
-  type: "Apache License 2.0",
-  url: "https://github.com/folke/tokyonight.nvim/blob/main/LICENSE",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name} Night`, false, {
-  neutral: chroma.scale([
-    "#1A1B26",
-    "#16161E",
-    "#2F3549",
-    "#444B6A",
-    "#787C99",
-    "#A9B1D6",
-    "#CBCCD1",
-    "#D5D6DB",
-  ]),
-  red: colorRamp(chroma("#C0CAF5")),
-  orange: colorRamp(chroma("#A9B1D6")),
-  yellow: colorRamp(chroma("#0DB9D7")),
-  green: colorRamp(chroma("#9ECE6A")),
-  cyan: colorRamp(chroma("#B4F9F8")),
-  blue: colorRamp(chroma("#2AC3DE")),
-  violet: colorRamp(chroma("#BB9AF7")),
-  magenta: colorRamp(chroma("#F7768E")),
-});
-
-export const light = createColorScheme(`${name} Day`, true, {
-  neutral: chroma.scale([
-    "#1A1B26",
-    "#1A1B26",
-    "#343B59",
-    "#4C505E",
-    "#9699A3",
-    "#DFE0E5",
-    "#CBCCD1",
-    "#D5D6DB",
-  ]),
-  red: colorRamp(chroma("#343B58")),
-  orange: colorRamp(chroma("#965027")),
-  yellow: colorRamp(chroma("#166775")),
-  green: colorRamp(chroma("#485E30")),
-  cyan: colorRamp(chroma("#3E6968")),
-  blue: colorRamp(chroma("#34548A")),
-  violet: colorRamp(chroma("#5A4A78")),
-  magenta: colorRamp(chroma("#8C4351")),
-});

styles/src/themes/staff/zed-pro.ts 🔗

@@ -1,36 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Zed Pro";
-const author = "Nate Butler"
-const url = "https://github.com/iamnbutler"
-const license = {
-  type: "?",
-  url: "?",
-};
-
-const ramps = {
-  neutral: chroma
-    .scale([
-      "#101010",
-      "#1C1C1C",
-      "#212121",
-      "#2D2D2D",
-      "#B9B9B9",
-      "#DADADA",
-      "#E6E6E6",
-      "#FFFFFF",
-    ])
-    .domain([0, 0.1, 0.2, 0.3, 0.7, 0.8, 0.9, 1]),
-  red: colorRamp(chroma("#DC604F")),
-  orange: colorRamp(chroma("#DE782F")),
-  yellow: colorRamp(chroma("#E0B750")),
-  green: colorRamp(chroma("#2A643D")),
-  cyan: colorRamp(chroma("#215050")),
-  blue: colorRamp(chroma("#2F6DB7")),
-  violet: colorRamp(chroma("#5874C1")),
-  magenta: colorRamp(chroma("#DE9AB8")),
-};
-
-export const dark = createColorScheme(`${name} Dark`, false, ramps);
-export const light = createColorScheme(`${name} Light`, true, ramps);

styles/src/themes/staff/zenburn.ts 🔗

@@ -1,32 +0,0 @@
-import chroma from "chroma-js";
-import { colorRamp, createColorScheme } from "../common/ramps";
-
-const name = "Zenburn";
-const author = "elnawe";
-const url = "https://github.com/elnawe/base16-zenburn-scheme";
-const license = {
-  type: "None",
-  url: "",
-};
-
-// `name-[light|dark]`, isLight, color ramps
-export const dark = createColorScheme(`${name}`, false, {
-  neutral: chroma.scale([
-    "#383838",
-    "#404040",
-    "#606060",
-    "#6f6f6f",
-    "#808080",
-    "#dcdccc",
-    "#c0c0c0",
-    "#ffffff",
-  ]),
-  red: colorRamp(chroma("#dca3a3")),
-  orange: colorRamp(chroma("#dfaf8f")),
-  yellow: colorRamp(chroma("#e0cf9f")),
-  green: colorRamp(chroma("#5f7f5f")),
-  cyan: colorRamp(chroma("#93e0e3")),
-  blue: colorRamp(chroma("#7cb8bb")),
-  violet: colorRamp(chroma("#dc8cc3")),
-  magenta: colorRamp(chroma("#000000")),
-});

styles/src/themes/summercamp.ts 🔗

@@ -1,40 +1,44 @@
-import chroma from "chroma-js";
-import { Meta } from "./common/colorScheme";
-import { colorRamp, createColorScheme } from "./common/ramps";
+import chroma from "chroma-js"
+import { Meta } from "./common/colorScheme"
+import { colorRamp, createColorScheme } from "./common/ramps"
 
-const name = "Summercamp";
+const name = "Summercamp"
 
 const ramps = {
-  neutral: chroma
-    .scale([
-      "#1c1810",
-      "#2a261c",
-      "#3a3527",
-      "#3a3527",
-      "#5f5b45",
-      "#736e55",
-      "#bab696",
-      "#f8f5de",
-    ])
-    .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]),
-  red: colorRamp(chroma("#e35142")),
-  orange: colorRamp(chroma("#fba11b")),
-  yellow: colorRamp(chroma("#f2ff27")),
-  green: colorRamp(chroma("#5ceb5a")),
-  cyan: colorRamp(chroma("#5aebbc")),
-  blue: colorRamp(chroma("#489bf0")),
-  violet: colorRamp(chroma("#FF8080")),
-  magenta: colorRamp(chroma("#F69BE7")),
-};
+    neutral: chroma
+        .scale([
+            "#1c1810",
+            "#2a261c",
+            "#3a3527",
+            "#3a3527",
+            "#5f5b45",
+            "#736e55",
+            "#bab696",
+            "#f8f5de",
+        ])
+        .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]),
+    red: colorRamp(chroma("#e35142")),
+    orange: colorRamp(chroma("#fba11b")),
+    yellow: colorRamp(chroma("#f2ff27")),
+    green: colorRamp(chroma("#5ceb5a")),
+    cyan: colorRamp(chroma("#5aebbc")),
+    blue: colorRamp(chroma("#489bf0")),
+    violet: colorRamp(chroma("#FF8080")),
+    magenta: colorRamp(chroma("#F69BE7")),
+}
 
-export const dark = createColorScheme(`${name}`, false, ramps);
+export const dark = createColorScheme(name, false, ramps)
 export const meta: Meta = {
-  name,
-  author: "zoefiri",
-  url: "https://github.com/zoefiri/base16-sc",
-  license: {
-    SPDX: "MIT",
-    https_url: "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE",
-    license_checksum: "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf"
-  }
-}
+    name,
+    author: "zoefiri",
+    url: "https://github.com/zoefiri/base16-sc",
+    license: {
+        SPDX: "MIT",
+        license_text: {
+            https_url:
+                "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE",
+            license_checksum:
+                "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf",
+        },
+    },
+}

styles/src/utils/color.ts 🔗

@@ -1,5 +1,5 @@
-import chroma from "chroma-js";
+import chroma from "chroma-js"
 
 export function withOpacity(color: string, opacity: number): string {
-  return chroma(color).alpha(opacity).hex();
+    return chroma(color).alpha(opacity).hex()
 }

styles/src/utils/snakeCase.ts 🔗

@@ -1,35 +1,35 @@
-import { snakeCase } from "case-anything";
+import { snakeCase } from "case-anything"
 
 // https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case
 
 // Typescript magic to convert any string from camelCase to snake_case at compile time
 type SnakeCase<S> = S extends string
-  ? S extends `${infer T}${infer U}`
-    ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
+    ? S extends `${infer T}${infer U}`
+        ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
+        : S
     : S
-  : S;
 
 type SnakeCased<Type> = {
-  [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>;
-};
+    [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
+}
 
 export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
-  const snakeObject: any = {};
-  for (const key in object) {
-    snakeObject[snakeCase(key, { keepSpecialCharacters: true })] =
-      snakeCaseValue(object[key]);
-  }
-  return snakeObject;
+    const snakeObject: any = {}
+    for (const key in object) {
+        snakeObject[snakeCase(key, { keepSpecialCharacters: true })] =
+            snakeCaseValue(object[key])
+    }
+    return snakeObject
 }
 
 function snakeCaseValue(value: any): any {
-  if (typeof value === "object") {
-    if (Array.isArray(value)) {
-      return value.map(snakeCaseValue);
+    if (typeof value === "object") {
+        if (Array.isArray(value)) {
+            return value.map(snakeCaseValue)
+        } else {
+            return snakeCaseTree(value)
+        }
     } else {
-      return snakeCaseTree(value);
+        return value
     }
-  } else {
-    return value;
-  }
 }

styles/tsconfig.json 🔗

@@ -1,12 +1,12 @@
 {
-  "compilerOptions": {
-    "target": "es2015",
-    "module": "commonjs",
-    "esModuleInterop": true,
-    "noImplicitAny": true,
-    "removeComments": true,
-    "preserveConstEnums": true,
-    "sourceMap": true
-  },
-  "exclude": ["node_modules"]
+    "compilerOptions": {
+        "target": "es2015",
+        "module": "commonjs",
+        "esModuleInterop": true,
+        "noImplicitAny": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": true
+    },
+    "exclude": ["node_modules"]
 }