Merge branch 'main' into auto-update

Antonio Scandurra created

Change summary

.gitignore                                                                |    5 
.zed.toml                                                                 |    2 
Cargo.lock                                                                |  442 
Dockerfile                                                                |   14 
Dockerfile.migrator                                                       |    2 
Procfile                                                                  |    2 
README.md                                                                 |    2 
assets/fonts/zed-mono/zed-mono-extended.ttf                               |    0 
assets/fonts/zed-mono/zed-mono-extendedbold.ttf                           |    0 
assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf                     |    0 
assets/fonts/zed-mono/zed-mono-extendeditalic.ttf                         |    0 
assets/fonts/zed-sans/zed-sans-extended.ttf                               |    0 
assets/fonts/zed-sans/zed-sans-extendedbold.ttf                           |    0 
assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf                     |    0 
assets/fonts/zed-sans/zed-sans-extendeditalic.ttf                         |    0 
assets/icons/broadcast-24.svg                                             |    0 
assets/icons/comment-16.svg                                               |    0 
assets/icons/diagnostic-error-10.svg                                      |    0 
assets/icons/diagnostic-summary-error.svg                                 |    0 
assets/icons/diagnostic-summary-warning.svg                               |    0 
assets/icons/diagnostic-warning-10.svg                                    |    0 
assets/icons/disclosure-closed.svg                                        |    0 
assets/icons/disclosure-open.svg                                          |    0 
assets/icons/file-16.svg                                                  |    0 
assets/icons/folder-tree-16.svg                                           |    0 
assets/icons/magnifier.svg                                                |    0 
assets/icons/offline-14.svg                                               |    0 
assets/icons/signed-out-12.svg                                            |    0 
assets/icons/user-16.svg                                                  |    0 
assets/icons/x.svg                                                        |    0 
assets/icons/zap.svg                                                      |    0 
assets/keymaps/default.json                                               |  264 
assets/keymaps/vim.json                                                   |   93 
assets/themes/dark.json                                                   | 1338 
assets/themes/light.json                                                  | 1338 
crates/assets/Cargo.toml                                                  |   14 
crates/assets/src/assets.rs                                               |    2 
crates/auto_update/Cargo.toml                                             |    3 
crates/auto_update/src/auto_update.rs                                     |   20 
crates/breadcrumbs/Cargo.toml                                             |    1 
crates/breadcrumbs/src/breadcrumbs.rs                                     |    3 
crates/chat_panel/Cargo.toml                                              |    1 
crates/chat_panel/src/chat_panel.rs                                       |   10 
crates/cli/Cargo.toml                                                     |   24 
crates/cli/src/cli.rs                                                     |   22 
crates/cli/src/main.rs                                                    |  124 
crates/client/Cargo.toml                                                  |    2 
crates/client/src/client.rs                                               |    4 
crates/client/src/test.rs                                                 |    6 
crates/collab/.env.template.toml                                          |    0 
crates/collab/Cargo.toml                                                  |   14 
crates/collab/Procfile                                                    |    2 
crates/collab/README.md                                                   |    0 
crates/collab/basic.conf                                                  |    0 
crates/collab/favicon.ico                                                 |    0 
crates/collab/k8s/environments/production.sh                              |    0 
crates/collab/k8s/environments/staging.sh                                 |    0 
crates/collab/k8s/manifest.template.yml                                   |   18 
crates/collab/k8s/migrate.template.yml                                    |    0 
crates/collab/migrations/20210527024318_initial_schema.sql                |    0 
crates/collab/migrations/20210607190313_create_access_tokens.sql          |    0 
crates/collab/migrations/20210805175147_create_chat_tables.sql            |    0 
crates/collab/migrations/20210916123647_add_nonce_to_channel_messages.sql |    0 
crates/collab/migrations/20210920192001_add_interests_to_signups.sql      |    0 
crates/collab/src/admin.rs                                                |    0 
crates/collab/src/api.rs                                                  |    8 
crates/collab/src/assets.rs                                               |    0 
crates/collab/src/auth.rs                                                 |    0 
crates/collab/src/bin/dotenv.rs                                           |    0 
crates/collab/src/bin/seed.rs                                             |    0 
crates/collab/src/careers.rs                                              |    0 
crates/collab/src/community.rs                                            |    0 
crates/collab/src/db.rs                                                   |    0 
crates/collab/src/env.rs                                                  |    0 
crates/collab/src/errors.rs                                               |    0 
crates/collab/src/expiring.rs                                             |    0 
crates/collab/src/github.rs                                               |    0 
crates/collab/src/home.rs                                                 |    9 
crates/collab/src/main.rs                                                 |    8 
crates/collab/src/releases.rs                                             |    0 
crates/collab/src/rpc.rs                                                  |  454 
crates/collab/src/rpc/store.rs                                            |   48 
crates/collab/src/team.rs                                                 |    0 
crates/collab/static/browserconfig.xml                                    |    0 
crates/collab/static/fonts/VisbyCF-Bold.eot                               |    0 
crates/collab/static/fonts/VisbyCF-Bold.woff                              |    0 
crates/collab/static/fonts/VisbyCF-Bold.woff2                             |    0 
crates/collab/static/fonts/VisbyCF-BoldOblique.eot                        |    0 
crates/collab/static/fonts/VisbyCF-BoldOblique.woff                       |    0 
crates/collab/static/fonts/VisbyCF-BoldOblique.woff2                      |    0 
crates/collab/static/fonts/VisbyCF-DemiBold.eot                           |    0 
crates/collab/static/fonts/VisbyCF-DemiBold.woff                          |    0 
crates/collab/static/fonts/VisbyCF-DemiBold.woff2                         |    0 
crates/collab/static/fonts/VisbyCF-DemiBoldOblique.eot                    |    0 
crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff                   |    0 
crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff2                  |    0 
crates/collab/static/fonts/VisbyCF-ExtraBold.eot                          |    0 
crates/collab/static/fonts/VisbyCF-ExtraBold.woff                         |    0 
crates/collab/static/fonts/VisbyCF-ExtraBold.woff2                        |    0 
crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.eot                   |    0 
crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff                  |    0 
crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff2                 |    0 
crates/collab/static/fonts/VisbyCF-Heavy.eot                              |    0 
crates/collab/static/fonts/VisbyCF-Heavy.woff                             |    0 
crates/collab/static/fonts/VisbyCF-Heavy.woff2                            |    0 
crates/collab/static/fonts/VisbyCF-HeavyOblique.eot                       |    0 
crates/collab/static/fonts/VisbyCF-HeavyOblique.woff                      |    0 
crates/collab/static/fonts/VisbyCF-HeavyOblique.woff2                     |    0 
crates/collab/static/fonts/VisbyCF-Light.eot                              |    0 
crates/collab/static/fonts/VisbyCF-Light.woff                             |    0 
crates/collab/static/fonts/VisbyCF-Light.woff2                            |    0 
crates/collab/static/fonts/VisbyCF-LightOblique.eot                       |    0 
crates/collab/static/fonts/VisbyCF-LightOblique.woff                      |    0 
crates/collab/static/fonts/VisbyCF-LightOblique.woff2                     |    0 
crates/collab/static/fonts/VisbyCF-Medium.eot                             |    0 
crates/collab/static/fonts/VisbyCF-Medium.woff                            |    0 
crates/collab/static/fonts/VisbyCF-Medium.woff2                           |    0 
crates/collab/static/fonts/VisbyCF-MediumOblique.eot                      |    0 
crates/collab/static/fonts/VisbyCF-MediumOblique.woff                     |    0 
crates/collab/static/fonts/VisbyCF-MediumOblique.woff2                    |    0 
crates/collab/static/fonts/VisbyCF-Regular.eot                            |    0 
crates/collab/static/fonts/VisbyCF-Regular.woff                           |    0 
crates/collab/static/fonts/VisbyCF-Regular.woff2                          |    0 
crates/collab/static/fonts/VisbyCF-RegularOblique.eot                     |    0 
crates/collab/static/fonts/VisbyCF-RegularOblique.woff                    |    0 
crates/collab/static/fonts/VisbyCF-RegularOblique.woff2                   |    0 
crates/collab/static/fonts/VisbyCF-Thin.eot                               |    0 
crates/collab/static/fonts/VisbyCF-Thin.woff                              |    0 
crates/collab/static/fonts/VisbyCF-Thin.woff2                             |    0 
crates/collab/static/fonts/VisbyCF-ThinOblique.eot                        |    0 
crates/collab/static/fonts/VisbyCF-ThinOblique.woff                       |    0 
crates/collab/static/fonts/VisbyCF-ThinOblique.woff2                      |    0 
crates/collab/static/images/android-chrome-192x192.png                    |    0 
crates/collab/static/images/android-chrome-512x512.png                    |    0 
crates/collab/static/images/apple-touch-icon.png                          |    0 
crates/collab/static/images/favicon-16x16.png                             |    0 
crates/collab/static/images/favicon-32x32.png                             |    0 
crates/collab/static/images/favicon.png                                   |    0 
crates/collab/static/images/favicon.svg                                   |    0 
crates/collab/static/images/mstile-144x144.png                            |    0 
crates/collab/static/images/mstile-150x150.png                            |    0 
crates/collab/static/images/mstile-310x150.png                            |    0 
crates/collab/static/images/mstile-310x310.png                            |    0 
crates/collab/static/images/mstile-70x70.png                              |    0 
crates/collab/static/images/safari-pinned-tab.svg                         |    0 
crates/collab/static/images/zed-og-image.png                              |    0 
crates/collab/static/images/zed-twitter-image.png                         |    0 
crates/collab/static/prism.js                                             |    0 
crates/collab/static/prose.css                                            |    0 
crates/collab/static/prose.css.map                                        |    0 
crates/collab/static/prose.scss                                           |    0 
crates/collab/static/site.webmanifest                                     |    0 
crates/collab/static/svg/hero.svg                                         |    0 
crates/collab/styles.css                                                  |    0 
crates/collab/templates/admin.hbs                                         |    0 
crates/collab/templates/careers.hbs                                       |    0 
crates/collab/templates/community.hbs                                     |    0 
crates/collab/templates/docs.hbs                                          |    0 
crates/collab/templates/error.hbs                                         |    0 
crates/collab/templates/home.hbs                                          |    0 
crates/collab/templates/partials/layout.hbs                               |    0 
crates/collab/templates/releases.hbs                                      |    0 
crates/collab/templates/signup.hbs                                        |    0 
crates/collab/templates/team.hbs                                          |    0 
crates/command_palette/Cargo.toml                                         |   26 
crates/command_palette/src/command_palette.rs                             |  362 
crates/contacts_panel/Cargo.toml                                          |    1 
crates/contacts_panel/src/contacts_panel.rs                               |   10 
crates/diagnostics/Cargo.toml                                             |    1 
crates/diagnostics/src/diagnostics.rs                                     |   34 
crates/diagnostics/src/items.rs                                           |   10 
crates/editor/Cargo.toml                                                  |    5 
crates/editor/src/context_menu.rs                                         |  272 
crates/editor/src/display_map.rs                                          |  102 
crates/editor/src/display_map/block_map.rs                                |   17 
crates/editor/src/display_map/fold_map.rs                                 |    5 
crates/editor/src/display_map/tab_map.rs                                  |   48 
crates/editor/src/display_map/wrap_map.rs                                 |   45 
crates/editor/src/editor.rs                                               |  659 
crates/editor/src/element.rs                                              |    6 
crates/editor/src/items.rs                                                |   20 
crates/editor/src/movement.rs                                             |   11 
crates/editor/src/multi_buffer.rs                                         |   70 
crates/editor/src/test.rs                                                 |   25 
crates/file_finder/Cargo.toml                                             |    2 
crates/file_finder/src/file_finder.rs                                     |  390 
crates/go_to_line/Cargo.toml                                              |    1 
crates/go_to_line/src/go_to_line.rs                                       |   15 
crates/gpui/Cargo.toml                                                    |    4 
crates/gpui/examples/text.rs                                              |    1 
crates/gpui/src/app.rs                                                    |  517 
crates/gpui/src/app/action.rs                                             |  109 
crates/gpui/src/elements.rs                                               |   10 
crates/gpui/src/elements/align.rs                                         |    3 
crates/gpui/src/elements/canvas.rs                                        |    1 
crates/gpui/src/elements/constrained_box.rs                               |    1 
crates/gpui/src/elements/container.rs                                     |    1 
crates/gpui/src/elements/empty.rs                                         |    1 
crates/gpui/src/elements/event_handler.rs                                 |   11 
crates/gpui/src/elements/expanded.rs                                      |    1 
crates/gpui/src/elements/flex.rs                                          |   98 
crates/gpui/src/elements/hook.rs                                          |    1 
crates/gpui/src/elements/image.rs                                         |    1 
crates/gpui/src/elements/label.rs                                         |    1 
crates/gpui/src/elements/list.rs                                          |    2 
crates/gpui/src/elements/mouse_event_handler.rs                           |    7 
crates/gpui/src/elements/overlay.rs                                       |    1 
crates/gpui/src/elements/stack.rs                                         |    1 
crates/gpui/src/elements/svg.rs                                           |    1 
crates/gpui/src/elements/text.rs                                          |    1 
crates/gpui/src/elements/uniform_list.rs                                  |   16 
crates/gpui/src/executor.rs                                               |   44 
crates/gpui/src/gpui.rs                                                   |    3 
crates/gpui/src/keymap.rs                                                 |  162 
crates/gpui/src/platform.rs                                               |   15 
crates/gpui/src/platform/event.rs                                         |   17 
crates/gpui/src/platform/mac/atlas.rs                                     |   28 
crates/gpui/src/platform/mac/fonts.rs                                     |  207 
crates/gpui/src/platform/mac/image_cache.rs                               |   78 
crates/gpui/src/platform/mac/platform.rs                                  |   86 
crates/gpui/src/platform/mac/renderer.rs                                  |   59 
crates/gpui/src/platform/mac/sprite_cache.rs                              |   38 
crates/gpui/src/platform/mac/window.rs                                    |    3 
crates/gpui/src/platform/test.rs                                          |    8 
crates/gpui/src/presenter.rs                                              |   45 
crates/gpui/src/scene.rs                                                  |   36 
crates/gpui/src/text_layout.rs                                            |   47 
crates/gpui/src/views/select.rs                                           |   13 
crates/gpui_macros/src/gpui_macros.rs                                     |   99 
crates/journal/Cargo.toml                                                 |    2 
crates/journal/src/journal.rs                                             |    7 
crates/language/Cargo.toml                                                |    3 
crates/language/src/buffer.rs                                             |   68 
crates/language/src/language.rs                                           |    8 
crates/language/src/tests.rs                                              |  121 
crates/lsp/Cargo.toml                                                     |    2 
crates/lsp/src/lsp.rs                                                     |    3 
crates/outline/Cargo.toml                                                 |    2 
crates/outline/src/outline.rs                                             |  239 
crates/picker/Cargo.toml                                                  |   23 
crates/picker/src/picker.rs                                               |  277 
crates/project/Cargo.toml                                                 |    3 
crates/project/src/project.rs                                             |  726 
crates/project/src/worktree.rs                                            |   90 
crates/project_panel/Cargo.toml                                           |    1 
crates/project_panel/src/project_panel.rs                                 |   23 
crates/project_symbols/Cargo.toml                                         |   10 
crates/project_symbols/src/project_symbols.rs                             |  474 
crates/rpc/Cargo.toml                                                     |   13 
crates/rpc/proto/zed.proto                                                |    2 
crates/rpc/src/conn.rs                                                    |   36 
crates/rpc/src/peer.rs                                                    |   54 
crates/rpc/src/rpc.rs                                                     |    2 
crates/search/Cargo.toml                                                  |    4 
crates/search/src/buffer_search.rs                                        |  128 
crates/search/src/project_search.rs                                       |  100 
crates/search/src/search.rs                                               |    9 
crates/server/.env.toml                                                   |   42 
crates/settings/Cargo.toml                                                |   24 
crates/settings/src/keymap_file.rs                                        |   62 
crates/settings/src/settings.rs                                           |  262 
crates/sum_tree/Cargo.toml                                                |    2 
crates/text/Cargo.toml                                                    |    2 
crates/text/src/text.rs                                                   |    5 
crates/theme/src/resolution.rs                                            |  497 
crates/theme/src/theme.rs                                                 |   12 
crates/theme/src/theme_registry.rs                                        |  234 
crates/theme_selector/Cargo.toml                                          |    4 
crates/theme_selector/src/theme_selector.rs                               |  359 
crates/util/Cargo.toml                                                    |    2 
crates/vim/Cargo.toml                                                     |    8 
crates/vim/src/editor_events.rs                                           |   24 
crates/vim/src/insert.rs                                                  |   23 
crates/vim/src/mode.rs                                                    |   72 
crates/vim/src/motion.rs                                                  |  271 
crates/vim/src/normal.rs                                                  |  776 
crates/vim/src/normal/g_prefix.rs                                         |   82 
crates/vim/src/state.rs                                                   |   82 
crates/vim/src/vim.rs                                                     |   95 
crates/vim/src/vim_test_context.rs                                        |   40 
crates/workspace/Cargo.toml                                               |    7 
crates/workspace/src/lsp_status.rs                                        |   11 
crates/workspace/src/menu.rs                                              |   31 
crates/workspace/src/pane.rs                                              |  496 
crates/workspace/src/pane_group.rs                                        |    3 
crates/workspace/src/settings.rs                                          |  325 
crates/workspace/src/sidebar.rs                                           |   16 
crates/workspace/src/status_bar.rs                                        |    3 
crates/workspace/src/toolbar.rs                                           |    3 
crates/workspace/src/workspace.rs                                         |  230 
crates/zed/Cargo.toml                                                     |   10 
crates/zed/assets/themes/_base.toml                                       |  413 
crates/zed/assets/themes/black.toml                                       |   67 
crates/zed/assets/themes/dark.toml                                        |   67 
crates/zed/assets/themes/light.toml                                       |   67 
crates/zed/src/languages.rs                                               |    5 
crates/zed/src/languages/javascript/brackets.scm                          |    5 
crates/zed/src/languages/javascript/config.toml                           |   12 
crates/zed/src/languages/javascript/highlights.scm                        |  219 
crates/zed/src/languages/javascript/indents.scm                           |   15 
crates/zed/src/languages/javascript/outline.scm                           |   55 
crates/zed/src/languages/tsx/config.toml                                  |    2 
crates/zed/src/main.rs                                                    |  233 
crates/zed/src/menus.rs                                                   |    8 
crates/zed/src/settings_file.rs                                           |  171 
crates/zed/src/test.rs                                                    |    5 
crates/zed/src/zed.rs                                                     |  219 
script/build-css                                                          |    2 
script/build-themes                                                       |    7 
script/bundle                                                             |   30 
script/deploy                                                             |    6 
script/seed-db                                                            |    4 
script/sqlx                                                               |    2 
script/tailwind.config.js                                                 |    4 
styles/.gitignore                                                         |    1 
styles/dist/core.json                                                     | 1155 
styles/dist/dark.json                                                     |  681 
styles/dist/light.json                                                    |  681 
styles/dist/tokens.json                                                   | 2519 
styles/nodemon.json                                                       |    8 
styles/package-lock.json                                                  | 2321 
styles/package.json                                                       |   22 
styles/src/buildThemes.ts                                                 |   17 
styles/src/buildTokens.ts                                                 |  110 
styles/src/styleTree/app.ts                                               |   45 
styles/src/styleTree/chatPanel.ts                                         |  108 
styles/src/styleTree/commandPalette.ts                                    |   23 
styles/src/styleTree/components.ts                                        |   93 
styles/src/styleTree/contactsPanel.ts                                     |   62 
styles/src/styleTree/editor.ts                                            |  146 
styles/src/styleTree/projectPanel.ts                                      |   37 
styles/src/styleTree/search.ts                                            |   84 
styles/src/styleTree/selectorModal.ts                                     |   59 
styles/src/styleTree/workspace.ts                                         |  158 
styles/src/themes/dark.ts                                                 |  241 
styles/src/themes/light.ts                                                |  239 
styles/src/themes/theme.ts                                                |  147 
styles/src/tokens.ts                                                      |  102 
styles/src/utils/color.ts                                                 |   52 
styles/src/utils/snakeCase.ts                                             |   35 
styles/tsconfig.json                                                      |   14 
341 files changed, 20,293 insertions(+), 5,244 deletions(-)

Detailed changes

.gitignore 🔗

@@ -2,6 +2,7 @@
 /zed.xcworkspace
 .DS_Store
 /script/node_modules
-/crates/server/.env.toml
-/crates/server/static/styles.css
+/styles/node_modules
+/crates/collab/.env.toml
+/crates/collab/static/styles.css
 /vendor/bin

.zed.toml 🔗

@@ -1 +1 @@
-collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "Kethku"]
+collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "gibusu", "Kethku"]

Cargo.lock 🔗

@@ -135,7 +135,7 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
 dependencies = [
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -168,6 +168,15 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
 
+[[package]]
+name = "assets"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "rust-embed",
+]
+
 [[package]]
 name = "async-attributes"
 version = "1.1.2"
@@ -300,7 +309,7 @@ dependencies = [
  "polling",
  "vec-arena",
  "waker-fn",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -355,7 +364,7 @@ dependencies = [
  "futures-lite",
  "once_cell",
  "signal-hook",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -449,7 +458,7 @@ dependencies = [
  "async-io",
  "async-lock",
  "async-process",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -541,7 +550,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
  "hermit-abi",
  "libc",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -555,6 +564,7 @@ dependencies = [
  "log",
  "serde",
  "serde_json",
+ "settings",
  "smol",
  "surf",
  "tempdir",
@@ -632,7 +642,7 @@ dependencies = [
  "cexpr",
  "clang-sys",
  "clap 2.33.3",
- "env_logger",
+ "env_logger 0.8.3",
  "lazy_static",
  "lazycell",
  "log",
@@ -747,6 +757,7 @@ dependencies = [
  "language",
  "project",
  "search",
+ "settings",
  "theme",
  "workspace",
 ]
@@ -899,6 +910,7 @@ dependencies = [
  "editor",
  "gpui",
  "postage",
+ "settings",
  "theme",
  "time 0.3.7",
  "util",
@@ -916,7 +928,7 @@ dependencies = [
  "num-traits",
  "serde",
  "time 0.1.44",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -971,9 +983,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "3.0.0-beta.2"
+version = "3.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
+checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c"
 dependencies = [
  "atty",
  "bitflags",
@@ -983,24 +995,36 @@ dependencies = [
  "os_str_bytes",
  "strsim 0.10.0",
  "termcolor",
- "textwrap 0.12.1",
- "unicode-width",
- "vec_map",
+ "textwrap 0.15.0",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "3.0.0-beta.2"
+version = "3.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
+checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
 dependencies = [
- "heck",
+ "heck 0.4.0",
  "proc-macro-error",
  "proc-macro2",
  "quote",
  "syn",
 ]
 
+[[package]]
+name = "cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap 3.1.8",
+ "core-foundation",
+ "core-services",
+ "dirs 3.0.1",
+ "ipc-channel",
+ "plist",
+ "serde",
+]
+
 [[package]]
 name = "client"
 version = "0.1.0"
@@ -1083,6 +1107,60 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "collab"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-io",
+ "async-sqlx-session",
+ "async-std",
+ "async-trait",
+ "async-tungstenite",
+ "base64 0.13.0",
+ "clap 3.1.8",
+ "client",
+ "collections",
+ "comrak",
+ "ctor",
+ "editor",
+ "either",
+ "env_logger 0.8.3",
+ "envy",
+ "futures",
+ "gpui",
+ "handlebars",
+ "http-auth-basic",
+ "json_env_logger",
+ "jwt-simple",
+ "language",
+ "lazy_static",
+ "lipsum",
+ "log",
+ "lsp",
+ "oauth2",
+ "oauth2-surf",
+ "parking_lot",
+ "project",
+ "rand 0.8.3",
+ "rpc",
+ "rust-embed",
+ "scrypt",
+ "serde",
+ "serde_json",
+ "settings",
+ "sha-1 0.9.6",
+ "sqlx 0.5.5",
+ "surf",
+ "theme",
+ "tide",
+ "tide-compress",
+ "time 0.2.27",
+ "toml",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "collections"
 version = "0.1.0"
@@ -1096,6 +1174,23 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "command_palette"
+version = "0.1.0"
+dependencies = [
+ "ctor",
+ "editor",
+ "env_logger 0.8.3",
+ "fuzzy",
+ "gpui",
+ "picker",
+ "serde_json",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "comrak"
 version = "0.10.1"
@@ -1149,6 +1244,7 @@ dependencies = [
  "client",
  "gpui",
  "postage",
+ "settings",
  "theme",
  "workspace",
 ]
@@ -1207,6 +1303,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "core-services"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b344b958cae90858bf6086f49599ecc5ec8698eacad0ea155509ba11fab347"
+dependencies = [
+ "core-foundation",
+]
+
 [[package]]
 name = "core-text"
 version = "19.2.0"
@@ -1261,6 +1366,16 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "crossbeam-channel"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
+dependencies = [
+ "crossbeam-utils 0.7.2",
+ "maybe-uninit",
+]
+
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.0"
@@ -1268,7 +1383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
 ]
 
 [[package]]
@@ -1279,7 +1394,7 @@ checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
 ]
 
 [[package]]
@@ -1289,7 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d60ab4a8dba064f2fbb5aa270c28da5cf4bbd0e72dae1140a6b0353a779dbe00"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
  "lazy_static",
  "loom",
  "memoffset",
@@ -1303,7 +1418,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg 1.0.1",
+ "cfg-if 0.1.10",
+ "lazy_static",
 ]
 
 [[package]]
@@ -1407,7 +1533,7 @@ dependencies = [
  "openssl-sys",
  "schannel",
  "socket2 0.4.0",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1423,7 +1549,7 @@ dependencies = [
  "openssl-sys",
  "pkg-config",
  "vcpkg",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1498,6 +1624,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "unindent",
  "util",
@@ -1568,7 +1695,7 @@ checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
 dependencies = [
  "libc",
  "redox_users",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1579,7 +1706,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
 dependencies = [
  "libc",
  "redox_users",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1602,7 +1729,7 @@ checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
 dependencies = [
  "lazy_static",
  "libc",
- "winapi",
+ "winapi 0.3.9",
  "wio",
 ]
 
@@ -1648,10 +1775,11 @@ dependencies = [
  "clock",
  "collections",
  "ctor",
- "env_logger",
+ "env_logger 0.8.3",
  "futures",
  "fuzzy",
  "gpui",
+ "indoc",
  "itertools",
  "language",
  "lazy_static",
@@ -1664,6 +1792,7 @@ dependencies = [
  "rand 0.8.3",
  "rpc",
  "serde",
+ "settings",
  "smallvec",
  "smol",
  "snippet",
@@ -1714,6 +1843,15 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
 
+[[package]]
+name = "env_logger"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+dependencies = [
+ "log",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.8.3"
@@ -1736,6 +1874,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "erased-serde"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad132dd8d0d0b546348d7d86cb3191aad14b34e5f979781fc005c80d4ac67ffd"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "etagere"
 version = "0.2.4"
@@ -1779,9 +1926,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 
 [[package]]
 name = "fastrand"
-version = "1.4.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
 dependencies = [
  "instant",
 ]
@@ -1818,12 +1965,14 @@ version = "0.1.0"
 dependencies = [
  "ctor",
  "editor",
- "env_logger",
+ "env_logger 0.8.3",
  "fuzzy",
  "gpui",
+ "picker",
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",
@@ -1897,7 +2046,7 @@ dependencies = [
  "pathfinder_simd",
  "servo-fontconfig",
  "walkdir",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1982,6 +2131,22 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
 [[package]]
 name = "funty"
 version = "1.1.0"
@@ -2113,7 +2278,7 @@ dependencies = [
  "libc",
  "log",
  "rustc_version",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -2224,6 +2389,7 @@ dependencies = [
  "editor",
  "gpui",
  "postage",
+ "settings",
  "text",
  "workspace",
 ]
@@ -2245,7 +2411,7 @@ dependencies = [
  "core-text",
  "ctor",
  "dhat",
- "env_logger",
+ "env_logger 0.8.3",
  "etagere",
  "font-kit",
  "foreign-types",
@@ -2360,6 +2526,12 @@ dependencies = [
  "unicode-segmentation",
 ]
 
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.18"
@@ -2441,7 +2613,7 @@ checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
 dependencies = [
  "bytes 1.0.1",
  "fnv",
- "itoa",
+ "itoa 0.4.7",
 ]
 
 [[package]]
@@ -2519,7 +2691,7 @@ version = "0.4.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
 dependencies = [
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
  "globset",
  "lazy_static",
  "log",
@@ -2584,6 +2756,34 @@ dependencies = [
  "cfg-if 1.0.0",
 ]
 
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ipc-channel"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cb1d9211085f0ea6f1379d944b93c4d07e8207aa3bcf49f37eda12b85081887"
+dependencies = [
+ "bincode",
+ "crossbeam-channel 0.4.4",
+ "fnv",
+ "lazy_static",
+ "libc",
+ "mio",
+ "rand 0.7.3",
+ "serde",
+ "tempfile",
+ "uuid",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "isahc"
 version = "0.9.14"
@@ -2591,7 +2791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a"
 dependencies = [
  "bytes 0.5.6",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
  "curl",
  "curl-sys",
  "flume",
@@ -2622,6 +2822,12 @@ version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
 [[package]]
 name = "jobserver"
 version = "0.1.24"
@@ -2662,6 +2868,18 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "json_env_logger"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e2ec540ea0448b187d3a8b4a9f13e75527d06ef76b3a2baa1cd982aecb62ce2"
+dependencies = [
+ "env_logger 0.7.1",
+ "kv-log-macro",
+ "log",
+ "serde_json",
+]
+
 [[package]]
 name = "jwt-simple"
 version = "0.10.1"
@@ -2696,6 +2914,16 @@ dependencies = [
  "sha2 0.9.5",
 ]
 
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
 [[package]]
 name = "kurbo"
 version = "0.8.1"
@@ -2725,7 +2953,7 @@ dependencies = [
  "clock",
  "collections",
  "ctor",
- "env_logger",
+ "env_logger 0.8.3",
  "futures",
  "fuzzy",
  "gpui",
@@ -2745,6 +2973,7 @@ dependencies = [
  "text",
  "theme",
  "tree-sitter",
+ "tree-sitter-json",
  "tree-sitter-rust",
  "unindent",
  "util",
@@ -2791,7 +3020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
 dependencies = [
  "cfg-if 1.0.0",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -2822,6 +3051,15 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
 [[package]]
 name = "lipsum"
 version = "0.8.0"
@@ -2843,11 +3081,12 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.14"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
 dependencies = [
  "cfg-if 1.0.0",
+ "serde",
  "value-bag",
 ]
 
@@ -2880,7 +3119,7 @@ dependencies = [
  "async-pipe",
  "collections",
  "ctor",
- "env_logger",
+ "env_logger 0.8.3",
  "futures",
  "gpui",
  "log",
@@ -2928,6 +3167,12 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
 
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
 [[package]]
 name = "md-5"
 version = "0.9.1"
@@ -3021,6 +3266,37 @@ dependencies = [
  "autocfg 1.0.1",
 ]
 
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
 [[package]]
 name = "multimap"
 version = "0.8.3"
@@ -3037,6 +3313,17 @@ dependencies = [
  "socket2 0.3.19",
 ]
 
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "nom"
 version = "5.1.2"
@@ -3258,9 +3545,12 @@ dependencies = [
 
 [[package]]
 name = "os_str_bytes"
-version = "2.4.0"
+version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
 
 [[package]]
 name = "outline"
@@ -3271,7 +3561,9 @@ dependencies = [
  "gpui",
  "language",
  "ordered-float",
+ "picker",
  "postage",
+ "settings",
  "smol",
  "text",
  "workspace",
@@ -3316,7 +3608,7 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3443,6 +3735,21 @@ dependencies = [
  "indexmap",
 ]
 
+[[package]]
+name = "picker"
+version = "0.1.0"
+dependencies = [
+ "ctor",
+ "editor",
+ "env_logger 0.8.3",
+ "gpui",
+ "serde_json",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "pico-args"
 version = "0.4.0"
@@ -3505,6 +3812,20 @@ version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
 
+[[package]]
+name = "plist"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
+dependencies = [
+ "base64 0.13.0",
+ "indexmap",
+ "line-wrap",
+ "serde",
+ "time 0.3.7",
+ "xml-rs",
+]
+
 [[package]]
 name = "png"
 version = "0.16.8"
@@ -3527,7 +3848,7 @@ dependencies = [
  "libc",
  "log",
  "wepoll-sys",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3607,9 +3928,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.24"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
 dependencies = [
  "unicode-xid",
 ]
@@ -3641,6 +3962,7 @@ dependencies = [
  "rpc",
  "serde",
  "serde_json",
+ "settings",
  "sha2 0.10.2",
  "similar",
  "smol",
@@ -3660,6 +3982,7 @@ dependencies = [
  "postage",
  "project",
  "serde_json",
+ "settings",
  "theme",
  "util",
  "workspace",
@@ -3671,11 +3994,16 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "editor",
+ "futures",
  "fuzzy",
  "gpui",
+ "language",
+ "lsp",
  "ordered-float",
+ "picker",
  "postage",
  "project",
+ "settings",
  "smol",
  "text",
  "util",
@@ -3699,7 +4027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603"
 dependencies = [
  "bytes 1.0.1",
- "heck",
+ "heck 0.3.3",
  "itertools",
  "log",
  "multimap",
@@ -3764,7 +4092,7 @@ dependencies = [
  "libc",
  "rand_core 0.3.1",
  "rdrand",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3881,9 +4209,9 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
 dependencies = [
- "crossbeam-channel",
+ "crossbeam-channel 0.5.0",
  "crossbeam-deque",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
  "lazy_static",
  "num_cpus",
 ]
@@ -3945,7 +4273,7 @@ version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
 dependencies = [
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3985,7 +4313,7 @@ dependencies = [
  "spin",
  "untrusted",
  "web-sys",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -4012,11 +4340,11 @@ dependencies = [
  "async-tungstenite",
  "base64 0.13.0",
  "clock",
+ "collections",
  "futures",
  "gpui",
  "log",
  "parking_lot",
- "postage",
  "prost",
  "prost-build",
  "rand 0.8.3",
@@ -4162,6 +4490,12 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
 [[package]]
 name = "salsa20"
 version = "0.8.0"
@@ -4187,7 +4521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
 dependencies = [
  "lazy_static",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -4274,7 +4608,9 @@ dependencies = [
  "log",
  "postage",
  "project",
+ "serde",
  "serde_json",
+ "settings",
  "theme",
  "unindent",
  "util",

Dockerfile 🔗

@@ -14,20 +14,20 @@ RUN --mount=type=cache,target=./script/node_modules \
 RUN --mount=type=cache,target=./script/node_modules \
     script/build-css --release
 
-# Compile server
+# Compile collab server
 RUN --mount=type=cache,target=./script/node_modules \
     --mount=type=cache,target=/usr/local/cargo/registry \
     --mount=type=cache,target=./target \
-    cargo build --release --package zed-server --bin zed-server
+    cargo build --release --package collab --bin collab
 
-# Copy server binary out of cached directory
+# Copy collab server binary out of cached directory
 RUN --mount=type=cache,target=./target \
-    cp /app/target/release/zed-server /app/zed-server
+    cp /app/target/release/collab /app/collab
 
-# Copy server binary to the runtime image
+# Copy collab server binary to the runtime image
 FROM debian:bullseye-slim as runtime
 RUN apt-get update; \
     apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates
 WORKDIR app
-COPY --from=builder /app/zed-server /app
-ENTRYPOINT ["/app/zed-server"]
+COPY --from=builder /app/collab /app
+ENTRYPOINT ["/app/collab"]

Dockerfile.migrator 🔗

@@ -11,5 +11,5 @@ RUN apt-get update; \
     apt-get install -y --no-install-recommends libssl1.1
 WORKDIR app
 COPY --from=builder /app/bin/sqlx /app
-COPY ./server/migrations /app/migrations
+COPY ./collab/migrations /app/migrations
 ENTRYPOINT ["/app/sqlx", "migrate", "run"]

Procfile 🔗

@@ -1,2 +1,2 @@
 web: cd ../zed.dev && PORT=3000 npx next dev
-collab: cd crates/server && cargo run
+collab: cd crates/collab && cargo run

README.md 🔗

@@ -23,7 +23,7 @@ script/sqlx migrate run
 script/seed-db
 ```
 
-Run `zed.dev` and the collaboration server.
+Run the web frontend and the collaboration server.
 
 ```
 brew install foreman

assets/keymaps/default.json 🔗

@@ -0,0 +1,264 @@
+{
+    "*": {
+        "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
+        "cmd-s": "workspace::Save",
+        "cmd-alt-i": "zed::DebugElements",
+        "cmd-k cmd-left": "workspace::ActivatePreviousPane",
+        "cmd-k cmd-right": "workspace::ActivateNextPane",
+        "cmd-=": "zed::IncreaseBufferFontSize",
+        "cmd--": "zed::DecreaseBufferFontSize",
+        "cmd-,": "zed::OpenSettings"
+    },
+    "menu": {
+        "up": "menu::SelectPrev",
+        "ctrl-p": "menu::SelectPrev",
+        "down": "menu::SelectNext",
+        "ctrl-n": "menu::SelectNext",
+        "cmd-up": "menu::SelectFirst",
+        "cmd-down": "menu::SelectLast",
+        "enter": "menu::Confirm",
+        "escape": "menu::Cancel"
+    },
+    "Pane": {
+        "shift-cmd-{": "pane::ActivatePrevItem",
+        "shift-cmd-}": "pane::ActivateNextItem",
+        "cmd-w": "pane::CloseActiveItem",
+        "alt-cmd-w": "pane::CloseInactiveItems",
+        "ctrl--": "pane::GoBack",
+        "shift-ctrl-_": "pane::GoForward",
+        "cmd-k up": [
+            "pane::Split",
+            "Up"
+        ],
+        "cmd-k down": [
+            "pane::Split",
+            "Down"
+        ],
+        "cmd-k left": [
+            "pane::Split",
+            "Left"
+        ],
+        "cmd-k right": [
+            "pane::Split",
+            "Right"
+        ],
+        "cmd-shift-F": "project_search::ToggleFocus",
+        "cmd-f": "project_search::ToggleFocus",
+        "cmd-g": "search::SelectNextMatch",
+        "cmd-shift-G": "search::SelectPrevMatch"
+    },
+    "Workspace": {
+        "cmd-shift-F": "project_search::Deploy",
+        "cmd-k cmd-t": "theme_selector::Toggle",
+        "cmd-k t": "theme_selector::Reload",
+        "cmd-t": "project_symbols::Toggle",
+        "cmd-p": "file_finder::Toggle",
+        "cmd-shift-P": "command_palette::Toggle",
+        "alt-shift-D": "diagnostics::Deploy",
+        "ctrl-alt-cmd-j": "journal::NewJournalEntry",
+        "cmd-1": [
+            "workspace::ToggleSidebarItemFocus",
+            {
+                "side": "Left",
+                "item_index": 0
+            }
+        ],
+        "cmd-shift-!": [
+            "workspace::ToggleSidebarItem",
+            {
+                "side": "Left",
+                "item_index": 0
+            }
+        ]
+    },
+    "ProjectSearchBar": {
+        "enter": "project_search::Search",
+        "cmd-enter": "project_search::SearchInNew"
+    },
+    "BufferSearchBar": {
+        "escape": "buffer_search::Dismiss",
+        "cmd-f": "buffer_search::FocusEditor",
+        "enter": "search::SelectNextMatch",
+        "shift-enter": "search::SelectPrevMatch"
+    },
+    "Editor": {
+        "escape": "editor::Cancel",
+        "backspace": "editor::Backspace",
+        "ctrl-h": "editor::Backspace",
+        "delete": "editor::Delete",
+        "ctrl-d": "editor::Delete",
+        "tab": "editor::Tab",
+        "shift-tab": "editor::TabPrev",
+        "cmd-[": "editor::Outdent",
+        "cmd-]": "editor::Indent",
+        "ctrl-shift-K": "editor::DeleteLine",
+        "alt-backspace": "editor::DeleteToPreviousWordStart",
+        "alt-h": "editor::DeleteToPreviousWordStart",
+        "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+        "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
+        "alt-delete": "editor::DeleteToNextWordEnd",
+        "alt-d": "editor::DeleteToNextWordEnd",
+        "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+        "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
+        "cmd-backspace": "editor::DeleteToBeginningOfLine",
+        "cmd-delete": "editor::DeleteToEndOfLine",
+        "ctrl-k": "editor::CutToEndOfLine",
+        "cmd-shift-D": "editor::DuplicateLine",
+        "ctrl-cmd-up": "editor::MoveLineUp",
+        "ctrl-cmd-down": "editor::MoveLineDown",
+        "cmd-x": "editor::Cut",
+        "cmd-c": "editor::Copy",
+        "cmd-v": "editor::Paste",
+        "cmd-z": "editor::Undo",
+        "cmd-shift-Z": "editor::Redo",
+        "up": "editor::MoveUp",
+        "down": "editor::MoveDown",
+        "left": "editor::MoveLeft",
+        "right": "editor::MoveRight",
+        "ctrl-p": "editor::MoveUp",
+        "ctrl-n": "editor::MoveDown",
+        "ctrl-b": "editor::MoveLeft",
+        "ctrl-f": "editor::MoveRight",
+        "alt-left": "editor::MoveToPreviousWordStart",
+        "alt-b": "editor::MoveToPreviousWordStart",
+        "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+        "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
+        "alt-right": "editor::MoveToNextWordEnd",
+        "alt-f": "editor::MoveToNextWordEnd",
+        "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+        "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
+        "cmd-left": "editor::MoveToBeginningOfLine",
+        "ctrl-a": "editor::MoveToBeginningOfLine",
+        "cmd-right": "editor::MoveToEndOfLine",
+        "ctrl-e": "editor::MoveToEndOfLine",
+        "cmd-up": "editor::MoveToBeginning",
+        "cmd-down": "editor::MoveToEnd",
+        "shift-up": "editor::SelectUp",
+        "ctrl-shift-P": "editor::SelectUp",
+        "shift-down": "editor::SelectDown",
+        "ctrl-shift-N": "editor::SelectDown",
+        "shift-left": "editor::SelectLeft",
+        "ctrl-shift-B": "editor::SelectLeft",
+        "shift-right": "editor::SelectRight",
+        "ctrl-shift-F": "editor::SelectRight",
+        "alt-shift-left": "editor::SelectToPreviousWordStart",
+        "alt-shift-B": "editor::SelectToPreviousWordStart",
+        "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
+        "ctrl-alt-shift-B": "editor::SelectToPreviousSubwordStart",
+        "alt-shift-right": "editor::SelectToNextWordEnd",
+        "alt-shift-F": "editor::SelectToNextWordEnd",
+        "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
+        "cmd-shift-up": "editor::SelectToBeginning",
+        "cmd-shift-down": "editor::SelectToEnd",
+        "cmd-a": "editor::SelectAll",
+        "cmd-l": "editor::SelectLine",
+        "cmd-shift-L": "editor::SplitSelectionIntoLines",
+        "cmd-alt-up": "editor::AddSelectionAbove",
+        "cmd-ctrl-p": "editor::AddSelectionAbove",
+        "cmd-alt-down": "editor::AddSelectionBelow",
+        "cmd-ctrl-n": "editor::AddSelectionBelow",
+        "ctrl-alt-shift-F": "editor::SelectToNextSubwordEnd",
+        "cmd-shift-left": [
+            "editor::SelectToBeginningOfLine",
+            {
+                "stop_at_soft_wraps": true
+            }
+        ],
+        "ctrl-shift-A": [
+            "editor::SelectToBeginningOfLine",
+            {
+                "stop_at_soft_wraps": true
+            }
+        ],
+        "cmd-shift-right": [
+            "editor::SelectToEndOfLine",
+            {
+                "stop_at_soft_wraps": true
+            }
+        ],
+        "ctrl-shift-E": [
+            "editor::SelectToEndOfLine",
+            {
+                "stop_at_soft_wraps": true
+            }
+        ],
+        "cmd-d": [
+            "editor::SelectNext",
+            {
+                "replace_newest": false
+            }
+        ],
+        "cmd-k cmd-d": [
+            "editor::SelectNext",
+            {
+                "replace_newest": true
+            }
+        ],
+        "cmd-/": "editor::ToggleComments",
+        "alt-up": "editor::SelectLargerSyntaxNode",
+        "ctrl-w": "editor::SelectLargerSyntaxNode",
+        "alt-down": "editor::SelectSmallerSyntaxNode",
+        "ctrl-shift-W": "editor::SelectSmallerSyntaxNode",
+        "cmd-u": "editor::UndoSelection",
+        "cmd-shift-U": "editor::RedoSelection",
+        "f8": "editor::GoToNextDiagnostic",
+        "shift-f8": "editor::GoToPrevDiagnostic",
+        "f2": "editor::Rename",
+        "f12": "editor::GoToDefinition",
+        "alt-shift-f12": "editor::FindAllReferences",
+        "ctrl-m": "editor::MoveToEnclosingBracket",
+        "pageup": "editor::PageUp",
+        "pagedown": "editor::PageDown",
+        "alt-cmd-[": "editor::Fold",
+        "alt-cmd-]": "editor::UnfoldLines",
+        "alt-cmd-f": "editor::FoldSelectedRanges",
+        "ctrl-space": "editor::ShowCompletions",
+        "cmd-.": "editor::ToggleCodeActions",
+        "alt-enter": "editor::OpenExcerpts",
+        "cmd-f10": "editor::RestartLanguageServer"
+    },
+    "Editor && renaming": {
+        "enter": "editor::ConfirmRename"
+    },
+    "Editor && showing_completions": {
+        "enter": "editor::ConfirmCompletion",
+        "tab": "editor::ConfirmCompletion"
+    },
+    "Editor && showing_code_actions": {
+        "enter": "editor::ConfirmCodeAction"
+    },
+    "Editor && mode == full": {
+        "enter": "editor::Newline",
+        "cmd-f": [
+            "buffer_search::Deploy",
+            {
+                "focus": true
+            }
+        ],
+        "cmd-e": [
+            "buffer_search::Deploy",
+            {
+                "focus": false
+            }
+        ],
+        "cmd-shift-O": "outline::Toggle",
+        "ctrl-g": "go_to_line::Toggle"
+    },
+    "Editor && mode == auto_height": {
+        "alt-enter": [
+            "editor::Input",
+            "\n"
+        ]
+    },
+    "GoToLine": {
+        "escape": "go_to_line::Toggle",
+        "enter": "go_to_line::Confirm"
+    },
+    "ChatPanel": {
+        "enter": "chat_panel::Send"
+    },
+    "ProjectPanel": {
+        "left": "project_panel::CollapseSelectedEntry",
+        "right": "project_panel::ExpandSelectedEntry"
+    }
+}

assets/keymaps/vim.json 🔗

@@ -0,0 +1,93 @@
+{
+    "Editor && VimControl": {
+        "i": [
+            "vim::SwitchMode",
+            "Insert"
+        ],
+        "g": [
+            "vim::PushOperator",
+            {
+                "Namespace": "G"
+            }
+        ],
+        "h": "vim::Left",
+        "j": "vim::Down",
+        "k": "vim::Up",
+        "l": "vim::Right",
+        "0": "vim::StartOfLine",
+        "shift-$": "vim::EndOfLine",
+        "shift-G": "vim::EndOfDocument",
+        "w": "vim::NextWordStart",
+        "shift-W": [
+            "vim::NextWordStart",
+            {
+                "ignorePunctuation": true
+            }
+        ],
+        "e": "vim::NextWordEnd",
+        "shift-E": [
+            "vim::NextWordEnd",
+            {
+                "ignorePunctuation": true
+            }
+        ],
+        "b": "vim::PreviousWordStart",
+        "shift-B": [
+            "vim::PreviousWordStart",
+            {
+                "ignorePunctuation": true
+            }
+        ],
+        "escape": [
+            "vim::SwitchMode",
+            "Normal"
+        ]
+    },
+    "Editor && vim_operator == g": {
+        "g": "vim::StartOfDocument"
+    },
+    "Editor && vim_mode == insert": {
+        "escape": "vim::NormalBefore",
+        "ctrl-c": "vim::NormalBefore"
+    },
+    "Editor && vim_mode == normal": {
+        "c": [
+            "vim::PushOperator",
+            "Change"
+        ],
+        "d": [
+            "vim::PushOperator",
+            "Delete"
+        ]
+    },
+    "Editor && vim_operator == c": {
+        "w": [
+            "vim::NextWordEnd",
+            {
+                "ignorePunctuation": false
+            }
+        ],
+        "shift-W": [
+            "vim::NextWordEnd",
+            {
+                "ignorePunctuation": true
+            }
+        ]
+    },
+    "Editor && vim_operator == d": {
+        "w": [
+            "vim::NextWordStart",
+            {
+                "ignorePunctuation": false,
+                "stopAtNewline": true
+            }
+        ],
+        "shift-W": [
+            "vim::NextWordStart",
+            {
+                "ignorePunctuation": true,
+                "stopAtNewline": true
+            }
+        ]
+    }
+}

assets/themes/dark.json 🔗

@@ -0,0 +1,1338 @@
+{
+  "selector": {
+    "background": "#1c1c1c",
+    "corner_radius": 8,
+    "padding": 8,
+    "item": {
+      "padding": {
+        "bottom": 4,
+        "left": 12,
+        "right": 12,
+        "top": 4
+      },
+      "corner_radius": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#9c9c9c",
+        "size": 14
+      },
+      "highlight_text": {
+        "family": "Zed Sans",
+        "color": "#4f8ff7",
+        "weight": "bold",
+        "size": 14
+      }
+    },
+    "active_item": {
+      "padding": {
+        "bottom": 4,
+        "left": 12,
+        "right": 12,
+        "top": 4
+      },
+      "corner_radius": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#f1f1f1",
+        "size": 14
+      },
+      "highlight_text": {
+        "family": "Zed Sans",
+        "color": "#4f8ff7",
+        "weight": "bold",
+        "size": 14
+      },
+      "background": "#2b2b2b"
+    },
+    "border": {
+      "color": "#070707",
+      "width": 1
+    },
+    "empty": {
+      "text": {
+        "family": "Zed Sans",
+        "color": "#474747",
+        "size": 14
+      },
+      "padding": {
+        "bottom": 4,
+        "left": 12,
+        "right": 12,
+        "top": 8
+      }
+    },
+    "input_editor": {
+      "background": "#000000",
+      "corner_radius": 8,
+      "placeholder_text": {
+        "family": "Zed Sans",
+        "color": "#474747",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "text": {
+        "family": "Zed Mono",
+        "color": "#f1f1f1",
+        "size": 14
+      },
+      "border": {
+        "color": "#232323",
+        "width": 1
+      },
+      "padding": {
+        "bottom": 7,
+        "left": 16,
+        "right": 16,
+        "top": 7
+      }
+    },
+    "margin": {
+      "bottom": 52,
+      "top": 52
+    },
+    "shadow": {
+      "blur": 16,
+      "color": "#00000052",
+      "offset": [
+        0,
+        2
+      ]
+    }
+  },
+  "workspace": {
+    "background": "#1c1c1c",
+    "leader_border_opacity": 0.7,
+    "leader_border_width": 2,
+    "tab": {
+      "height": 32,
+      "background": "#1c1c1c",
+      "icon_close": "#555555",
+      "icon_close_active": "#ffffff",
+      "icon_conflict": "#f6a724",
+      "icon_dirty": "#135acd",
+      "icon_width": 8,
+      "spacing": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#9c9c9c",
+        "size": 14
+      },
+      "border": {
+        "color": "#070707",
+        "width": 1,
+        "left": true,
+        "bottom": true,
+        "overlay": true
+      },
+      "padding": {
+        "left": 8,
+        "right": 8
+      }
+    },
+    "active_tab": {
+      "height": 32,
+      "background": "#000000",
+      "icon_close": "#555555",
+      "icon_close_active": "#ffffff",
+      "icon_conflict": "#f6a724",
+      "icon_dirty": "#135acd",
+      "icon_width": 8,
+      "spacing": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#ffffff",
+        "size": 14
+      },
+      "border": {
+        "color": "#070707",
+        "width": 1,
+        "left": true,
+        "bottom": false,
+        "overlay": true
+      },
+      "padding": {
+        "left": 8,
+        "right": 8
+      }
+    },
+    "left_sidebar": {
+      "width": 30,
+      "background": "#1c1c1c",
+      "border": {
+        "color": "#070707",
+        "width": 1,
+        "right": true
+      },
+      "item": {
+        "height": 32,
+        "icon_color": "#9c9c9c",
+        "icon_size": 18
+      },
+      "active_item": {
+        "height": 32,
+        "icon_color": "#ffffff",
+        "icon_size": 18
+      },
+      "resize_handle": {
+        "background": "#070707",
+        "padding": {
+          "left": 1
+        }
+      }
+    },
+    "right_sidebar": {
+      "width": 30,
+      "background": "#1c1c1c",
+      "border": {
+        "color": "#070707",
+        "width": 1,
+        "left": true
+      },
+      "item": {
+        "height": 32,
+        "icon_color": "#9c9c9c",
+        "icon_size": 18
+      },
+      "active_item": {
+        "height": 32,
+        "icon_color": "#ffffff",
+        "icon_size": 18
+      },
+      "resize_handle": {
+        "background": "#070707",
+        "padding": {
+          "left": 1
+        }
+      }
+    },
+    "pane_divider": {
+      "color": "#232323",
+      "width": 1
+    },
+    "status_bar": {
+      "height": 24,
+      "item_spacing": 8,
+      "padding": {
+        "left": 6,
+        "right": 6
+      },
+      "border": {
+        "color": "#070707",
+        "width": 1,
+        "top": true,
+        "overlay": true
+      },
+      "cursor_position": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "diagnostic_message": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "lsp_message": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "auto_update_progress_message": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "auto_update_done_message": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      }
+    },
+    "titlebar": {
+      "avatar_width": 18,
+      "height": 32,
+      "background": "#2b2b2b",
+      "share_icon_color": "#9c9c9c",
+      "share_icon_active_color": "#2472f2",
+      "title": {
+        "family": "Zed Sans",
+        "color": "#f1f1f1",
+        "size": 14
+      },
+      "avatar": {
+        "corner_radius": 10,
+        "border": {
+          "color": "#00000088",
+          "width": 1
+        }
+      },
+      "avatar_ribbon": {
+        "height": 3,
+        "width": 12
+      },
+      "border": {
+        "color": "#070707",
+        "width": 1,
+        "bottom": true
+      },
+      "sign_in_prompt": {
+        "family": "Zed Sans",
+        "color": "#9c9c9c",
+        "size": 12,
+        "border": {
+          "color": "#070707",
+          "width": 1
+        },
+        "corner_radius": 6,
+        "margin": {
+          "top": 1,
+          "right": 6
+        },
+        "padding": {
+          "left": 6,
+          "right": 6
+        }
+      },
+      "hovered_sign_in_prompt": {
+        "family": "Zed Sans",
+        "color": "#ffffff",
+        "size": 12,
+        "border": {
+          "color": "#070707",
+          "width": 1
+        },
+        "corner_radius": 6,
+        "margin": {
+          "top": 1,
+          "right": 6
+        },
+        "padding": {
+          "left": 6,
+          "right": 6
+        }
+      },
+      "offline_icon": {
+        "color": "#9c9c9c",
+        "width": 16,
+        "padding": {
+          "right": 4
+        }
+      },
+      "outdated_warning": {
+        "family": "Zed Sans",
+        "color": "#f7bb57",
+        "size": 13
+      }
+    },
+    "toolbar": {
+      "height": 34,
+      "background": "#000000",
+      "border": {
+        "color": "#232323",
+        "width": 1,
+        "bottom": true
+      },
+      "item_spacing": 8,
+      "padding": {
+        "left": 16,
+        "right": 8,
+        "top": 4,
+        "bottom": 4
+      }
+    },
+    "breadcrumbs": {
+      "family": "Zed Mono",
+      "color": "#9c9c9c",
+      "size": 14,
+      "padding": {
+        "left": 6
+      }
+    },
+    "disconnected_overlay": {
+      "family": "Zed Sans",
+      "color": "#ffffff",
+      "size": 14,
+      "background": "#000000aa"
+    }
+  },
+  "editor": {
+    "text_color": "#d5d5d5",
+    "background": "#000000",
+    "active_line_background": "#ffffff12",
+    "code_actions_indicator": "#555555",
+    "diff_background_deleted": "#f15656",
+    "diff_background_inserted": "#1b9447",
+    "document_highlight_read_background": "#ffffff1f",
+    "document_highlight_write_background": "#ffffff29",
+    "error_color": "#f15656",
+    "gutter_background": "#000000",
+    "gutter_padding_factor": 3.5,
+    "highlighted_line_background": "#ffffff1f",
+    "line_number": "#474747",
+    "line_number_active": "#ffffff",
+    "rename_fade": 0.6,
+    "unnecessary_code_fade": 0.5,
+    "selection": {
+      "cursor": "#2472f2",
+      "selection": "#2472f23d"
+    },
+    "guest_selections": [
+      {
+        "cursor": "#79ba16",
+        "selection": "#79ba163d"
+      },
+      {
+        "cursor": "#d430e0",
+        "selection": "#d430e03d"
+      },
+      {
+        "cursor": "#ee670a",
+        "selection": "#ee670a3d"
+      },
+      {
+        "cursor": "#993bf3",
+        "selection": "#993bf33d"
+      },
+      {
+        "cursor": "#16d6c1",
+        "selection": "#16d6c13d"
+      },
+      {
+        "cursor": "#ef59a3",
+        "selection": "#ef59a33d"
+      },
+      {
+        "cursor": "#f7bf17",
+        "selection": "#f7bf173d"
+      }
+    ],
+    "autocomplete": {
+      "background": "#000000",
+      "corner_radius": 8,
+      "padding": 4,
+      "border": {
+        "color": "#232323",
+        "width": 1
+      },
+      "item": {
+        "corner_radius": 6,
+        "padding": {
+          "bottom": 2,
+          "left": 6,
+          "right": 6,
+          "top": 2
+        }
+      },
+      "hovered_item": {
+        "corner_radius": 6,
+        "padding": {
+          "bottom": 2,
+          "left": 6,
+          "right": 6,
+          "top": 2
+        },
+        "background": "#ffffff14"
+      },
+      "margin": {
+        "left": -14
+      },
+      "match_highlight": {
+        "family": "Zed Mono",
+        "color": "#4f8ff7",
+        "size": 14
+      },
+      "selected_item": {
+        "corner_radius": 6,
+        "padding": {
+          "bottom": 2,
+          "left": 6,
+          "right": 6,
+          "top": 2
+        },
+        "background": "#ffffff1f"
+      }
+    },
+    "diagnostic_header": {
+      "background": "#1c1c1c",
+      "icon_width_factor": 1.5,
+      "text_scale_factor": 0.857,
+      "border": {
+        "color": "#232323",
+        "width": 1,
+        "bottom": true,
+        "top": true
+      },
+      "code": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14,
+        "margin": {
+          "left": 10
+        }
+      },
+      "message": {
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#f1f1f1",
+          "size": 14,
+          "weight": "bold"
+        },
+        "text": {
+          "family": "Zed Sans",
+          "color": "#9c9c9c",
+          "size": 14
+        }
+      }
+    },
+    "diagnostic_path_header": {
+      "background": "#ffffff12",
+      "text_scale_factor": 0.857,
+      "filename": {
+        "family": "Zed Mono",
+        "color": "#f1f1f1",
+        "size": 14
+      },
+      "path": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14,
+        "margin": {
+          "left": 12
+        }
+      }
+    },
+    "error_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#f15656",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#f15656",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "warning_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#f7bb57",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#f7bb57",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "information_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "hint_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_error_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_hint_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_information_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_warning_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#070707",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "syntax": {
+      "keyword": "#4f8ff7",
+      "function": "#f9da82",
+      "string": "#f99d5f",
+      "type": "#3eeeda",
+      "number": "#aeef4b",
+      "comment": "#aaaaaa",
+      "property": "#4f8ff7",
+      "variant": "#53c1f5",
+      "constant": "#d5d5d5",
+      "title": {
+        "color": "#de900c",
+        "weight": "bold"
+      },
+      "emphasis": "#4f8ff7",
+      "emphasis_strong": {
+        "color": "#4f8ff7",
+        "weight": "bold"
+      },
+      "link_uri": {
+        "color": "#79ba16",
+        "underline": true
+      },
+      "link_text": {
+        "color": "#ee670a",
+        "italic": true
+      },
+      "list_marker": "#c6c6c6"
+    }
+  },
+  "project_diagnostics": {
+    "tab_icon_spacing": 4,
+    "tab_icon_width": 13,
+    "tab_summary_spacing": 10,
+    "empty_message": {
+      "family": "Zed Sans",
+      "color": "#f1f1f1",
+      "size": 18
+    },
+    "status_bar_item": {
+      "family": "Zed Sans",
+      "color": "#808080",
+      "size": 14,
+      "margin": {
+        "right": 10
+      }
+    }
+  },
+  "command_palette": {
+    "keystroke_spacing": 8,
+    "key": {
+      "text": {
+        "family": "Zed Mono",
+        "color": "#9c9c9c",
+        "size": 12
+      },
+      "corner_radius": 4,
+      "background": "#0e0e0e80",
+      "border": {
+        "color": "#232323",
+        "width": 1
+      },
+      "padding": {
+        "top": 2,
+        "bottom": 2,
+        "left": 8,
+        "right": 8
+      },
+      "margin": {
+        "left": 2
+      }
+    }
+  },
+  "project_panel": {
+    "padding": {
+      "top": 6,
+      "left": 12
+    },
+    "entry": {
+      "height": 22,
+      "icon_color": "#555555",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#9c9c9c",
+        "size": 14
+      }
+    },
+    "hovered_entry": {
+      "height": 22,
+      "background": "#232323",
+      "icon_color": "#555555",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#9c9c9c",
+        "size": 14
+      }
+    },
+    "selected_entry": {
+      "height": 22,
+      "icon_color": "#555555",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#f1f1f1",
+        "size": 14
+      }
+    },
+    "hovered_selected_entry": {
+      "height": 22,
+      "background": "#232323",
+      "icon_color": "#555555",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#f1f1f1",
+        "size": 14
+      }
+    }
+  },
+  "chat_panel": {
+    "padding": {
+      "top": 12,
+      "left": 12,
+      "bottom": 12,
+      "right": 12
+    },
+    "channel_name": {
+      "family": "Zed Sans",
+      "color": "#f1f1f1",
+      "weight": "bold",
+      "size": 14
+    },
+    "channel_name_hash": {
+      "family": "Zed Sans",
+      "color": "#808080",
+      "size": 14,
+      "padding": {
+        "right": 8
+      }
+    },
+    "channel_select": {
+      "header": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#f1f1f1",
+          "size": 14
+        },
+        "padding": {
+          "bottom": 4,
+          "left": 0
+        },
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "corner_radius": 0
+      },
+      "item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#9c9c9c",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "corner_radius": 0
+      },
+      "hovered_item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#9c9c9c",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "background": "#232323",
+        "corner_radius": 6
+      },
+      "active_item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#f1f1f1",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "corner_radius": 0
+      },
+      "hovered_active_item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#f1f1f1",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#808080",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "background": "#232323",
+        "corner_radius": 6
+      },
+      "menu": {
+        "background": "#000000",
+        "corner_radius": 6,
+        "padding": 4,
+        "border": {
+          "color": "#070707",
+          "width": 1
+        },
+        "shadow": {
+          "blur": 16,
+          "color": "#00000052",
+          "offset": [
+            0,
+            2
+          ]
+        }
+      }
+    },
+    "sign_in_prompt": {
+      "family": "Zed Sans",
+      "color": "#9c9c9c",
+      "underline": true,
+      "size": 14
+    },
+    "hovered_sign_in_prompt": {
+      "family": "Zed Sans",
+      "color": "#f1f1f1",
+      "underline": true,
+      "size": 14
+    },
+    "message": {
+      "body": {
+        "family": "Zed Sans",
+        "color": "#9c9c9c",
+        "size": 14
+      },
+      "timestamp": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "padding": {
+        "bottom": 6
+      },
+      "sender": {
+        "family": "Zed Sans",
+        "color": "#f1f1f1",
+        "weight": "bold",
+        "size": 14,
+        "margin": {
+          "right": 8
+        }
+      }
+    },
+    "pending_message": {
+      "body": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "timestamp": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "padding": {
+        "bottom": 6
+      },
+      "sender": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "weight": "bold",
+        "size": 14,
+        "margin": {
+          "right": 8
+        }
+      }
+    },
+    "input_editor": {
+      "background": "#000000",
+      "corner_radius": 6,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#f1f1f1",
+        "size": 14
+      },
+      "placeholder_text": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "border": {
+        "color": "#232323",
+        "width": 1
+      },
+      "padding": {
+        "bottom": 7,
+        "left": 8,
+        "right": 8,
+        "top": 7
+      }
+    }
+  },
+  "contacts_panel": {
+    "padding": {
+      "top": 12,
+      "left": 12,
+      "bottom": 12,
+      "right": 12
+    },
+    "host_row_height": 28,
+    "tree_branch_color": "#404040",
+    "tree_branch_width": 1,
+    "host_avatar": {
+      "corner_radius": 10,
+      "width": 18
+    },
+    "host_username": {
+      "family": "Zed Mono",
+      "color": "#f1f1f1",
+      "size": 14,
+      "padding": {
+        "left": 8
+      }
+    },
+    "project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      }
+    },
+    "shared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#9c9c9c",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      },
+      "background": "#1c1c1c",
+      "corner_radius": 6
+    },
+    "hovered_shared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#9c9c9c",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      },
+      "background": "#232323",
+      "corner_radius": 6
+    },
+    "unshared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      }
+    },
+    "hovered_unshared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      },
+      "corner_radius": 6
+    }
+  },
+  "search": {
+    "match_background": "#3f15a380",
+    "tab_icon_spacing": 8,
+    "tab_icon_width": 14,
+    "active_hovered_option_button": {
+      "family": "Zed Mono",
+      "color": "#ffffff",
+      "size": 14,
+      "background": "#232323",
+      "corner_radius": 4,
+      "border": {
+        "color": "#404040",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "active_option_button": {
+      "family": "Zed Mono",
+      "color": "#ffffff",
+      "size": 14,
+      "background": "#232323",
+      "corner_radius": 4,
+      "border": {
+        "color": "#404040",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "editor": {
+      "background": "#000000",
+      "corner_radius": 8,
+      "min_width": 200,
+      "max_width": 500,
+      "placeholder_text": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "text": {
+        "family": "Zed Mono",
+        "color": "#ffffff",
+        "size": 14
+      },
+      "border": {
+        "color": "#232323",
+        "width": 1
+      },
+      "margin": {
+        "right": 6
+      },
+      "padding": {
+        "top": 3,
+        "bottom": 3,
+        "left": 12,
+        "right": 8
+      }
+    },
+    "hovered_option_button": {
+      "family": "Zed Mono",
+      "color": "#ffffff",
+      "size": 14,
+      "background": "#0e0e0e",
+      "corner_radius": 4,
+      "border": {
+        "color": "#404040",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "invalid_editor": {
+      "background": "#000000",
+      "corner_radius": 8,
+      "min_width": 200,
+      "max_width": 500,
+      "placeholder_text": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "text": {
+        "family": "Zed Mono",
+        "color": "#ffffff",
+        "size": 14
+      },
+      "border": {
+        "color": "#eb2d2d",
+        "width": 1
+      },
+      "margin": {
+        "right": 6
+      },
+      "padding": {
+        "top": 3,
+        "bottom": 3,
+        "left": 12,
+        "right": 8
+      }
+    },
+    "match_index": {
+      "family": "Zed Mono",
+      "color": "#808080",
+      "size": 14,
+      "padding": 6
+    },
+    "option_button": {
+      "family": "Zed Mono",
+      "color": "#9c9c9c",
+      "size": 14,
+      "background": "#0e0e0e",
+      "corner_radius": 4,
+      "border": {
+        "color": "#232323",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "option_button_group": {
+      "padding": {
+        "left": 4,
+        "right": 4
+      }
+    },
+    "results_status": {
+      "family": "Zed Mono",
+      "color": "#f1f1f1",
+      "size": 18
+    }
+  },
+  "breadcrumbs": {
+    "family": "Zed Sans",
+    "color": "#9c9c9c",
+    "size": 14,
+    "padding": {
+      "left": 6
+    }
+  }
+}

assets/themes/light.json 🔗

@@ -0,0 +1,1338 @@
+{
+  "selector": {
+    "background": "#f8f8f8",
+    "corner_radius": 8,
+    "padding": 8,
+    "item": {
+      "padding": {
+        "bottom": 4,
+        "left": 12,
+        "right": 12,
+        "top": 4
+      },
+      "corner_radius": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#474747",
+        "size": 14
+      },
+      "highlight_text": {
+        "family": "Zed Sans",
+        "color": "#484bed",
+        "weight": "bold",
+        "size": 14
+      }
+    },
+    "active_item": {
+      "padding": {
+        "bottom": 4,
+        "left": 12,
+        "right": 12,
+        "top": 4
+      },
+      "corner_radius": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#2b2b2b",
+        "size": 14
+      },
+      "highlight_text": {
+        "family": "Zed Sans",
+        "color": "#484bed",
+        "weight": "bold",
+        "size": 14
+      },
+      "background": "#e3e3e3"
+    },
+    "border": {
+      "color": "#d5d5d5",
+      "width": 1
+    },
+    "empty": {
+      "text": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "padding": {
+        "bottom": 4,
+        "left": 12,
+        "right": 12,
+        "top": 8
+      }
+    },
+    "input_editor": {
+      "background": "#ffffff",
+      "corner_radius": 8,
+      "placeholder_text": {
+        "family": "Zed Sans",
+        "color": "#808080",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "text": {
+        "family": "Zed Mono",
+        "color": "#2b2b2b",
+        "size": 14
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1
+      },
+      "padding": {
+        "bottom": 7,
+        "left": 16,
+        "right": 16,
+        "top": 7
+      }
+    },
+    "margin": {
+      "bottom": 52,
+      "top": 52
+    },
+    "shadow": {
+      "blur": 16,
+      "color": "#0000001f",
+      "offset": [
+        0,
+        2
+      ]
+    }
+  },
+  "workspace": {
+    "background": "#f8f8f8",
+    "leader_border_opacity": 0.7,
+    "leader_border_width": 2,
+    "tab": {
+      "height": 32,
+      "background": "#f8f8f8",
+      "icon_close": "#9c9c9c",
+      "icon_close_active": "#000000",
+      "icon_conflict": "#f7bf17",
+      "icon_dirty": "#135acd",
+      "icon_width": 8,
+      "spacing": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#474747",
+        "size": 14
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "left": true,
+        "bottom": true,
+        "overlay": true
+      },
+      "padding": {
+        "left": 8,
+        "right": 8
+      }
+    },
+    "active_tab": {
+      "height": 32,
+      "background": "#ffffff",
+      "icon_close": "#9c9c9c",
+      "icon_close_active": "#000000",
+      "icon_conflict": "#f7bf17",
+      "icon_dirty": "#135acd",
+      "icon_width": 8,
+      "spacing": 8,
+      "text": {
+        "family": "Zed Sans",
+        "color": "#000000",
+        "size": 14
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "left": true,
+        "bottom": false,
+        "overlay": true
+      },
+      "padding": {
+        "left": 8,
+        "right": 8
+      }
+    },
+    "left_sidebar": {
+      "width": 30,
+      "background": "#f8f8f8",
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "right": true
+      },
+      "item": {
+        "height": 32,
+        "icon_color": "#717171",
+        "icon_size": 18
+      },
+      "active_item": {
+        "height": 32,
+        "icon_color": "#000000",
+        "icon_size": 18
+      },
+      "resize_handle": {
+        "background": "#d5d5d5",
+        "padding": {
+          "left": 1
+        }
+      }
+    },
+    "right_sidebar": {
+      "width": 30,
+      "background": "#f8f8f8",
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "left": true
+      },
+      "item": {
+        "height": 32,
+        "icon_color": "#717171",
+        "icon_size": 18
+      },
+      "active_item": {
+        "height": 32,
+        "icon_color": "#000000",
+        "icon_size": 18
+      },
+      "resize_handle": {
+        "background": "#d5d5d5",
+        "padding": {
+          "left": 1
+        }
+      }
+    },
+    "pane_divider": {
+      "color": "#d5d5d5",
+      "width": 1
+    },
+    "status_bar": {
+      "height": 24,
+      "item_spacing": 8,
+      "padding": {
+        "left": 6,
+        "right": 6
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "top": true,
+        "overlay": true
+      },
+      "cursor_position": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "diagnostic_message": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "lsp_message": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "auto_update_progress_message": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "auto_update_done_message": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      }
+    },
+    "titlebar": {
+      "avatar_width": 18,
+      "height": 32,
+      "background": "#eaeaea",
+      "share_icon_color": "#717171",
+      "share_icon_active_color": "#484bed",
+      "title": {
+        "family": "Zed Sans",
+        "color": "#2b2b2b",
+        "size": 14
+      },
+      "avatar": {
+        "corner_radius": 10,
+        "border": {
+          "color": "#00000088",
+          "width": 1
+        }
+      },
+      "avatar_ribbon": {
+        "height": 3,
+        "width": 12
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "bottom": true
+      },
+      "sign_in_prompt": {
+        "family": "Zed Sans",
+        "color": "#474747",
+        "size": 12,
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1
+        },
+        "corner_radius": 6,
+        "margin": {
+          "top": 1,
+          "right": 6
+        },
+        "padding": {
+          "left": 6,
+          "right": 6
+        }
+      },
+      "hovered_sign_in_prompt": {
+        "family": "Zed Sans",
+        "color": "#000000",
+        "size": 12,
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1
+        },
+        "corner_radius": 6,
+        "margin": {
+          "top": 1,
+          "right": 6
+        },
+        "padding": {
+          "left": 6,
+          "right": 6
+        }
+      },
+      "offline_icon": {
+        "color": "#717171",
+        "width": 16,
+        "padding": {
+          "right": 4
+        }
+      },
+      "outdated_warning": {
+        "family": "Zed Sans",
+        "color": "#d3a20b",
+        "size": 13
+      }
+    },
+    "toolbar": {
+      "height": 34,
+      "background": "#ffffff",
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "bottom": true
+      },
+      "item_spacing": 8,
+      "padding": {
+        "left": 16,
+        "right": 8,
+        "top": 4,
+        "bottom": 4
+      }
+    },
+    "breadcrumbs": {
+      "family": "Zed Mono",
+      "color": "#474747",
+      "size": 14,
+      "padding": {
+        "left": 6
+      }
+    },
+    "disconnected_overlay": {
+      "family": "Zed Sans",
+      "color": "#000000",
+      "size": 14,
+      "background": "#000000aa"
+    }
+  },
+  "editor": {
+    "text_color": "#1c1c1c",
+    "background": "#ffffff",
+    "active_line_background": "#0000000f",
+    "code_actions_indicator": "#9c9c9c",
+    "diff_background_deleted": "#fcc6c6",
+    "diff_background_inserted": "#b7f9ce",
+    "document_highlight_read_background": "#0000000f",
+    "document_highlight_write_background": "#00000029",
+    "error_color": "#eb2d2d",
+    "gutter_background": "#ffffff",
+    "gutter_padding_factor": 3.5,
+    "highlighted_line_background": "#0000001f",
+    "line_number": "#aaaaaa",
+    "line_number_active": "#000000",
+    "rename_fade": 0.6,
+    "unnecessary_code_fade": 0.5,
+    "selection": {
+      "cursor": "#2472f2",
+      "selection": "#2472f23d"
+    },
+    "guest_selections": [
+      {
+        "cursor": "#12d796",
+        "selection": "#12d7963d"
+      },
+      {
+        "cursor": "#de57e8",
+        "selection": "#de57e83d"
+      },
+      {
+        "cursor": "#f9812e",
+        "selection": "#f9812e3d"
+      },
+      {
+        "cursor": "#b066f8",
+        "selection": "#b066f83d"
+      },
+      {
+        "cursor": "#16d6c1",
+        "selection": "#16d6c13d"
+      },
+      {
+        "cursor": "#ef59a3",
+        "selection": "#ef59a33d"
+      },
+      {
+        "cursor": "#f7bf17",
+        "selection": "#f7bf173d"
+      }
+    ],
+    "autocomplete": {
+      "background": "#ffffff",
+      "corner_radius": 8,
+      "padding": 4,
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1
+      },
+      "item": {
+        "corner_radius": 6,
+        "padding": {
+          "bottom": 2,
+          "left": 6,
+          "right": 6,
+          "top": 2
+        }
+      },
+      "hovered_item": {
+        "corner_radius": 6,
+        "padding": {
+          "bottom": 2,
+          "left": 6,
+          "right": 6,
+          "top": 2
+        },
+        "background": "#00000008"
+      },
+      "margin": {
+        "left": -14
+      },
+      "match_highlight": {
+        "family": "Zed Mono",
+        "color": "#484bed",
+        "size": 14
+      },
+      "selected_item": {
+        "corner_radius": 6,
+        "padding": {
+          "bottom": 2,
+          "left": 6,
+          "right": 6,
+          "top": 2
+        },
+        "background": "#0000000f"
+      }
+    },
+    "diagnostic_header": {
+      "background": "#f8f8f8",
+      "icon_width_factor": 1.5,
+      "text_scale_factor": 0.857,
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1,
+        "bottom": true,
+        "top": true
+      },
+      "code": {
+        "family": "Zed Mono",
+        "color": "#636363",
+        "size": 14,
+        "margin": {
+          "left": 10
+        }
+      },
+      "message": {
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#2b2b2b",
+          "size": 14,
+          "weight": "bold"
+        },
+        "text": {
+          "family": "Zed Sans",
+          "color": "#474747",
+          "size": 14
+        }
+      }
+    },
+    "diagnostic_path_header": {
+      "background": "#0000000f",
+      "text_scale_factor": 0.857,
+      "filename": {
+        "family": "Zed Mono",
+        "color": "#2b2b2b",
+        "size": 14
+      },
+      "path": {
+        "family": "Zed Mono",
+        "color": "#636363",
+        "size": 14,
+        "margin": {
+          "left": 12
+        }
+      }
+    },
+    "error_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#eb2d2d",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#eb2d2d",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "warning_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#d3a20b",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#d3a20b",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "information_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "hint_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#2472f2",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_error_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_hint_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_information_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "invalid_warning_diagnostic": {
+      "text_scale_factor": 0.857,
+      "header": {
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1,
+          "top": true
+        }
+      },
+      "message": {
+        "text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14
+        },
+        "highlight_text": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "weight": "bold"
+        }
+      }
+    },
+    "syntax": {
+      "keyword": "#1819a1",
+      "function": "#bb550e",
+      "string": "#eb2d2d",
+      "type": "#a8820e",
+      "number": "#484bed",
+      "comment": "#717171",
+      "property": "#106c4e",
+      "variant": "#97142a",
+      "constant": "#1c1c1c",
+      "title": {
+        "color": "#1096d3",
+        "weight": "bold"
+      },
+      "emphasis": "#484bed",
+      "emphasis_strong": {
+        "color": "#484bed",
+        "weight": "bold"
+      },
+      "link_uri": {
+        "color": "#79ba16",
+        "underline": true
+      },
+      "link_text": {
+        "color": "#eb2d2d",
+        "italic": true
+      },
+      "list_marker": "#555555"
+    }
+  },
+  "project_diagnostics": {
+    "tab_icon_spacing": 4,
+    "tab_icon_width": 13,
+    "tab_summary_spacing": 10,
+    "empty_message": {
+      "family": "Zed Sans",
+      "color": "#2b2b2b",
+      "size": 18
+    },
+    "status_bar_item": {
+      "family": "Zed Sans",
+      "color": "#636363",
+      "size": 14,
+      "margin": {
+        "right": 10
+      }
+    }
+  },
+  "command_palette": {
+    "keystroke_spacing": 8,
+    "key": {
+      "text": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 12
+      },
+      "corner_radius": 4,
+      "background": "#f1f1f1",
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1
+      },
+      "padding": {
+        "top": 2,
+        "bottom": 2,
+        "left": 8,
+        "right": 8
+      },
+      "margin": {
+        "left": 2
+      }
+    }
+  },
+  "project_panel": {
+    "padding": {
+      "top": 6,
+      "left": 12
+    },
+    "entry": {
+      "height": 22,
+      "icon_color": "#9c9c9c",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14
+      }
+    },
+    "hovered_entry": {
+      "height": 22,
+      "background": "#eaeaea",
+      "icon_color": "#9c9c9c",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14
+      }
+    },
+    "selected_entry": {
+      "height": 22,
+      "icon_color": "#9c9c9c",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#2b2b2b",
+        "size": 14
+      }
+    },
+    "hovered_selected_entry": {
+      "height": 22,
+      "background": "#eaeaea",
+      "icon_color": "#9c9c9c",
+      "icon_size": 8,
+      "icon_spacing": 8,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#2b2b2b",
+        "size": 14
+      }
+    }
+  },
+  "chat_panel": {
+    "padding": {
+      "top": 12,
+      "left": 12,
+      "bottom": 12,
+      "right": 12
+    },
+    "channel_name": {
+      "family": "Zed Sans",
+      "color": "#2b2b2b",
+      "weight": "bold",
+      "size": 14
+    },
+    "channel_name_hash": {
+      "family": "Zed Sans",
+      "color": "#636363",
+      "size": 14,
+      "padding": {
+        "right": 8
+      }
+    },
+    "channel_select": {
+      "header": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#2b2b2b",
+          "size": 14
+        },
+        "padding": {
+          "bottom": 4,
+          "left": 0
+        },
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "corner_radius": 0
+      },
+      "item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#474747",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "corner_radius": 0
+      },
+      "hovered_item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#474747",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "background": "#eaeaea",
+        "corner_radius": 6
+      },
+      "active_item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#2b2b2b",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "corner_radius": 0
+      },
+      "hovered_active_item": {
+        "name": {
+          "family": "Zed Sans",
+          "color": "#2b2b2b",
+          "size": 14
+        },
+        "padding": 4,
+        "hash": {
+          "family": "Zed Sans",
+          "color": "#636363",
+          "size": 14,
+          "margin": {
+            "right": 8
+          }
+        },
+        "background": "#eaeaea",
+        "corner_radius": 6
+      },
+      "menu": {
+        "background": "#ffffff",
+        "corner_radius": 6,
+        "padding": 4,
+        "border": {
+          "color": "#d5d5d5",
+          "width": 1
+        },
+        "shadow": {
+          "blur": 16,
+          "color": "#0000001f",
+          "offset": [
+            0,
+            2
+          ]
+        }
+      }
+    },
+    "sign_in_prompt": {
+      "family": "Zed Sans",
+      "color": "#474747",
+      "underline": true,
+      "size": 14
+    },
+    "hovered_sign_in_prompt": {
+      "family": "Zed Sans",
+      "color": "#2b2b2b",
+      "underline": true,
+      "size": 14
+    },
+    "message": {
+      "body": {
+        "family": "Zed Sans",
+        "color": "#474747",
+        "size": 14
+      },
+      "timestamp": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "padding": {
+        "bottom": 6
+      },
+      "sender": {
+        "family": "Zed Sans",
+        "color": "#2b2b2b",
+        "weight": "bold",
+        "size": 14,
+        "margin": {
+          "right": 8
+        }
+      }
+    },
+    "pending_message": {
+      "body": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "timestamp": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "size": 14
+      },
+      "padding": {
+        "bottom": 6
+      },
+      "sender": {
+        "family": "Zed Sans",
+        "color": "#636363",
+        "weight": "bold",
+        "size": 14,
+        "margin": {
+          "right": 8
+        }
+      }
+    },
+    "input_editor": {
+      "background": "#ffffff",
+      "corner_radius": 6,
+      "text": {
+        "family": "Zed Mono",
+        "color": "#2b2b2b",
+        "size": 14
+      },
+      "placeholder_text": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1
+      },
+      "padding": {
+        "bottom": 7,
+        "left": 8,
+        "right": 8,
+        "top": 7
+      }
+    }
+  },
+  "contacts_panel": {
+    "padding": {
+      "top": 12,
+      "left": 12,
+      "bottom": 12,
+      "right": 12
+    },
+    "host_row_height": 28,
+    "tree_branch_color": "#e3e3e3",
+    "tree_branch_width": 1,
+    "host_avatar": {
+      "corner_radius": 10,
+      "width": 18
+    },
+    "host_username": {
+      "family": "Zed Mono",
+      "color": "#2b2b2b",
+      "size": 14,
+      "padding": {
+        "left": 8
+      }
+    },
+    "project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      }
+    },
+    "shared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      },
+      "background": "#f8f8f8",
+      "corner_radius": 6
+    },
+    "hovered_shared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#474747",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      },
+      "background": "#eaeaea",
+      "corner_radius": 6
+    },
+    "unshared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      }
+    },
+    "hovered_unshared_project": {
+      "guest_avatar_spacing": 4,
+      "height": 24,
+      "guest_avatar": {
+        "corner_radius": 8,
+        "width": 14
+      },
+      "name": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14,
+        "margin": {
+          "right": 6
+        }
+      },
+      "padding": {
+        "left": 8
+      },
+      "corner_radius": 6
+    }
+  },
+  "search": {
+    "match_background": "#fce9b7",
+    "tab_icon_spacing": 8,
+    "tab_icon_width": 14,
+    "active_hovered_option_button": {
+      "family": "Zed Mono",
+      "color": "#000000",
+      "size": 14,
+      "background": "#ffffff",
+      "corner_radius": 4,
+      "border": {
+        "color": "#e3e3e3",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "active_option_button": {
+      "family": "Zed Mono",
+      "color": "#000000",
+      "size": 14,
+      "background": "#ffffff",
+      "corner_radius": 4,
+      "border": {
+        "color": "#e3e3e3",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "editor": {
+      "background": "#ffffff",
+      "corner_radius": 8,
+      "min_width": 200,
+      "max_width": 500,
+      "placeholder_text": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "text": {
+        "family": "Zed Mono",
+        "color": "#000000",
+        "size": 14
+      },
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1
+      },
+      "margin": {
+        "right": 6
+      },
+      "padding": {
+        "top": 3,
+        "bottom": 3,
+        "left": 12,
+        "right": 8
+      }
+    },
+    "hovered_option_button": {
+      "family": "Zed Mono",
+      "color": "#000000",
+      "size": 14,
+      "background": "#f1f1f1",
+      "corner_radius": 4,
+      "border": {
+        "color": "#e3e3e3",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "invalid_editor": {
+      "background": "#ffffff",
+      "corner_radius": 8,
+      "min_width": 200,
+      "max_width": 500,
+      "placeholder_text": {
+        "family": "Zed Mono",
+        "color": "#808080",
+        "size": 14
+      },
+      "selection": {
+        "cursor": "#2472f2",
+        "selection": "#2472f23d"
+      },
+      "text": {
+        "family": "Zed Mono",
+        "color": "#000000",
+        "size": 14
+      },
+      "border": {
+        "color": "#f9a0a0",
+        "width": 1
+      },
+      "margin": {
+        "right": 6
+      },
+      "padding": {
+        "top": 3,
+        "bottom": 3,
+        "left": 12,
+        "right": 8
+      }
+    },
+    "match_index": {
+      "family": "Zed Mono",
+      "color": "#636363",
+      "size": 14,
+      "padding": 6
+    },
+    "option_button": {
+      "family": "Zed Mono",
+      "color": "#474747",
+      "size": 14,
+      "background": "#f1f1f1",
+      "corner_radius": 4,
+      "border": {
+        "color": "#d5d5d5",
+        "width": 1
+      },
+      "margin": {
+        "left": 2,
+        "right": 2
+      },
+      "padding": {
+        "bottom": 3,
+        "left": 8,
+        "right": 8,
+        "top": 3
+      }
+    },
+    "option_button_group": {
+      "padding": {
+        "left": 4,
+        "right": 4
+      }
+    },
+    "results_status": {
+      "family": "Zed Mono",
+      "color": "#2b2b2b",
+      "size": 18
+    }
+  },
+  "breadcrumbs": {
+    "family": "Zed Sans",
+    "color": "#474747",
+    "size": 14,
+    "padding": {
+      "left": 6
+    }
+  }
+}

crates/assets/Cargo.toml 🔗

@@ -0,0 +1,14 @@
+[package]
+name = "assets"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/assets.rs"
+doctest = false
+
+[dependencies]
+gpui = { path = "../gpui" }
+anyhow = "1.0.38"
+rust-embed = { version = "6.3", features = ["include-exclude"] }
+

crates/zed/src/assets.rs → crates/assets/src/assets.rs 🔗

@@ -3,7 +3,7 @@ use gpui::AssetSource;
 use rust_embed::RustEmbed;
 
 #[derive(RustEmbed)]
-#[folder = "assets"]
+#[folder = "../../assets"]
 #[exclude = "*.DS_Store"]
 pub struct Assets;
 

crates/auto_update/Cargo.toml 🔗

@@ -8,9 +8,10 @@ path = "src/auto_update.rs"
 doctest = false
 
 [dependencies]
+client = { path = "../client" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
-client = { path = "../client" }
 workspace = { path = "../workspace" }
 anyhow = "1.0.38"
 lazy_static = "1.4"

crates/auto_update/src/auto_update.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use client::http::{self, HttpClient};
 use gpui::{
-    action,
+    actions,
     elements::{Empty, MouseEventHandler, Text},
     platform::AppVersion,
     AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View,
@@ -9,10 +9,11 @@ use gpui::{
 };
 use lazy_static::lazy_static;
 use serde::Deserialize;
+use settings::Settings;
 use smol::{fs::File, io::AsyncReadExt, process::Command};
 use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
 use surf::Request;
-use workspace::{ItemHandle, Settings, StatusItemView};
+use workspace::{ItemHandle, StatusItemView};
 
 const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
 const ACCESS_TOKEN: &'static str = "618033988749894";
@@ -24,6 +25,8 @@ lazy_static! {
     pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
 }
 
+actions!(auto_update, [Check, DismissErrorMessage]);
+
 #[derive(Clone, PartialEq, Eq)]
 pub enum AutoUpdateStatus {
     Idle,
@@ -46,8 +49,6 @@ pub struct AutoUpdateIndicator {
     updater: Option<ModelHandle<AutoUpdater>>,
 }
 
-action!(DismissErrorMessage);
-
 #[derive(Deserialize)]
 struct JsonRelease {
     version: String,
@@ -66,16 +67,15 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut Mutab
             updater
         });
         cx.set_global(Some(auto_updater));
+        cx.add_global_action(|_: &Check, cx| {
+            if let Some(updater) = AutoUpdater::get(cx) {
+                updater.update(cx, |updater, cx| updater.poll(cx));
+            }
+        });
         cx.add_action(AutoUpdateIndicator::dismiss_error_message);
     }
 }
 
-pub fn check(cx: &mut MutableAppContext) {
-    if let Some(updater) = AutoUpdater::get(cx) {
-        updater.update(cx, |updater, cx| updater.poll(cx));
-    }
-}
-
 impl AutoUpdater {
     fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
         cx.default_global::<Option<ModelHandle<Self>>>().clone()

crates/breadcrumbs/Cargo.toml 🔗

@@ -14,6 +14,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
 search = { path = "../search" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -6,8 +6,9 @@ use gpui::{
 use language::{Buffer, OutlineItem};
 use project::Project;
 use search::ProjectSearchView;
+use settings::Settings;
 use theme::SyntaxTheme;
-use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 pub enum Event {
     UpdateLocation,

crates/chat_panel/Cargo.toml 🔗

@@ -11,6 +11,7 @@ doctest = false
 client = { path = "../client" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/chat_panel/src/chat_panel.rs 🔗

@@ -4,19 +4,18 @@ use client::{
 };
 use editor::Editor;
 use gpui::{
-    action,
+    actions,
     elements::*,
-    keymap::Binding,
     platform::CursorStyle,
     views::{ItemType, Select, SelectStyle},
     AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
     ViewContext, ViewHandle,
 };
 use postage::prelude::Stream;
+use settings::{Settings, SoftWrap};
 use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
 use util::{ResultExt, TryFutureExt};
-use workspace::{settings::SoftWrap, Settings};
 
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 
@@ -33,14 +32,11 @@ pub struct ChatPanel {
 
 pub enum Event {}
 
-action!(Send);
-action!(LoadMoreMessages);
+actions!(chat_panel, [Send, LoadMoreMessages]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ChatPanel::send);
     cx.add_action(ChatPanel::load_more_messages);
-
-    cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]);
 }
 
 impl ChatPanel {

crates/cli/Cargo.toml 🔗

@@ -0,0 +1,24 @@
+[package]
+name = "cli"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/cli.rs"
+doctest = false
+
+[[bin]]
+name = "cli"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "3.1", features = ["derive"] }
+dirs = "3.0"
+ipc-channel = "0.16"
+serde = { version = "1.0", features = ["derive"] }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+core-foundation = "0.9"
+core-services = "0.2"
+plist = "1.3"

crates/cli/src/cli.rs 🔗

@@ -0,0 +1,22 @@
+pub use ipc_channel::ipc;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+#[derive(Serialize, Deserialize)]
+pub struct IpcHandshake {
+    pub requests: ipc::IpcSender<CliRequest>,
+    pub responses: ipc::IpcReceiver<CliResponse>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum CliRequest {
+    Open { paths: Vec<PathBuf>, wait: bool },
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum CliResponse {
+    Ping,
+    Stdout { message: String },
+    Stderr { message: String },
+    Exit { status: i32 },
+}

crates/cli/src/main.rs 🔗

@@ -0,0 +1,124 @@
+use anyhow::{anyhow, Result};
+use clap::Parser;
+use cli::{CliRequest, CliResponse, IpcHandshake};
+use core_foundation::{
+    array::{CFArray, CFIndex},
+    string::kCFStringEncodingUTF8,
+    url::{CFURLCreateWithBytes, CFURL},
+};
+use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
+use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
+use serde::Deserialize;
+use std::{ffi::OsStr, fs, path::PathBuf, ptr};
+
+#[derive(Parser)]
+#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
+struct Args {
+    /// Wait for all of the given paths to be closed before exiting.
+    #[clap(short, long)]
+    wait: bool,
+    /// A sequence of space-separated paths that you want to open.
+    #[clap()]
+    paths: Vec<PathBuf>,
+    /// Print Zed's version and the app path.
+    #[clap(short, long)]
+    version: bool,
+    /// Custom Zed.app path
+    #[clap(short, long)]
+    bundle_path: Option<PathBuf>,
+}
+
+#[derive(Debug, Deserialize)]
+struct InfoPlist {
+    #[serde(rename = "CFBundleShortVersionString")]
+    bundle_short_version_string: String,
+}
+
+fn main() -> Result<()> {
+    let args = Args::parse();
+
+    let bundle_path = if let Some(bundle_path) = args.bundle_path {
+        bundle_path.canonicalize()?
+    } else {
+        locate_bundle()?
+    };
+
+    if args.version {
+        let plist_path = bundle_path.join("Contents/Info.plist");
+        let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
+        println!(
+            "Zed {} – {}",
+            plist.bundle_short_version_string,
+            bundle_path.to_string_lossy()
+        );
+        return Ok(());
+    }
+
+    let (tx, rx) = launch_app(bundle_path)?;
+
+    tx.send(CliRequest::Open {
+        paths: args
+            .paths
+            .into_iter()
+            .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
+            .collect::<Result<Vec<PathBuf>>>()?,
+        wait: args.wait,
+    })?;
+
+    while let Ok(response) = rx.recv() {
+        match response {
+            CliResponse::Ping => {}
+            CliResponse::Stdout { message } => println!("{message}"),
+            CliResponse::Stderr { message } => eprintln!("{message}"),
+            CliResponse::Exit { status } => std::process::exit(status),
+        }
+    }
+
+    Ok(())
+}
+
+fn locate_bundle() -> Result<PathBuf> {
+    let cli_path = std::env::current_exe()?.canonicalize()?;
+    let mut app_path = cli_path.clone();
+    while app_path.extension() != Some(OsStr::new("app")) {
+        if !app_path.pop() {
+            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
+        }
+    }
+    Ok(app_path)
+}
+
+fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
+    let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
+    let url = format!("zed-cli://{server_name}");
+
+    let status = unsafe {
+        let app_url =
+            CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
+        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
+            ptr::null(),
+            url.as_ptr(),
+            url.len() as CFIndex,
+            kCFStringEncodingUTF8,
+            ptr::null(),
+        ));
+        let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
+        LSOpenFromURLSpec(
+            &LSLaunchURLSpec {
+                appURL: app_url.as_concrete_TypeRef(),
+                itemURLs: urls_to_open.as_concrete_TypeRef(),
+                passThruParams: ptr::null(),
+                launchFlags: kLSLaunchDefaults,
+                asyncRefCon: ptr::null_mut(),
+            },
+            ptr::null_mut(),
+        )
+    };
+
+    if status == 0 {
+        let (_, handshake) = server.accept()?;
+        Ok((handshake.requests, handshake.responses))
+    } else {
+        Err(anyhow!("cannot start {:?}", app_path))
+    }
+}

crates/client/Cargo.toml 🔗

@@ -21,7 +21,7 @@ async-tungstenite = { version = "0.16", features = ["async-tls"] }
 futures = "0.3"
 image = "0.23"
 lazy_static = "1.4.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8.3"

crates/client/src/client.rs 🔗

@@ -13,7 +13,7 @@ use async_tungstenite::tungstenite::{
 };
 use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
 use gpui::{
-    action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
+    actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
     Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
 };
 use http::HttpClient;
@@ -50,7 +50,7 @@ lazy_static! {
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
 }
 
-action!(Authenticate);
+actions!(client, [Authenticate]);
 
 pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
     cx.add_global_action(move |_: &Authenticate, cx| {

crates/client/src/test.rs 🔗

@@ -6,7 +6,6 @@ use anyhow::{anyhow, Result};
 use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
 use gpui::{executor, ModelHandle, TestAppContext};
 use parking_lot::Mutex;
-use postage::barrier;
 use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
 use std::{fmt, rc::Rc, sync::Arc};
 
@@ -23,7 +22,6 @@ struct FakeServerState {
     connection_id: Option<ConnectionId>,
     forbid_connections: bool,
     auth_count: usize,
-    connection_killer: Option<barrier::Sender>,
     access_token: usize,
 }
 
@@ -76,15 +74,13 @@ impl FakeServer {
                             Err(EstablishConnectionError::Unauthorized)?
                         }
 
-                        let (client_conn, server_conn, kill) =
-                            Connection::in_memory(cx.background());
+                        let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
                         let (connection_id, io, incoming) =
                             peer.add_test_connection(server_conn, cx.background()).await;
                         cx.background().spawn(io).detach();
                         let mut state = state.lock();
                         state.connection_id = Some(connection_id);
                         state.incoming = Some(incoming);
-                        state.connection_killer = Some(kill);
                         Ok(client_conn)
                     })
                 }

crates/server/Cargo.toml → crates/collab/Cargo.toml 🔗

@@ -1,12 +1,12 @@
 [package]
 authors = ["Nathan Sobo <nathan@warp.dev>"]
-default-run = "zed-server"
+default-run = "collab"
 edition = "2021"
-name = "zed-server"
+name = "collab"
 version = "0.1.0"
 
 [[bin]]
-name = "zed-server"
+name = "collab"
 
 [[bin]]
 name = "seed"
@@ -15,25 +15,27 @@ required-features = ["seed-support"]
 [dependencies]
 collections = { path = "../collections" }
 rpc = { path = "../rpc" }
+util = { path = "../util" }
 anyhow = "1.0.40"
 async-io = "1.3"
 async-std = { version = "1.8.0", features = ["attributes"] }
 async-trait = "0.1.50"
 async-tungstenite = "0.16"
 base64 = "0.13"
-clap = "=3.0.0-beta.2"
+clap = "3.1"
 comrak = "0.10"
 either = "1.6"
 envy = "0.4.2"
 futures = "0.3"
 handlebars = "3.5"
 http-auth-basic = "0.1.3"
+json_env_logger = "0.1"
 jwt-simple = "0.10.0"
 lipsum = { version = "0.8", optional = true }
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 oauth2 = { version = "4.0.0", default_features = false }
 oauth2-surf = "0.1.1"
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8"
 rust-embed = { version = "6.3", features = ["include-exclude"] }
 scrypt = "0.7"
@@ -64,6 +66,8 @@ editor = { path = "../editor", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+theme = { path = "../theme" }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/server/Procfile → crates/collab/Procfile 🔗

@@ -1,2 +1,2 @@
-web: ./target/release/zed-server
+collab: ./target/release/collab
 release: ./target/release/sqlx migrate run

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

@@ -8,14 +8,14 @@ kind: Service
 apiVersion: v1
 metadata:
   namespace: ${ZED_KUBE_NAMESPACE}
-  name: zed
+  name: collab
   annotations:
     service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
-    service.beta.kubernetes.io/do-loadbalancer-certificate-id: "2634d353-1ab4-437f-add2-4ffd8f315233"
+    service.beta.kubernetes.io/do-loadbalancer-certificate-id: "40879815-9a6b-4bbb-8207-8f2c7c0218f9"
 spec:
   type: LoadBalancer
   selector:
-    app: zed
+    app: collab
   ports:
     - name: web
       protocol: TCP
@@ -26,19 +26,19 @@ apiVersion: apps/v1
 kind: Deployment
 metadata:
   namespace: ${ZED_KUBE_NAMESPACE}
-  name: zed
+  name: collab
 spec:
   replicas: 1
   selector:
     matchLabels:
-      app: zed
+      app: collab
   template:
     metadata:
       labels:
-        app: zed
+        app: collab
     spec:
       containers:
-        - name: zed
+        - name: collab
           image: "${ZED_IMAGE_ID}"
           ports:
             - containerPort: 8080
@@ -81,6 +81,10 @@ spec:
                 secretKeyRef:
                   name: api
                   key: token
+            - name: LOG_JSON
+              value: "1"
+            - name: RUST_LOG
+              value: "trace"
           securityContext:
             capabilities:
               # FIXME - Switch to the more restrictive `PERFMON` capability.

crates/server/src/api.rs → crates/collab/src/api.rs 🔗

@@ -111,7 +111,6 @@ async fn create_access_token(request: Request) -> tide::Result {
         .get_user_by_github_login(request.param("github_login")?)
         .await?
         .ok_or_else(|| surf::Error::from_str(StatusCode::NotFound, "user not found"))?;
-    let access_token = auth::create_access_token(request.db().as_ref(), user.id).await?;
 
     #[derive(Deserialize)]
     struct QueryParams {
@@ -123,9 +122,6 @@ async fn create_access_token(request: Request) -> tide::Result {
         surf::Error::from_str(StatusCode::UnprocessableEntity, "invalid query params")
     })?;
 
-    let encrypted_access_token =
-        auth::encrypt_access_token(&access_token, query_params.public_key.clone())?;
-
     let mut user_id = user.id;
     if let Some(impersonate) = query_params.impersonate {
         if user.admin {
@@ -151,6 +147,10 @@ async fn create_access_token(request: Request) -> tide::Result {
         }
     }
 
+    let access_token = auth::create_access_token(request.db().as_ref(), user_id).await?;
+    let encrypted_access_token =
+        auth::encrypt_access_token(&access_token, query_params.public_key.clone())?;
+
     Ok(tide::Response::builder(StatusCode::Ok)
         .body(json!({"user_id": user_id, "encrypted_access_token": encrypted_access_token}))
         .build())

crates/server/src/home.rs → crates/collab/src/home.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{AppState, Request, RequestExt as _};
-use serde::Deserialize;
+use log::as_serde;
+use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use tide::{http::mime, log, Server};
+use tide::{http::mime, Server};
 
 pub fn add_routes(app: &mut Server<Arc<AppState>>) {
     app.at("/").get(get_home);
@@ -18,7 +19,7 @@ async fn get_home(mut request: Request) -> tide::Result {
 }
 
 async fn post_signup(mut request: Request) -> tide::Result {
-    #[derive(Debug, Deserialize)]
+    #[derive(Debug, Deserialize, Serialize)]
     struct Form {
         github_login: String,
         email_address: String,
@@ -38,7 +39,7 @@ async fn post_signup(mut request: Request) -> tide::Result {
         .map(str::to_string)
         .unwrap_or(form.github_login);
 
-    log::info!("Signup submitted: {:?}", form);
+    log::info!(form = as_serde!(form); "signup submitted");
 
     // Save signup in the database
     request

crates/server/src/main.rs → crates/collab/src/main.rs 🔗

@@ -27,7 +27,7 @@ use rust_embed::RustEmbed;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use surf::http::cookies::SameSite;
-use tide::{log, sessions::SessionMiddleware};
+use tide::sessions::SessionMiddleware;
 use tide_compress::CompressMiddleware;
 
 type Request = tide::Request<Arc<AppState>>;
@@ -138,7 +138,11 @@ struct LayoutData {
 
 #[async_std::main]
 async fn main() -> tide::Result<()> {
-    log::start();
+    if std::env::var("LOG_JSON").is_ok() {
+        json_env_logger::init();
+    } else {
+        tide::log::start();
+    }
 
     if let Err(error) = env::load_dotenv() {
         log::error!(

crates/server/src/rpc.rs → crates/collab/src/rpc.rs 🔗

@@ -7,11 +7,14 @@ use super::{
 };
 use anyhow::anyhow;
 use async_io::Timer;
-use async_std::task;
+use async_std::{
+    sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
+    task,
+};
 use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
 use collections::{HashMap, HashSet};
 use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt};
-use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
+use log::{as_debug, as_display};
 use rpc::{
     proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
     Connection, ConnectionId, Peer, TypedEnvelope,
@@ -20,17 +23,20 @@ use sha1::{Digest as _, Sha1};
 use std::{
     any::TypeId,
     future::Future,
+    marker::PhantomData,
+    ops::{Deref, DerefMut},
+    rc::Rc,
     sync::Arc,
     time::{Duration, Instant},
 };
 use store::{Store, Worktree};
 use surf::StatusCode;
-use tide::log;
 use tide::{
     http::headers::{HeaderName, CONNECTION, UPGRADE},
     Request, Response,
 };
 use time::OffsetDateTime;
+use util::ResultExt;
 
 type MessageHandler = Box<
     dyn Send
@@ -58,6 +64,16 @@ pub struct RealExecutor;
 const MESSAGE_COUNT_PER_PAGE: usize = 100;
 const MAX_MESSAGE_LEN: usize = 1024;
 
+struct StoreReadGuard<'a> {
+    guard: RwLockReadGuard<'a, Store>,
+    _not_send: PhantomData<Rc<()>>,
+}
+
+struct StoreWriteGuard<'a> {
+    guard: RwLockWriteGuard<'a, Store>,
+    _not_send: PhantomData<Rc<()>>,
+}
+
 impl Server {
     pub fn new(
         app_state: Arc<AppState>,
@@ -78,7 +94,7 @@ impl Server {
             .add_message_handler(Server::unregister_project)
             .add_request_handler(Server::share_project)
             .add_message_handler(Server::unshare_project)
-            .add_request_handler(Server::join_project)
+            .add_sync_request_handler(Server::join_project)
             .add_message_handler(Server::leave_project)
             .add_request_handler(Server::register_worktree)
             .add_message_handler(Server::unregister_worktree)
@@ -170,6 +186,42 @@ impl Server {
         })
     }
 
+    /// Handle a request while holding a lock to the store. This is useful when we're registering
+    /// a connection but we want to respond on the connection before anybody else can send on it.
+    fn add_sync_request_handler<F, M>(&mut self, handler: F) -> &mut Self
+    where
+        F: 'static
+            + Send
+            + Sync
+            + Fn(Arc<Self>, &mut Store, TypedEnvelope<M>) -> tide::Result<M::Response>,
+        M: RequestMessage,
+    {
+        let handler = Arc::new(handler);
+        self.add_message_handler(move |server, envelope| {
+            let receipt = envelope.receipt();
+            let handler = handler.clone();
+            async move {
+                let mut store = server.store.write().await;
+                let response = (handler)(server.clone(), &mut *store, envelope);
+                match response {
+                    Ok(response) => {
+                        server.peer.respond(receipt, response)?;
+                        Ok(())
+                    }
+                    Err(error) => {
+                        server.peer.respond_with_error(
+                            receipt,
+                            proto::Error {
+                                message: error.to_string(),
+                            },
+                        )?;
+                        Err(error)
+                    }
+                }
+            }
+        })
+    }
+
     pub fn handle_connection<E: Executor>(
         self: &Arc<Self>,
         connection: Connection,
@@ -197,9 +249,10 @@ impl Server {
                 let _ = send_connection_id.send(connection_id).await;
             }
 
-            this.state_mut().add_connection(connection_id, user_id);
-            if let Err(err) = this.update_contacts_for_users(&[user_id]) {
-                log::error!("error updating contacts for {:?}: {}", user_id, err);
+            {
+                let mut state = this.state_mut().await;
+                state.add_connection(connection_id, user_id);
+                this.update_contacts_for_users(&*state, &[user_id]);
             }
 
             let handle_io = handle_io.fuse();
@@ -218,16 +271,16 @@ impl Server {
                         if let Some(message) = message {
                             let start_time = Instant::now();
                             let type_name = message.payload_type_name();
-                            log::info!("rpc message received. connection:{}, type:{}", connection_id, type_name);
+                            log::info!(connection_id = connection_id.0, type_name = type_name; "rpc message received");
                             if let Some(handler) = this.handlers.get(&message.payload_type_id()) {
                                 let notifications = this.notifications.clone();
                                 let is_background = message.is_background();
                                 let handle_message = (handler)(this.clone(), message);
                                 let handle_message = async move {
                                     if let Err(err) = handle_message.await {
-                                        log::error!("rpc message error. connection:{}, type:{}, error:{:?}", connection_id, type_name, err);
+                                        log::error!(connection_id = connection_id.0, type = type_name, error = as_display!(err); "rpc message error");
                                     } else {
-                                        log::info!("rpc message handled. connection:{}, type:{}, duration:{:?}", connection_id, type_name, start_time.elapsed());
+                                        log::info!(connection_id = connection_id.0, type = type_name, duration = as_debug!(start_time.elapsed()); "rpc message handled");
                                     }
                                     if let Some(mut notifications) = notifications {
                                         let _ = notifications.send(()).await;
@@ -242,7 +295,7 @@ impl Server {
                                 log::warn!("unhandled message: {}", type_name);
                             }
                         } else {
-                            log::info!("rpc connection closed {:?}", addr);
+                            log::info!(address = as_debug!(addr); "rpc connection closed");
                             break;
                         }
                     }
@@ -257,7 +310,8 @@ impl Server {
 
     async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> tide::Result<()> {
         self.peer.disconnect(connection_id);
-        let removed_connection = self.state_mut().remove_connection(connection_id)?;
+        let mut state = self.state_mut().await;
+        let removed_connection = state.remove_connection(connection_id)?;
 
         for (project_id, project) in removed_connection.hosted_projects {
             if let Some(share) = project.share {
@@ -268,7 +322,7 @@ impl Server {
                         self.peer
                             .send(conn_id, proto::UnshareProject { project_id })
                     },
-                )?;
+                );
             }
         }
 
@@ -281,10 +335,10 @@ impl Server {
                         peer_id: connection_id.0,
                     },
                 )
-            })?;
+            });
         }
 
-        self.update_contacts_for_users(removed_connection.contact_ids.iter())?;
+        self.update_contacts_for_users(&*state, removed_connection.contact_ids.iter());
         Ok(())
     }
 
@@ -293,11 +347,11 @@ impl Server {
     }
 
     async fn register_project(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::RegisterProject>,
     ) -> tide::Result<proto::RegisterProjectResponse> {
         let project_id = {
-            let mut state = self.state_mut();
+            let mut state = self.state_mut().await;
             let user_id = state.user_id_for_connection(request.sender_id)?;
             state.register_project(request.sender_id, user_id)
         };
@@ -305,51 +359,49 @@ impl Server {
     }
 
     async fn unregister_project(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::UnregisterProject>,
     ) -> tide::Result<()> {
-        let project = self
-            .state_mut()
-            .unregister_project(request.payload.project_id, request.sender_id)?;
-        self.update_contacts_for_users(project.authorized_user_ids().iter())?;
+        let mut state = self.state_mut().await;
+        let project = state.unregister_project(request.payload.project_id, request.sender_id)?;
+        self.update_contacts_for_users(&*state, &project.authorized_user_ids());
         Ok(())
     }
 
     async fn share_project(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::ShareProject>,
     ) -> tide::Result<proto::Ack> {
-        self.state_mut()
-            .share_project(request.payload.project_id, request.sender_id);
+        let mut state = self.state_mut().await;
+        let project = state.share_project(request.payload.project_id, request.sender_id)?;
+        self.update_contacts_for_users(&mut *state, &project.authorized_user_ids);
         Ok(proto::Ack {})
     }
 
     async fn unshare_project(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::UnshareProject>,
     ) -> tide::Result<()> {
         let project_id = request.payload.project_id;
-        let project = self
-            .state_mut()
-            .unshare_project(project_id, request.sender_id)?;
-
+        let mut state = self.state_mut().await;
+        let project = state.unshare_project(project_id, request.sender_id)?;
         broadcast(request.sender_id, project.connection_ids, |conn_id| {
             self.peer
                 .send(conn_id, proto::UnshareProject { project_id })
-        })?;
-        self.update_contacts_for_users(&project.authorized_user_ids)?;
+        });
+        self.update_contacts_for_users(&mut *state, &project.authorized_user_ids);
         Ok(())
     }
 
-    async fn join_project(
-        mut self: Arc<Server>,
+    fn join_project(
+        self: Arc<Server>,
+        state: &mut Store,
         request: TypedEnvelope<proto::JoinProject>,
     ) -> tide::Result<proto::JoinProjectResponse> {
         let project_id = request.payload.project_id;
 
-        let user_id = self.state().user_id_for_connection(request.sender_id)?;
-        let (response, connection_ids, contact_user_ids) = self
-            .state_mut()
+        let user_id = state.user_id_for_connection(request.sender_id)?;
+        let (response, connection_ids, contact_user_ids) = state
             .join_project(request.sender_id, user_id, project_id)
             .and_then(|joined| {
                 let share = joined.project.share()?;
@@ -410,19 +462,19 @@ impl Server {
                     }),
                 },
             )
-        })?;
-        self.update_contacts_for_users(&contact_user_ids)?;
+        });
+        self.update_contacts_for_users(state, &contact_user_ids);
         Ok(response)
     }
 
     async fn leave_project(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::LeaveProject>,
     ) -> tide::Result<()> {
         let sender_id = request.sender_id;
         let project_id = request.payload.project_id;
-        let worktree = self.state_mut().leave_project(sender_id, project_id)?;
-
+        let mut state = self.state_mut().await;
+        let worktree = state.leave_project(sender_id, project_id)?;
         broadcast(sender_id, worktree.connection_ids, |conn_id| {
             self.peer.send(
                 conn_id,
@@ -431,60 +483,57 @@ impl Server {
                     peer_id: sender_id.0,
                 },
             )
-        })?;
-        self.update_contacts_for_users(&worktree.authorized_user_ids)?;
-
+        });
+        self.update_contacts_for_users(&*state, &worktree.authorized_user_ids);
         Ok(())
     }
 
     async fn register_worktree(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::RegisterWorktree>,
     ) -> tide::Result<proto::Ack> {
-        let host_user_id = self.state().user_id_for_connection(request.sender_id)?;
-
         let mut contact_user_ids = HashSet::default();
-        contact_user_ids.insert(host_user_id);
         for github_login in &request.payload.authorized_logins {
             let contact_user_id = self.app_state.db.create_user(github_login, false).await?;
             contact_user_ids.insert(contact_user_id);
         }
 
+        let mut state = self.state_mut().await;
+        let host_user_id = state.user_id_for_connection(request.sender_id)?;
+        contact_user_ids.insert(host_user_id);
+
         let contact_user_ids = contact_user_ids.into_iter().collect::<Vec<_>>();
-        let guest_connection_ids;
-        {
-            let mut state = self.state_mut();
-            guest_connection_ids = state
-                .read_project(request.payload.project_id, request.sender_id)?
-                .guest_connection_ids();
-            state.register_worktree(
-                request.payload.project_id,
-                request.payload.worktree_id,
-                request.sender_id,
-                Worktree {
-                    authorized_user_ids: contact_user_ids.clone(),
-                    root_name: request.payload.root_name.clone(),
-                    visible: request.payload.visible,
-                },
-            )?;
-        }
+        let guest_connection_ids = state
+            .read_project(request.payload.project_id, request.sender_id)?
+            .guest_connection_ids();
+        state.register_worktree(
+            request.payload.project_id,
+            request.payload.worktree_id,
+            request.sender_id,
+            Worktree {
+                authorized_user_ids: contact_user_ids.clone(),
+                root_name: request.payload.root_name.clone(),
+                visible: request.payload.visible,
+            },
+        )?;
+
         broadcast(request.sender_id, guest_connection_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
-        self.update_contacts_for_users(&contact_user_ids)?;
+        });
+        self.update_contacts_for_users(&*state, &contact_user_ids);
         Ok(proto::Ack {})
     }
 
     async fn unregister_worktree(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::UnregisterWorktree>,
     ) -> tide::Result<()> {
         let project_id = request.payload.project_id;
         let worktree_id = request.payload.worktree_id;
+        let mut state = self.state_mut().await;
         let (worktree, guest_connection_ids) =
-            self.state_mut()
-                .unregister_worktree(project_id, worktree_id, request.sender_id)?;
+            state.unregister_worktree(project_id, worktree_id, request.sender_id)?;
         broadcast(request.sender_id, guest_connection_ids, |conn_id| {
             self.peer.send(
                 conn_id,
@@ -493,16 +542,16 @@ impl Server {
                     worktree_id,
                 },
             )
-        })?;
-        self.update_contacts_for_users(&worktree.authorized_user_ids)?;
+        });
+        self.update_contacts_for_users(&*state, &worktree.authorized_user_ids);
         Ok(())
     }
 
     async fn update_worktree(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::UpdateWorktree>,
     ) -> tide::Result<proto::Ack> {
-        let connection_ids = self.state_mut().update_worktree(
+        let connection_ids = self.state_mut().await.update_worktree(
             request.sender_id,
             request.payload.project_id,
             request.payload.worktree_id,
@@ -513,13 +562,13 @@ impl Server {
         broadcast(request.sender_id, connection_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
 
         Ok(proto::Ack {})
     }
 
     async fn update_diagnostic_summary(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::UpdateDiagnosticSummary>,
     ) -> tide::Result<()> {
         let summary = request
@@ -527,7 +576,7 @@ impl Server {
             .summary
             .clone()
             .ok_or_else(|| anyhow!("invalid summary"))?;
-        let receiver_ids = self.state_mut().update_diagnostic_summary(
+        let receiver_ids = self.state_mut().await.update_diagnostic_summary(
             request.payload.project_id,
             request.payload.worktree_id,
             request.sender_id,
@@ -537,15 +586,15 @@ impl Server {
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(())
     }
 
     async fn start_language_server(
-        mut self: Arc<Server>,
+        self: Arc<Server>,
         request: TypedEnvelope<proto::StartLanguageServer>,
     ) -> tide::Result<()> {
-        let receiver_ids = self.state_mut().start_language_server(
+        let receiver_ids = self.state_mut().await.start_language_server(
             request.payload.project_id,
             request.sender_id,
             request
@@ -557,7 +606,7 @@ impl Server {
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(())
     }
 
@@ -567,11 +616,12 @@ impl Server {
     ) -> tide::Result<()> {
         let receiver_ids = self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?;
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(())
     }
 
@@ -584,6 +634,7 @@ impl Server {
     {
         let host_connection_id = self
             .state()
+            .await
             .read_project(request.payload.remote_entity_id(), request.sender_id)?
             .host_connection_id;
         Ok(self
@@ -596,24 +647,25 @@ impl Server {
         self: Arc<Server>,
         request: TypedEnvelope<proto::SaveBuffer>,
     ) -> tide::Result<proto::BufferSaved> {
-        let host;
-        let mut guests;
-        {
-            let state = self.state();
-            let project = state.read_project(request.payload.project_id, request.sender_id)?;
-            host = project.host_connection_id;
-            guests = project.guest_connection_ids()
-        }
-
+        let host = self
+            .state()
+            .await
+            .read_project(request.payload.project_id, request.sender_id)?
+            .host_connection_id;
         let response = self
             .peer
             .forward_request(request.sender_id, host, request.payload.clone())
             .await?;
 
+        let mut guests = self
+            .state()
+            .await
+            .read_project(request.payload.project_id, request.sender_id)?
+            .connection_ids();
         guests.retain(|guest_connection_id| *guest_connection_id != request.sender_id);
         broadcast(host, guests, |conn_id| {
             self.peer.forward_send(host, conn_id, response.clone())
-        })?;
+        });
 
         Ok(response)
     }
@@ -624,11 +676,12 @@ impl Server {
     ) -> tide::Result<proto::Ack> {
         let receiver_ids = self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?;
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(proto::Ack {})
     }
 
@@ -638,11 +691,12 @@ impl Server {
     ) -> tide::Result<()> {
         let receiver_ids = self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?;
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(())
     }
 
@@ -652,11 +706,12 @@ impl Server {
     ) -> tide::Result<()> {
         let receiver_ids = self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?;
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(())
     }
 
@@ -666,11 +721,12 @@ impl Server {
     ) -> tide::Result<()> {
         let receiver_ids = self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?;
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
-        })?;
+        });
         Ok(())
     }
 
@@ -682,6 +738,7 @@ impl Server {
         let follower_id = request.sender_id;
         if !self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, follower_id)?
             .contains(&leader_id)
         {
@@ -704,6 +761,7 @@ impl Server {
         let leader_id = ConnectionId(request.payload.leader_id);
         if !self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?
             .contains(&leader_id)
         {
@@ -720,6 +778,7 @@ impl Server {
     ) -> tide::Result<()> {
         let connection_ids = self
             .state()
+            .await
             .project_connection_ids(request.payload.project_id, request.sender_id)?;
         let leader_id = request
             .payload
@@ -744,7 +803,10 @@ impl Server {
         self: Arc<Server>,
         request: TypedEnvelope<proto::GetChannels>,
     ) -> tide::Result<proto::GetChannelsResponse> {
-        let user_id = self.state().user_id_for_connection(request.sender_id)?;
+        let user_id = self
+            .state()
+            .await
+            .user_id_for_connection(request.sender_id)?;
         let channels = self.app_state.db.get_accessible_channels(user_id).await?;
         Ok(proto::GetChannelsResponse {
             channels: channels
@@ -783,32 +845,33 @@ impl Server {
     }
 
     fn update_contacts_for_users<'a>(
-        self: &Arc<Server>,
+        self: &Arc<Self>,
+        state: &Store,
         user_ids: impl IntoIterator<Item = &'a UserId>,
-    ) -> anyhow::Result<()> {
-        let mut result = Ok(());
-        let state = self.state();
+    ) {
         for user_id in user_ids {
             let contacts = state.contacts_for_user(*user_id);
             for connection_id in state.connection_ids_for_user(*user_id) {
-                if let Err(error) = self.peer.send(
-                    connection_id,
-                    proto::UpdateContacts {
-                        contacts: contacts.clone(),
-                    },
-                ) {
-                    result = Err(error);
-                }
+                self.peer
+                    .send(
+                        connection_id,
+                        proto::UpdateContacts {
+                            contacts: contacts.clone(),
+                        },
+                    )
+                    .log_err();
             }
         }
-        result
     }
 
     async fn join_channel(
-        mut self: Arc<Self>,
+        self: Arc<Self>,
         request: TypedEnvelope<proto::JoinChannel>,
     ) -> tide::Result<proto::JoinChannelResponse> {
-        let user_id = self.state().user_id_for_connection(request.sender_id)?;
+        let user_id = self
+            .state()
+            .await
+            .user_id_for_connection(request.sender_id)?;
         let channel_id = ChannelId::from_proto(request.payload.channel_id);
         if !self
             .app_state
@@ -819,7 +882,9 @@ impl Server {
             Err(anyhow!("access denied"))?;
         }
 
-        self.state_mut().join_channel(request.sender_id, channel_id);
+        self.state_mut()
+            .await
+            .join_channel(request.sender_id, channel_id);
         let messages = self
             .app_state
             .db
@@ -841,10 +906,13 @@ impl Server {
     }
 
     async fn leave_channel(
-        mut self: Arc<Self>,
+        self: Arc<Self>,
         request: TypedEnvelope<proto::LeaveChannel>,
     ) -> tide::Result<()> {
-        let user_id = self.state().user_id_for_connection(request.sender_id)?;
+        let user_id = self
+            .state()
+            .await
+            .user_id_for_connection(request.sender_id)?;
         let channel_id = ChannelId::from_proto(request.payload.channel_id);
         if !self
             .app_state
@@ -856,6 +924,7 @@ impl Server {
         }
 
         self.state_mut()
+            .await
             .leave_channel(request.sender_id, channel_id);
 
         Ok(())
@@ -869,7 +938,7 @@ impl Server {
         let user_id;
         let connection_ids;
         {
-            let state = self.state();
+            let state = self.state().await;
             user_id = state.user_id_for_connection(request.sender_id)?;
             connection_ids = state.channel_connection_ids(channel_id)?;
         }
@@ -910,7 +979,7 @@ impl Server {
                     message: Some(message.clone()),
                 },
             )
-        })?;
+        });
         Ok(proto::SendChannelMessageResponse {
             message: Some(message),
         })
@@ -920,7 +989,10 @@ impl Server {
         self: Arc<Self>,
         request: TypedEnvelope<proto::GetChannelMessages>,
     ) -> tide::Result<proto::GetChannelMessagesResponse> {
-        let user_id = self.state().user_id_for_connection(request.sender_id)?;
+        let user_id = self
+            .state()
+            .await
+            .user_id_for_connection(request.sender_id)?;
         let channel_id = ChannelId::from_proto(request.payload.channel_id);
         if !self
             .app_state
@@ -956,12 +1028,57 @@ impl Server {
         })
     }
 
-    fn state<'a>(self: &'a Arc<Self>) -> RwLockReadGuard<'a, Store> {
-        self.store.read()
+    async fn state<'a>(self: &'a Arc<Self>) -> StoreReadGuard<'a> {
+        #[cfg(test)]
+        async_std::task::yield_now().await;
+        let guard = self.store.read().await;
+        #[cfg(test)]
+        async_std::task::yield_now().await;
+        StoreReadGuard {
+            guard,
+            _not_send: PhantomData,
+        }
+    }
+
+    async fn state_mut<'a>(self: &'a Arc<Self>) -> StoreWriteGuard<'a> {
+        #[cfg(test)]
+        async_std::task::yield_now().await;
+        let guard = self.store.write().await;
+        #[cfg(test)]
+        async_std::task::yield_now().await;
+        StoreWriteGuard {
+            guard,
+            _not_send: PhantomData,
+        }
     }
+}
+
+impl<'a> Deref for StoreReadGuard<'a> {
+    type Target = Store;
+
+    fn deref(&self) -> &Self::Target {
+        &*self.guard
+    }
+}
+
+impl<'a> Deref for StoreWriteGuard<'a> {
+    type Target = Store;
+
+    fn deref(&self) -> &Self::Target {
+        &*self.guard
+    }
+}
+
+impl<'a> DerefMut for StoreWriteGuard<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut *self.guard
+    }
+}
 
-    fn state_mut<'a>(self: &'a mut Arc<Self>) -> RwLockWriteGuard<'a, Store> {
-        self.store.write()
+impl<'a> Drop for StoreWriteGuard<'a> {
+    fn drop(&mut self) {
+        #[cfg(test)]
+        self.check_invariants();
     }
 }
 
@@ -977,25 +1094,15 @@ impl Executor for RealExecutor {
     }
 }
 
-fn broadcast<F>(
-    sender_id: ConnectionId,
-    receiver_ids: Vec<ConnectionId>,
-    mut f: F,
-) -> anyhow::Result<()>
+fn broadcast<F>(sender_id: ConnectionId, receiver_ids: Vec<ConnectionId>, mut f: F)
 where
     F: FnMut(ConnectionId) -> anyhow::Result<()>,
 {
-    let mut result = Ok(());
     for receiver_id in receiver_ids {
         if receiver_id != sender_id {
-            if let Err(error) = f(receiver_id) {
-                if result.is_ok() {
-                    result = Err(error);
-                }
-            }
+            f(receiver_id).log_err();
         }
     }
-    result
 }
 
 pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
@@ -1080,21 +1187,24 @@ mod tests {
     use ::rpc::Peer;
     use client::{
         self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials,
-        EstablishConnectionError, UserStore,
+        EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
     };
     use collections::BTreeMap;
     use editor::{
         self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
         ToOffset, ToggleCodeActions, Undo,
     };
-    use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
+    use gpui::{
+        executor::{self, Deterministic},
+        geometry::vector::vec2f,
+        ModelHandle, TestAppContext, ViewHandle,
+    };
     use language::{
         range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
         LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
     };
     use lsp::{self, FakeLanguageServer};
     use parking_lot::Mutex;
-    use postage::barrier;
     use project::{
         fs::{FakeFs, Fs as _},
         search::SearchQuery,
@@ -1104,9 +1214,9 @@ mod tests {
     use rand::prelude::*;
     use rpc::PeerId;
     use serde_json::json;
+    use settings::Settings;
     use sqlx::types::time::OffsetDateTime;
     use std::{
-        cell::Cell,
         env,
         ops::Deref,
         path::{Path, PathBuf},
@@ -1117,7 +1227,8 @@ mod tests {
         },
         time::Duration,
     };
-    use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
+    use theme::ThemeRegistry;
+    use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -2252,6 +2363,25 @@ mod tests {
                 ]
             );
         });
+
+        // Simulate a language server reporting no errors for a file.
+        fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
+            lsp::PublishDiagnosticsParams {
+                uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
+                version: None,
+                diagnostics: vec![],
+            },
+        );
+        project_a
+            .condition(cx_a, |project, cx| {
+                project.diagnostic_summaries(cx).collect::<Vec<_>>() == &[]
+            })
+            .await;
+        project_b
+            .condition(cx_b, |project, cx| {
+                project.diagnostic_summaries(cx).collect::<Vec<_>>() == &[]
+            })
+            .await;
     }
 
     #[gpui::test(iterations = 10)]
@@ -2417,7 +2547,7 @@ mod tests {
             .condition(&cx_b, |editor, _| editor.context_menu_visible())
             .await;
         editor_b.update(cx_b, |editor, cx| {
-            editor.confirm_completion(&ConfirmCompletion(Some(0)), cx);
+            editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
             assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
         });
 
@@ -3606,7 +3736,12 @@ mod tests {
 
         // Toggle code actions and wait for them to display.
         editor_b.update(cx_b, |editor, cx| {
-            editor.toggle_code_actions(&ToggleCodeActions(false), cx);
+            editor.toggle_code_actions(
+                &ToggleCodeActions {
+                    deployed_from_indicator: false,
+                },
+                cx,
+            );
         });
         editor_b
             .condition(&cx_b, |editor, _| editor.context_menu_visible())
@@ -3617,7 +3752,7 @@ mod tests {
         // Confirming the code action will trigger a resolve request.
         let confirm_action = workspace_b
             .update(cx_b, |workspace, cx| {
-                Editor::confirm_code_action(workspace, &ConfirmCodeAction(Some(0)), cx)
+                Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
             })
             .unwrap();
         fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
@@ -4349,19 +4484,19 @@ mod tests {
         client_a
             .user_store
             .condition(&cx_a, |user_store, _| {
-                contacts(user_store) == vec![("user_a", vec![("a", vec![])])]
+                contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])]
             })
             .await;
         client_b
             .user_store
             .condition(&cx_b, |user_store, _| {
-                contacts(user_store) == vec![("user_a", vec![("a", vec![])])]
+                contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])]
             })
             .await;
         client_c
             .user_store
             .condition(&cx_c, |user_store, _| {
-                contacts(user_store) == vec![("user_a", vec![("a", vec![])])]
+                contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])]
             })
             .await;
 
@@ -4372,6 +4507,24 @@ mod tests {
             .update(cx_a, |project, cx| project.share(cx))
             .await
             .unwrap();
+        client_a
+            .user_store
+            .condition(&cx_a, |user_store, _| {
+                contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])]
+            })
+            .await;
+        client_b
+            .user_store
+            .condition(&cx_b, |user_store, _| {
+                contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])]
+            })
+            .await;
+        client_c
+            .user_store
+            .condition(&cx_c, |user_store, _| {
+                contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])]
+            })
+            .await;
 
         let _project_b = Project::remote(
             project_id,
@@ -4387,19 +4540,19 @@ mod tests {
         client_a
             .user_store
             .condition(&cx_a, |user_store, _| {
-                contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])]
+                contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])]
             })
             .await;
         client_b
             .user_store
             .condition(&cx_b, |user_store, _| {
-                contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])]
+                contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])]
             })
             .await;
         client_c
             .user_store
             .condition(&cx_c, |user_store, _| {
-                contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])]
+                contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])]
             })
             .await;
 
@@ -4423,7 +4576,7 @@ mod tests {
             .condition(&cx_c, |user_store, _| contacts(user_store) == vec![])
             .await;
 
-        fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, Vec<&str>)>)> {
+        fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, bool, Vec<&str>)>)> {
             user_store
                 .contacts()
                 .iter()
@@ -4434,6 +4587,7 @@ mod tests {
                         .map(|p| {
                             (
                                 p.worktree_root_names[0].as_str(),
+                                p.is_shared,
                                 p.guests.iter().map(|p| p.github_login.as_str()).collect(),
                             )
                         })

crates/server/src/rpc/store.rs → crates/collab/src/rpc/store.rs 🔗

@@ -66,6 +66,10 @@ pub struct JoinedProject<'a> {
     pub project: &'a Project,
 }
 
+pub struct SharedProject {
+    pub authorized_user_ids: Vec<UserId>,
+}
+
 pub struct UnsharedProject {
     pub connection_ids: Vec<ConnectionId>,
     pub authorized_user_ids: Vec<UserId>,
@@ -130,9 +134,6 @@ impl Store {
             }
         }
 
-        #[cfg(test)]
-        self.check_invariants();
-
         Ok(result)
     }
 
@@ -244,6 +245,9 @@ impl Store {
                 language_servers: Default::default(),
             },
         );
+        if let Some(connection) = self.connections.get_mut(&host_connection_id) {
+            connection.projects.insert(project_id);
+        }
         self.next_project_id += 1;
         project_id
     }
@@ -266,16 +270,12 @@ impl Store {
                     .or_default()
                     .insert(project_id);
             }
-            if let Some(connection) = self.connections.get_mut(&project.host_connection_id) {
-                connection.projects.insert(project_id);
-            }
+
             project.worktrees.insert(worktree_id, worktree);
             if let Ok(share) = project.share_mut() {
                 share.worktrees.insert(worktree_id, Default::default());
             }
 
-            #[cfg(test)]
-            self.check_invariants();
             Ok(())
         } else {
             Err(anyhow!("no such project"))?
@@ -312,8 +312,6 @@ impl Store {
                         }
                     }
 
-                    #[cfg(test)]
-                    self.check_invariants();
                     Ok(project)
                 } else {
                     Err(anyhow!("no such project"))?
@@ -358,13 +356,14 @@ impl Store {
             }
         }
 
-        #[cfg(test)]
-        self.check_invariants();
-
         Ok((worktree, guest_connection_ids))
     }
 
-    pub fn share_project(&mut self, project_id: u64, connection_id: ConnectionId) -> bool {
+    pub fn share_project(
+        &mut self,
+        project_id: u64,
+        connection_id: ConnectionId,
+    ) -> tide::Result<SharedProject> {
         if let Some(project) = self.projects.get_mut(&project_id) {
             if project.host_connection_id == connection_id {
                 let mut share = ProjectShare::default();
@@ -372,10 +371,12 @@ impl Store {
                     share.worktrees.insert(*worktree_id, Default::default());
                 }
                 project.share = Some(share);
-                return true;
+                return Ok(SharedProject {
+                    authorized_user_ids: project.authorized_user_ids(),
+                });
             }
         }
-        false
+        Err(anyhow!("no such project"))?
     }
 
     pub fn unshare_project(
@@ -402,9 +403,6 @@ impl Store {
                 }
             }
 
-            #[cfg(test)]
-            self.check_invariants();
-
             Ok(UnsharedProject {
                 connection_ids,
                 authorized_user_ids,
@@ -490,9 +488,6 @@ impl Store {
         share.active_replica_ids.insert(replica_id);
         share.guests.insert(connection_id, (replica_id, user_id));
 
-        #[cfg(test)]
-        self.check_invariants();
-
         Ok(JoinedProject {
             replica_id,
             project: &self.projects[&project_id],
@@ -525,9 +520,6 @@ impl Store {
         let connection_ids = project.connection_ids();
         let authorized_user_ids = project.authorized_user_ids();
 
-        #[cfg(test)]
-        self.check_invariants();
-
         Ok(LeftProject {
             connection_ids,
             authorized_user_ids,
@@ -555,10 +547,6 @@ impl Store {
             worktree.entries.insert(entry.id, entry.clone());
         }
         let connection_ids = project.connection_ids();
-
-        #[cfg(test)]
-        self.check_invariants();
-
         Ok(connection_ids)
     }
 
@@ -632,7 +620,7 @@ impl Store {
     }
 
     #[cfg(test)]
-    fn check_invariants(&self) {
+    pub fn check_invariants(&self) {
         for (connection_id, connection) in &self.connections {
             for project_id in &connection.projects {
                 let project = &self.projects.get(&project_id).unwrap();

crates/command_palette/Cargo.toml 🔗

@@ -0,0 +1,26 @@
+[package]
+name = "command_palette"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/command_palette.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+picker = { path = "../picker" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"

crates/command_palette/src/command_palette.rs 🔗

@@ -0,0 +1,362 @@
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    actions,
+    elements::{ChildView, Flex, Label, ParentElement},
+    keymap::Keystroke,
+    Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle,
+};
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
+use std::cmp;
+use workspace::Workspace;
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(CommandPalette::toggle);
+    Picker::<CommandPalette>::init(cx);
+}
+
+actions!(command_palette, [Toggle]);
+
+pub struct CommandPalette {
+    picker: ViewHandle<Picker<Self>>,
+    actions: Vec<Command>,
+    matches: Vec<StringMatch>,
+    selected_ix: usize,
+    focused_view_id: usize,
+}
+
+pub enum Event {
+    Dismissed,
+    Confirmed {
+        window_id: usize,
+        focused_view_id: usize,
+        action: Box<dyn Action>,
+    },
+}
+
+struct Command {
+    name: String,
+    action: Box<dyn Action>,
+    keystrokes: Vec<Keystroke>,
+}
+
+impl CommandPalette {
+    pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
+        let this = cx.weak_handle();
+        let actions = cx
+            .available_actions(cx.window_id(), focused_view_id)
+            .map(|(name, action, bindings)| Command {
+                name: humanize_action_name(name),
+                action,
+                keystrokes: bindings
+                    .last()
+                    .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
+            })
+            .collect();
+        let picker = cx.add_view(|cx| Picker::new(this, cx));
+        Self {
+            picker,
+            actions,
+            matches: vec![],
+            selected_ix: 0,
+            focused_view_id,
+        }
+    }
+
+    fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+        let workspace = cx.handle();
+        let window_id = cx.window_id();
+        let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id());
+
+        cx.as_mut().defer(move |cx| {
+            let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx));
+            workspace.update(cx, |workspace, cx| {
+                workspace.toggle_modal(cx, |cx, _| {
+                    cx.subscribe(&this, Self::on_event).detach();
+                    this
+                });
+            });
+        });
+    }
+
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<Self>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => workspace.dismiss_modal(cx),
+            Event::Confirmed {
+                window_id,
+                focused_view_id,
+                action,
+            } => {
+                let window_id = *window_id;
+                let focused_view_id = *focused_view_id;
+                let action = (*action).boxed_clone();
+                workspace.dismiss_modal(cx);
+                cx.as_mut()
+                    .defer(move |cx| cx.dispatch_action_at(window_id, focused_view_id, &*action))
+            }
+        }
+    }
+}
+
+impl Entity for CommandPalette {
+    type Event = Event;
+}
+
+impl View for CommandPalette {
+    fn ui_name() -> &'static str {
+        "CommandPalette"
+    }
+
+    fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        ChildView::new(self.picker.clone()).boxed()
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.focus(&self.picker);
+    }
+}
+
+impl PickerDelegate for CommandPalette {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_ix
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+        self.selected_ix = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut gpui::ViewContext<Self>,
+    ) -> gpui::Task<()> {
+        let candidates = self
+            .actions
+            .iter()
+            .enumerate()
+            .map(|(ix, command)| StringMatchCandidate {
+                id: ix,
+                string: command.name.to_string(),
+                char_bag: command.name.chars().collect(),
+            })
+            .collect::<Vec<_>>();
+        cx.spawn(move |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 {
+                fuzzy::match_strings(
+                    &candidates,
+                    &query,
+                    true,
+                    10000,
+                    &Default::default(),
+                    cx.background(),
+                )
+                .await
+            };
+            this.update(&mut cx, |this, _| {
+                this.matches = matches;
+                if this.matches.is_empty() {
+                    this.selected_ix = 0;
+                } else {
+                    this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
+                }
+            });
+        })
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        if !self.matches.is_empty() {
+            let action_ix = self.matches[self.selected_ix].candidate_id;
+            cx.emit(Event::Confirmed {
+                window_id: cx.window_id(),
+                focused_view_id: self.focused_view_id,
+                action: self.actions.remove(action_ix).action,
+            });
+        } else {
+            cx.emit(Event::Dismissed);
+        }
+    }
+
+    fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox {
+        let mat = &self.matches[ix];
+        let command = &self.actions[mat.candidate_id];
+        let settings = cx.global::<Settings>();
+        let theme = &settings.theme;
+        let style = if selected {
+            &theme.selector.active_item
+        } else {
+            &theme.selector.item
+        };
+        let key_style = &theme.command_palette.key;
+        let keystroke_spacing = theme.command_palette.keystroke_spacing;
+
+        Flex::row()
+            .with_child(
+                Label::new(mat.string.clone(), style.label.clone())
+                    .with_highlights(mat.positions.clone())
+                    .boxed(),
+            )
+            .with_children(command.keystrokes.iter().map(|keystroke| {
+                Flex::row()
+                    .with_children(
+                        [
+                            (keystroke.ctrl, "^"),
+                            (keystroke.alt, "⎇"),
+                            (keystroke.cmd, "⌘"),
+                            (keystroke.shift, "⇧"),
+                        ]
+                        .into_iter()
+                        .filter_map(|(modifier, label)| {
+                            if modifier {
+                                Some(
+                                    Label::new(label.into(), key_style.label.clone())
+                                        .contained()
+                                        .with_style(key_style.container)
+                                        .boxed(),
+                                )
+                            } else {
+                                None
+                            }
+                        }),
+                    )
+                    .with_child(
+                        Label::new(keystroke.key.clone(), key_style.label.clone())
+                            .contained()
+                            .with_style(key_style.container)
+                            .boxed(),
+                    )
+                    .contained()
+                    .with_margin_left(keystroke_spacing)
+                    .flex_float()
+                    .boxed()
+            }))
+            .contained()
+            .with_style(style.container)
+            .boxed()
+    }
+}
+
+fn humanize_action_name(name: &str) -> String {
+    let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
+    let mut result = String::with_capacity(capacity);
+    for char in name.chars() {
+        if char == ':' {
+            if result.ends_with(':') {
+                result.push(' ');
+            } else {
+                result.push(':');
+            }
+        } else if char.is_uppercase() {
+            if !result.ends_with(' ') {
+                result.push(' ');
+            }
+            result.extend(char.to_lowercase());
+        } else {
+            result.push(char);
+        }
+    }
+    result
+}
+
+impl std::fmt::Debug for Command {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Command")
+            .field("name", &self.name)
+            .field("keystrokes", &self.keystrokes)
+            .finish()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use editor::Editor;
+    use gpui::TestAppContext;
+    use workspace::{Workspace, WorkspaceParams};
+
+    #[test]
+    fn test_humanize_action_name() {
+        assert_eq!(
+            &humanize_action_name("editor::GoToDefinition"),
+            "editor: go to definition"
+        );
+        assert_eq!(
+            &humanize_action_name("editor::Backspace"),
+            "editor: backspace"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_command_palette(cx: &mut TestAppContext) {
+        let params = cx.update(WorkspaceParams::test);
+
+        cx.update(|cx| {
+            editor::init(cx);
+            workspace::init(&params.client, cx);
+            init(cx);
+        });
+
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let editor = cx.add_view(window_id, |cx| {
+            let mut editor = Editor::single_line(None, cx);
+            editor.set_text("abc", cx);
+            editor
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            cx.focus(editor.clone());
+            workspace.add_item(Box::new(editor.clone()), cx)
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            CommandPalette::toggle(workspace, &Toggle, cx)
+        });
+
+        let palette = workspace.read_with(cx, |workspace, _| {
+            workspace
+                .modal()
+                .unwrap()
+                .clone()
+                .downcast::<CommandPalette>()
+                .unwrap()
+        });
+
+        palette
+            .update(cx, |palette, cx| {
+                palette.update_matches("bcksp".to_string(), cx)
+            })
+            .await;
+
+        palette.update(cx, |palette, cx| {
+            assert_eq!(palette.matches[0].string, "editor: backspace");
+            palette.confirm(cx);
+        });
+
+        editor.read_with(cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "ab");
+        });
+    }
+}

crates/contacts_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 [dependencies]
 client = { path = "../client" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4.1", features = ["futures-traits"] }

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -1,5 +1,3 @@
-use std::sync::Arc;
-
 use client::{Contact, UserStore};
 use gpui::{
     elements::*,
@@ -8,7 +6,9 @@ use gpui::{
     Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
     ViewContext,
 };
-use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
+use settings::Settings;
+use std::sync::Arc;
+use workspace::{AppState, JoinProject};
 
 pub struct ContactsPanel {
     contacts: ListState,
@@ -206,10 +206,10 @@ impl ContactsPanel {
                                 })
                                 .on_click(move |cx| {
                                     if !is_host && !is_guest {
-                                        cx.dispatch_global_action(JoinProject(JoinProjectParams {
+                                        cx.dispatch_global_action(JoinProject {
                                             project_id,
                                             app_state: app_state.clone(),
-                                        }));
+                                        });
                                     }
                                 })
                                 .flex(1., true)

crates/diagnostics/Cargo.toml 🔗

@@ -14,6 +14,7 @@ editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -8,7 +8,7 @@ use editor::{
     highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
 };
 use gpui::{
-    action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
+    actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
@@ -16,6 +16,8 @@ use language::{
     Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
 };
 use project::{DiagnosticSummary, Project, ProjectPath};
+use serde_json::json;
+use settings::Settings;
 use std::{
     any::{Any, TypeId},
     cmp::Ordering,
@@ -25,14 +27,13 @@ use std::{
     sync::Arc,
 };
 use util::TryFutureExt;
-use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
 
-action!(Deploy);
+actions!(diagnostics, [Deploy]);
 
 const CONTEXT_LINE_COUNT: u32 = 1;
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]);
     cx.add_action(ProjectDiagnosticsEditor::deploy);
 }
 
@@ -91,6 +92,31 @@ impl View for ProjectDiagnosticsEditor {
             cx.focus(&self.editor);
         }
     }
+
+    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+        let project = self.project.read(cx);
+        json!({
+            "project": json!({
+                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
+                "summary": project.diagnostic_summary(cx),
+            }),
+            "summary": self.summary,
+            "paths_to_update": self.paths_to_update.iter().map(|path|
+                path.path.to_string_lossy()
+            ).collect::<Vec<_>>(),
+            "paths_states": self.path_states.iter().map(|state|
+                json!({
+                    "path": state.path.path.to_string_lossy(),
+                    "groups": state.diagnostic_groups.iter().map(|group|
+                        json!({
+                            "block_count": group.blocks.len(),
+                            "excerpt_count": group.excerpts.len(),
+                        })
+                    ).collect::<Vec<_>>(),
+                })
+            ).collect::<Vec<_>>(),
+        })
+    }
 }
 
 impl ProjectDiagnosticsEditor {

crates/diagnostics/src/items.rs 🔗

@@ -1,9 +1,11 @@
 use crate::render_summary;
 use gpui::{
-    elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
+    elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, RenderContext, View,
+    ViewContext,
 };
 use project::Project;
-use workspace::{Settings, StatusItemView};
+use settings::Settings;
+use workspace::StatusItemView;
 
 pub struct DiagnosticSummary {
     summary: project::DiagnosticSummary,
@@ -66,6 +68,10 @@ impl View for DiagnosticSummary {
         .on_click(|cx| cx.dispatch_action(crate::Deploy))
         .boxed()
     }
+
+    fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
+        serde_json::json!({ "summary": self.summary })
+    }
 }
 
 impl StatusItemView for DiagnosticSummary {

crates/editor/Cargo.toml 🔗

@@ -28,6 +28,7 @@ language = { path = "../language" }
 lsp = { path = "../lsp" }
 project = { path = "../project" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 theme = { path = "../theme" }
@@ -36,9 +37,10 @@ workspace = { path = "../workspace" }
 aho-corasick = "0.7"
 anyhow = "1.0"
 futures = "0.3"
+indoc = "1.0.4"
 itertools = "0.10"
 lazy_static = "1.4"
-log = "0.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"] }
@@ -54,6 +56,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"

crates/editor/src/context_menu.rs 🔗

@@ -0,0 +1,272 @@
+pub enum ContextMenu {
+    Completions(CompletionsMenu),
+    CodeActions(CodeActionsMenu),
+}
+
+impl ContextMenu {
+    pub fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+        if self.visible() {
+            match self {
+                ContextMenu::Completions(menu) => menu.select_prev(cx),
+                ContextMenu::CodeActions(menu) => menu.select_prev(cx),
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+        if self.visible() {
+            match self {
+                ContextMenu::Completions(menu) => menu.select_next(cx),
+                ContextMenu::CodeActions(menu) => menu.select_next(cx),
+            }
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn visible(&self) -> bool {
+        match self {
+            ContextMenu::Completions(menu) => menu.visible(),
+            ContextMenu::CodeActions(menu) => menu.visible(),
+        }
+    }
+
+    pub fn render(
+        &self,
+        cursor_position: DisplayPoint,
+        style: EditorStyle,
+        cx: &AppContext,
+    ) -> (DisplayPoint, ElementBox) {
+        match self {
+            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
+            ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
+        }
+    }
+}
+
+struct CompletionsMenu {
+    id: CompletionId,
+    initial_position: Anchor,
+    buffer: ModelHandle<Buffer>,
+    completions: Arc<[Completion]>,
+    match_candidates: Vec<StringMatchCandidate>,
+    matches: Arc<[StringMatch]>,
+    selected_item: usize,
+    list: UniformListState,
+}
+
+impl CompletionsMenu {
+    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item > 0 {
+            self.selected_item -= 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        }
+        cx.notify();
+    }
+
+    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item + 1 < self.matches.len() {
+            self.selected_item += 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        }
+        cx.notify();
+    }
+
+    fn visible(&self) -> bool {
+        !self.matches.is_empty()
+    }
+
+    fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
+        enum CompletionTag {}
+
+        let completions = self.completions.clone();
+        let matches = self.matches.clone();
+        let selected_item = self.selected_item;
+        let container_style = style.autocomplete.container;
+        UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
+            let start_ix = range.start;
+            for (ix, mat) in matches[range].iter().enumerate() {
+                let completion = &completions[mat.candidate_id];
+                let item_ix = start_ix + ix;
+                items.push(
+                    MouseEventHandler::new::<CompletionTag, _, _>(
+                        mat.candidate_id,
+                        cx,
+                        |state, _| {
+                            let item_style = if item_ix == selected_item {
+                                style.autocomplete.selected_item
+                            } else if state.hovered {
+                                style.autocomplete.hovered_item
+                            } else {
+                                style.autocomplete.item
+                            };
+
+                            Text::new(completion.label.text.clone(), style.text.clone())
+                                .with_soft_wrap(false)
+                                .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                                    &completion.label.text,
+                                    style.text.color.into(),
+                                    styled_runs_for_code_label(&completion.label, &style.syntax),
+                                    &mat.positions,
+                                ))
+                                .contained()
+                                .with_style(item_style)
+                                .boxed()
+                        },
+                    )
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_mouse_down(move |cx| {
+                        cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
+                    })
+                    .boxed(),
+                );
+            }
+        })
+        .with_width_from_item(
+            self.matches
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, mat)| {
+                    self.completions[mat.candidate_id]
+                        .label
+                        .text
+                        .chars()
+                        .count()
+                })
+                .map(|(ix, _)| ix),
+        )
+        .contained()
+        .with_style(container_style)
+        .boxed()
+    }
+
+    pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
+        let mut matches = if let Some(query) = query {
+            fuzzy::match_strings(
+                &self.match_candidates,
+                query,
+                false,
+                100,
+                &Default::default(),
+                executor,
+            )
+            .await
+        } else {
+            self.match_candidates
+                .iter()
+                .enumerate()
+                .map(|(candidate_id, candidate)| StringMatch {
+                    candidate_id,
+                    score: Default::default(),
+                    positions: Default::default(),
+                    string: candidate.string.clone(),
+                })
+                .collect()
+        };
+        matches.sort_unstable_by_key(|mat| {
+            (
+                Reverse(OrderedFloat(mat.score)),
+                self.completions[mat.candidate_id].sort_key(),
+            )
+        });
+
+        for mat in &mut matches {
+            let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
+            for position in &mut mat.positions {
+                *position += filter_start;
+            }
+        }
+
+        self.matches = matches.into();
+    }
+}
+
+#[derive(Clone)]
+struct CodeActionsMenu {
+    actions: Arc<[CodeAction]>,
+    buffer: ModelHandle<Buffer>,
+    selected_item: usize,
+    list: UniformListState,
+    deployed_from_indicator: bool,
+}
+
+impl CodeActionsMenu {
+    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item > 0 {
+            self.selected_item -= 1;
+            cx.notify()
+        }
+    }
+
+    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item + 1 < self.actions.len() {
+            self.selected_item += 1;
+            cx.notify()
+        }
+    }
+
+    fn visible(&self) -> bool {
+        !self.actions.is_empty()
+    }
+
+    fn render(
+        &self,
+        mut cursor_position: DisplayPoint,
+        style: EditorStyle,
+    ) -> (DisplayPoint, ElementBox) {
+        enum ActionTag {}
+
+        let container_style = style.autocomplete.container;
+        let actions = self.actions.clone();
+        let selected_item = self.selected_item;
+        let element =
+            UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
+                let start_ix = range.start;
+                for (ix, action) in actions[range].iter().enumerate() {
+                    let item_ix = start_ix + ix;
+                    items.push(
+                        MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
+                            let item_style = if item_ix == selected_item {
+                                style.autocomplete.selected_item
+                            } else if state.hovered {
+                                style.autocomplete.hovered_item
+                            } else {
+                                style.autocomplete.item
+                            };
+
+                            Text::new(action.lsp_action.title.clone(), style.text.clone())
+                                .with_soft_wrap(false)
+                                .contained()
+                                .with_style(item_style)
+                                .boxed()
+                        })
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_mouse_down(move |cx| {
+                            cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
+                        })
+                        .boxed(),
+                    );
+                }
+            })
+            .with_width_from_item(
+                self.actions
+                    .iter()
+                    .enumerate()
+                    .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+                    .map(|(ix, _)| ix),
+            )
+            .contained()
+            .with_style(container_style)
+            .boxed();
+
+        if self.deployed_from_indicator {
+            *cursor_position.column_mut() = 0;
+        }
+
+        (cursor_position, element)
+    }
+}

crates/editor/src/display_map.rs 🔗

@@ -12,6 +12,7 @@ use gpui::{
     Entity, ModelContext, ModelHandle,
 };
 use language::{Point, Subscription as BufferSubscription};
+use settings::Settings;
 use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
@@ -46,7 +47,6 @@ impl Entity for DisplayMap {
 impl DisplayMap {
     pub fn new(
         buffer: ModelHandle<MultiBuffer>,
-        tab_size: usize,
         font_id: FontId,
         font_size: f32,
         wrap_width: Option<f32>,
@@ -55,6 +55,8 @@ impl DisplayMap {
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+        let tab_size = Self::tab_size(&buffer, cx);
         let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
         let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
@@ -76,7 +78,9 @@ impl DisplayMap {
         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 (tabs_snapshot, edits) = self.tab_map.sync(folds_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
             .wrap_map
             .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
@@ -100,14 +104,15 @@ impl DisplayMap {
     ) {
         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 (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.tab_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.tab_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));
@@ -122,14 +127,15 @@ impl DisplayMap {
     ) {
         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 (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.tab_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.tab_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));
@@ -143,8 +149,9 @@ impl DisplayMap {
     ) -> Vec<BlockId> {
         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.tab_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));
@@ -159,8 +166,9 @@ impl DisplayMap {
     pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
         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.tab_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));
@@ -195,6 +203,16 @@ impl DisplayMap {
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
     }
 
+    fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> u32 {
+        let language_name = buffer
+            .read(cx)
+            .as_singleton()
+            .and_then(|buffer| buffer.read(cx).language())
+            .map(|language| language.name());
+
+        cx.global::<Settings>().tab_size(language_name.as_deref())
+    }
+
     #[cfg(test)]
     pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
         self.wrap_map.read(cx).is_rewrapping()
@@ -536,6 +554,8 @@ pub mod tests {
         log::info!("tab size: {}", tab_size);
         log::info!("wrap width: {:?}", wrap_width);
 
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
+
         let buffer = cx.update(|cx| {
             if rng.gen() {
                 let len = rng.gen_range(0..10);
@@ -549,7 +569,6 @@ pub mod tests {
         let map = cx.add_model(|cx| {
             DisplayMap::new(
                 buffer.clone(),
-                tab_size,
                 font_id,
                 font_size,
                 wrap_width,
@@ -759,27 +778,18 @@ pub mod tests {
 
         let font_cache = cx.font_cache();
 
-        let tab_size = 4;
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 12.0;
         let wrap_width = Some(64.);
+        cx.set_global(Settings::test(cx));
 
         let text = "one two three four five\nsix seven eight";
         let buffer = MultiBuffer::build_simple(text, cx);
         let map = cx.add_model(|cx| {
-            DisplayMap::new(
-                buffer.clone(),
-                tab_size,
-                font_id,
-                font_size,
-                wrap_width,
-                1,
-                1,
-                cx,
-            )
+            DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
         });
 
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@@ -847,18 +857,17 @@ pub mod tests {
 
     #[gpui::test]
     fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let text = sample_text(6, 6, 'a');
         let buffer = MultiBuffer::build_simple(&text, cx);
-        let tab_size = 4;
         let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
         let font_id = cx
             .font_cache()
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 14.0;
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
         buffer.update(cx, |buffer, cx| {
             buffer.edit(
                 vec![
@@ -923,12 +932,17 @@ pub mod tests {
             .unwrap(),
         );
         language.set_theme(&theme);
+        cx.update(|cx| {
+            cx.set_global(Settings {
+                tab_size: 2,
+                ..Settings::test(cx)
+            })
+        });
 
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
-        let tab_size = 2;
         let font_cache = cx.font_cache();
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
@@ -936,8 +950,7 @@ pub mod tests {
             .unwrap();
         let font_size = 14.0;
 
-        let map = cx
-            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
         assert_eq!(
             cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
             vec![
@@ -1011,22 +1024,22 @@ pub mod tests {
         );
         language.set_theme(&theme);
 
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
+
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
         let font_cache = cx.font_cache();
 
-        let tab_size = 4;
         let family_id = font_cache.load_family(&["Courier"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 16.0;
 
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
         assert_eq!(
             cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
             [
@@ -1058,6 +1071,7 @@ pub mod tests {
     async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
 
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let theme = SyntaxTheme::new(vec![
             ("operator".to_string(), Color::red().into()),
             ("string".to_string(), Color::green().into()),
@@ -1090,14 +1104,12 @@ pub mod tests {
         let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
 
         let font_cache = cx.font_cache();
-        let tab_size = 4;
         let family_id = font_cache.load_family(&["Courier"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 16.0;
-        let map = cx
-            .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+        let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
 
         enum MyType {}
 
@@ -1136,6 +1148,7 @@ pub mod tests {
 
     #[gpui::test]
     fn test_clip_point(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
             let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
 
@@ -1152,10 +1165,7 @@ pub mod tests {
                         *markers[0].column_mut() += 1;
                     }
 
-                    assert_eq!(
-                        unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
-                        markers[1]
-                    )
+                    assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
                 }
             };
         }
@@ -1187,6 +1197,8 @@ pub mod tests {
 
     #[gpui::test]
     fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
+
         fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
             let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
             unmarked_snapshot.clip_at_line_ends = true;
@@ -1204,9 +1216,9 @@ pub mod tests {
 
     #[gpui::test]
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
         let buffer = MultiBuffer::build_simple(text, cx);
-        let tab_size = 4;
         let font_cache = cx.font_cache();
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
@@ -1214,9 +1226,8 @@ pub mod tests {
             .unwrap();
         let font_size = 14.0;
 
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "✅       α\nβ   \n🏀β      γ");
         assert_eq!(
@@ -1264,17 +1275,16 @@ pub mod tests {
 
     #[gpui::test]
     fn test_max_point(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
-        let tab_size = 4;
         let font_cache = cx.font_cache();
         let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
         let font_size = 14.0;
-        let map = cx.add_model(|cx| {
-            DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
-        });
+        let map =
+            cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
             DisplayPoint::new(1, 11)

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

@@ -969,6 +969,7 @@ mod tests {
     use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
     use rand::prelude::*;
+    use settings::Settings;
     use std::env;
     use text::RandomCharIter;
 
@@ -988,6 +989,8 @@ mod tests {
 
     #[gpui::test]
     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 font_id = cx
             .font_cache()
@@ -1157,7 +1160,7 @@ mod tests {
 
         let (folds_snapshot, fold_edits) =
             fold_map.read(buffer_snapshot, subscription.consume().into_inner());
-        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, 4);
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
             wrap_map.sync(tabs_snapshot, tab_edits, cx)
         });
@@ -1167,6 +1170,8 @@ mod tests {
 
     #[gpui::test]
     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 font_id = cx
             .font_cache()
@@ -1209,6 +1214,8 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_blocks(cx: &mut gpui::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);
@@ -1296,7 +1303,8 @@ mod tests {
 
                     let (folds_snapshot, fold_edits) =
                         fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(folds_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tabs_snapshot, tab_edits, cx)
                     });
@@ -1318,7 +1326,8 @@ mod tests {
 
                     let (folds_snapshot, fold_edits) =
                         fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                    let (tabs_snapshot, tab_edits) =
+                        tab_map.sync(folds_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tabs_snapshot, tab_edits, cx)
                     });
@@ -1338,7 +1347,7 @@ 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);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                 wrap_map.sync(tabs_snapshot, tab_edits, cx)
             });

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

@@ -1210,6 +1210,7 @@ mod tests {
     use super::*;
     use crate::{MultiBuffer, ToPoint};
     use rand::prelude::*;
+    use settings::Settings;
     use std::{cmp::Reverse, env, mem, sync::Arc};
     use sum_tree::TreeMap;
     use text::RandomCharIter;
@@ -1218,6 +1219,7 @@ mod tests {
 
     #[gpui::test]
     fn test_basic_folds(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1291,6 +1293,7 @@ mod tests {
 
     #[gpui::test]
     fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1354,6 +1357,7 @@ mod tests {
 
     #[gpui::test]
     fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1404,6 +1408,7 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_folds(cx: &mut gpui::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);

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

@@ -12,7 +12,7 @@ use text::Point;
 pub struct TabMap(Mutex<TabSnapshot>);
 
 impl TabMap {
-    pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, TabSnapshot) {
+    pub fn new(input: FoldSnapshot, tab_size: u32) -> (Self, TabSnapshot) {
         let snapshot = TabSnapshot {
             fold_snapshot: input,
             tab_size,
@@ -24,12 +24,13 @@ impl TabMap {
         &self,
         fold_snapshot: FoldSnapshot,
         mut fold_edits: Vec<FoldEdit>,
+        tab_size: u32,
     ) -> (TabSnapshot, Vec<TabEdit>) {
         let mut old_snapshot = self.0.lock();
         let max_offset = old_snapshot.fold_snapshot.len();
         let new_snapshot = TabSnapshot {
             fold_snapshot,
-            tab_size: old_snapshot.tab_size,
+            tab_size,
         };
 
         let mut tab_edits = Vec::with_capacity(fold_edits.len());
@@ -87,7 +88,7 @@ impl TabMap {
 #[derive(Clone)]
 pub struct TabSnapshot {
     pub fold_snapshot: FoldSnapshot,
-    pub tab_size: usize,
+    pub tab_size: u32,
 }
 
 impl TabSnapshot {
@@ -95,6 +96,22 @@ impl TabSnapshot {
         self.fold_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
+        } else {
+            max_point.column()
+        }
+    }
+
     pub fn text_summary(&self) -> TextSummary {
         self.text_summary_for_range(TabPoint::zero()..self.max_point())
     }
@@ -234,7 +251,7 @@ impl TabSnapshot {
             .to_buffer_point(&self.fold_snapshot)
     }
 
-    fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
+    fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: u32) -> usize {
         let mut expanded_chars = 0;
         let mut expanded_bytes = 0;
         let mut collapsed_bytes = 0;
@@ -243,7 +260,7 @@ impl TabSnapshot {
                 break;
             }
             if c == '\t' {
-                let tab_len = tab_size - expanded_chars % tab_size;
+                let tab_len = tab_size as usize - expanded_chars % tab_size as usize;
                 expanded_bytes += tab_len;
                 expanded_chars += tab_len;
             } else {
@@ -259,7 +276,7 @@ impl TabSnapshot {
         mut chars: impl Iterator<Item = char>,
         column: usize,
         bias: Bias,
-        tab_size: usize,
+        tab_size: u32,
     ) -> (usize, usize, usize) {
         let mut expanded_bytes = 0;
         let mut expanded_chars = 0;
@@ -270,7 +287,7 @@ impl TabSnapshot {
             }
 
             if c == '\t' {
-                let tab_len = tab_size - (expanded_chars % tab_size);
+                let tab_len = tab_size as usize - (expanded_chars % tab_size as usize);
                 expanded_chars += tab_len;
                 expanded_bytes += tab_len;
                 if expanded_bytes > column {
@@ -383,7 +400,7 @@ pub struct TabChunks<'a> {
     column: usize,
     output_position: Point,
     max_output_position: Point,
-    tab_size: usize,
+    tab_size: u32,
     skip_leading_tab: bool,
 }
 
@@ -415,16 +432,16 @@ impl<'a> Iterator for TabChunks<'a> {
                         });
                     } else {
                         self.chunk.text = &self.chunk.text[1..];
-                        let mut len = self.tab_size - self.column % self.tab_size;
+                        let mut len = self.tab_size - self.column as u32 % self.tab_size;
                         let next_output_position = cmp::min(
-                            self.output_position + Point::new(0, len as u32),
+                            self.output_position + Point::new(0, len),
                             self.max_output_position,
                         );
-                        len = (next_output_position.column - self.output_position.column) as usize;
-                        self.column += len;
+                        len = next_output_position.column - self.output_position.column;
+                        self.column += len as usize;
                         self.output_position = next_output_position;
                         return Some(Chunk {
-                            text: &SPACES[0..len],
+                            text: &SPACES[0..len as usize],
                             ..self.chunk
                         });
                     }
@@ -516,8 +533,11 @@ mod tests {
                 actual_summary.longest_row = expected_summary.longest_row;
                 actual_summary.longest_row_chars = expected_summary.longest_row_chars;
             }
+            assert_eq!(actual_summary, expected_summary);
+        }
 
-            assert_eq!(actual_summary, expected_summary,);
+        for row in 0..=text.max_point().row {
+            assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
         }
     }
 }

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

@@ -559,11 +559,6 @@ impl WrapSnapshot {
         Patch::new(wrap_edits)
     }
 
-    pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-        self.chunks(wrap_row..self.max_point().row() + 1, false, None)
-            .map(|h| h.text)
-    }
-
     pub fn chunks<'a>(
         &'a self,
         rows: Range<u32>,
@@ -599,16 +594,23 @@ impl WrapSnapshot {
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
-        let mut len = 0;
-        for chunk in self.text_chunks(row) {
-            if let Some(newline_ix) = chunk.find('\n') {
-                len += newline_ix;
-                break;
+        let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+        cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
+        if cursor
+            .item()
+            .map_or(false, |transform| transform.is_isomorphic())
+        {
+            let overshoot = row - cursor.start().0.row();
+            let tab_row = cursor.start().1.row() + overshoot;
+            let tab_line_len = self.tab_snapshot.line_len(tab_row);
+            if overshoot == 0 {
+                cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
             } else {
-                len += chunk.len();
+                tab_line_len
             }
+        } else {
+            cursor.start().0.column()
         }
-        len as u32
     }
 
     pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
@@ -741,6 +743,7 @@ 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;
@@ -754,6 +757,8 @@ impl WrapSnapshot {
                     expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
                     prev_tab_row = tab_point.row();
                 }
+
+                assert_eq!(self.line_len(display_row), text.line_len(display_row));
             }
 
             for start_display_row in 0..expected_buffer_rows.len() {
@@ -957,6 +962,10 @@ impl WrapPoint {
         &mut self.0.row
     }
 
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
     pub fn column_mut(&mut self) -> &mut u32 {
         &mut self.0.column
     }
@@ -1014,12 +1023,14 @@ mod tests {
     use gpui::test::observe;
     use language::RandomCharIter;
     use rand::prelude::*;
+    use settings::Settings;
     use smol::stream::StreamExt;
     use std::{cmp, env};
     use text::Rope;
 
     #[gpui::test(iterations = 100)]
     async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
         cx.foreground().set_block_on_ticks(0..=50);
         cx.foreground().forbid_parking();
         let operations = env::var("OPERATIONS")
@@ -1104,7 +1115,8 @@ mod tests {
                 }
                 20..=39 => {
                     for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-                        let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+                        let (tabs_snapshot, tab_edits) =
+                            tab_map.sync(folds_snapshot, fold_edits, tab_size);
                         let (mut snapshot, wrap_edits) =
                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
@@ -1129,7 +1141,7 @@ mod tests {
                 "Unwrapped text (unexpanded tabs): {:?}",
                 folds_snapshot.text()
             );
-            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
             log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();
@@ -1269,6 +1281,11 @@ mod tests {
             self.text_chunks(0).collect()
         }
 
+        pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+            self.chunks(wrap_row..self.max_point().row() + 1, false, None)
+                .map(|h| h.text)
+        }
+
         fn verify_chunks(&mut self, rng: &mut impl Rng) {
             for _ in 0..5 {
                 let mut end_row = rng.gen_range(0..=self.max_point().row());

crates/editor/src/editor.rs 🔗

@@ -16,13 +16,13 @@ use display_map::*;
 pub use element::*;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    action,
+    actions,
     color::Color,
     elements::*,
     executor,
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::{vec2f, Vector2F},
-    keymap::Binding,
+    impl_actions, impl_internal_actions,
     platform::CursorStyle,
     text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
@@ -41,6 +41,7 @@ pub use multi_buffer::{
 use ordered_float::OrderedFloat;
 use project::{Project, ProjectTransaction};
 use serde::{Deserialize, Serialize};
+use settings::Settings;
 use smallvec::SmallVec;
 use smol::Timer;
 use snippet::Snippet;
@@ -55,93 +56,154 @@ use std::{
 };
 pub use sum_tree::Bias;
 use text::rope::TextDimension;
-use theme::DiagnosticStyle;
+use theme::{DiagnosticStyle, Theme};
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{settings, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemNavHistory, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
-action!(Cancel);
-action!(Backspace);
-action!(Delete);
-action!(Input, String);
-action!(Newline);
-action!(Tab, Direction);
-action!(Indent);
-action!(Outdent);
-action!(DeleteLine);
-action!(DeleteToPreviousWordStart);
-action!(DeleteToPreviousSubwordStart);
-action!(DeleteToNextWordEnd);
-action!(DeleteToNextSubwordEnd);
-action!(DeleteToBeginningOfLine);
-action!(DeleteToEndOfLine);
-action!(CutToEndOfLine);
-action!(DuplicateLine);
-action!(MoveLineUp);
-action!(MoveLineDown);
-action!(Cut);
-action!(Copy);
-action!(Paste);
-action!(Undo);
-action!(Redo);
-action!(MoveUp);
-action!(MoveDown);
-action!(MoveLeft);
-action!(MoveRight);
-action!(MoveToPreviousWordStart);
-action!(MoveToPreviousSubwordStart);
-action!(MoveToNextWordEnd);
-action!(MoveToNextSubwordEnd);
-action!(MoveToBeginningOfLine);
-action!(MoveToEndOfLine);
-action!(MoveToBeginning);
-action!(MoveToEnd);
-action!(SelectUp);
-action!(SelectDown);
-action!(SelectLeft);
-action!(SelectRight);
-action!(SelectToPreviousWordStart);
-action!(SelectToPreviousSubwordStart);
-action!(SelectToNextWordEnd);
-action!(SelectToNextSubwordEnd);
-action!(SelectToBeginningOfLine, bool);
-action!(SelectToEndOfLine, bool);
-action!(SelectToBeginning);
-action!(SelectToEnd);
-action!(SelectAll);
-action!(SelectLine);
-action!(SplitSelectionIntoLines);
-action!(AddSelectionAbove);
-action!(AddSelectionBelow);
-action!(SelectNext, bool);
-action!(ToggleComments);
-action!(SelectLargerSyntaxNode);
-action!(SelectSmallerSyntaxNode);
-action!(MoveToEnclosingBracket);
-action!(UndoSelection);
-action!(RedoSelection);
-action!(GoToDiagnostic, Direction);
-action!(GoToDefinition);
-action!(FindAllReferences);
-action!(Rename);
-action!(ConfirmRename);
-action!(PageUp);
-action!(PageDown);
-action!(Fold);
-action!(UnfoldLines);
-action!(FoldSelectedRanges);
-action!(Scroll, Vector2F);
-action!(Select, SelectPhase);
-action!(ShowCompletions);
-action!(ToggleCodeActions, bool);
-action!(ConfirmCompletion, Option<usize>);
-action!(ConfirmCodeAction, Option<usize>);
-action!(OpenExcerpts);
-action!(RestartLanguageServer);
+#[derive(Clone, Deserialize)]
+pub struct SelectNext {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
+
+#[derive(Clone)]
+pub struct GoToDiagnostic(pub Direction);
+
+#[derive(Clone)]
+pub struct Scroll(pub Vector2F);
+
+#[derive(Clone)]
+pub struct Select(pub SelectPhase);
+
+#[derive(Clone, Deserialize)]
+pub struct Input(pub String);
+
+#[derive(Clone, Deserialize)]
+pub struct SelectToBeginningOfLine {
+    #[serde(default)]
+    stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct SelectToEndOfLine {
+    #[serde(default)]
+    stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct ToggleCodeActions {
+    #[serde(default)]
+    pub deployed_from_indicator: bool,
+}
+
+#[derive(Clone, Default, Deserialize)]
+pub struct ConfirmCompletion {
+    #[serde(default)]
+    pub item_ix: Option<usize>,
+}
+
+#[derive(Clone, Default, Deserialize)]
+pub struct ConfirmCodeAction {
+    #[serde(default)]
+    pub item_ix: Option<usize>,
+}
+
+actions!(
+    editor,
+    [
+        Cancel,
+        Backspace,
+        Delete,
+        Newline,
+        GoToNextDiagnostic,
+        GoToPrevDiagnostic,
+        Indent,
+        Outdent,
+        DeleteLine,
+        DeleteToPreviousWordStart,
+        DeleteToPreviousSubwordStart,
+        DeleteToNextWordEnd,
+        DeleteToNextSubwordEnd,
+        DeleteToBeginningOfLine,
+        DeleteToEndOfLine,
+        CutToEndOfLine,
+        DuplicateLine,
+        MoveLineUp,
+        MoveLineDown,
+        Cut,
+        Copy,
+        Paste,
+        Undo,
+        Redo,
+        MoveUp,
+        MoveDown,
+        MoveLeft,
+        MoveRight,
+        MoveToPreviousWordStart,
+        MoveToPreviousSubwordStart,
+        MoveToNextWordEnd,
+        MoveToNextSubwordEnd,
+        MoveToBeginningOfLine,
+        MoveToEndOfLine,
+        MoveToBeginning,
+        MoveToEnd,
+        SelectUp,
+        SelectDown,
+        SelectLeft,
+        SelectRight,
+        SelectToPreviousWordStart,
+        SelectToPreviousSubwordStart,
+        SelectToNextWordEnd,
+        SelectToNextSubwordEnd,
+        SelectToBeginning,
+        SelectToEnd,
+        SelectAll,
+        SelectLine,
+        SplitSelectionIntoLines,
+        AddSelectionAbove,
+        AddSelectionBelow,
+        Tab,
+        TabPrev,
+        ToggleComments,
+        SelectLargerSyntaxNode,
+        SelectSmallerSyntaxNode,
+        MoveToEnclosingBracket,
+        UndoSelection,
+        RedoSelection,
+        GoToDefinition,
+        FindAllReferences,
+        Rename,
+        ConfirmRename,
+        PageUp,
+        PageDown,
+        Fold,
+        UnfoldLines,
+        FoldSelectedRanges,
+        ShowCompletions,
+        OpenExcerpts,
+        RestartLanguageServer,
+    ]
+);
+
+impl_actions!(
+    editor,
+    [
+        Input,
+        SelectNext,
+        SelectToBeginningOfLine,
+        SelectToEndOfLine,
+        ToggleCodeActions,
+        ConfirmCompletion,
+        ConfirmCodeAction,
+    ]
+);
+
+impl_internal_actions!(editor, [Scroll, Select]);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
@@ -153,159 +215,6 @@ pub enum Direction {
 }
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings(vec![
-        Binding::new("escape", Cancel, Some("Editor")),
-        Binding::new("backspace", Backspace, Some("Editor")),
-        Binding::new("ctrl-h", Backspace, Some("Editor")),
-        Binding::new("delete", Delete, Some("Editor")),
-        Binding::new("ctrl-d", Delete, Some("Editor")),
-        Binding::new("enter", Newline, Some("Editor && mode == full")),
-        Binding::new(
-            "alt-enter",
-            Input("\n".into()),
-            Some("Editor && mode == auto_height"),
-        ),
-        Binding::new(
-            "enter",
-            ConfirmCompletion(None),
-            Some("Editor && showing_completions"),
-        ),
-        Binding::new(
-            "enter",
-            ConfirmCodeAction(None),
-            Some("Editor && showing_code_actions"),
-        ),
-        Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
-        Binding::new("tab", Tab(Direction::Next), Some("Editor")),
-        Binding::new("shift-tab", Tab(Direction::Prev), Some("Editor")),
-        Binding::new(
-            "tab",
-            ConfirmCompletion(None),
-            Some("Editor && showing_completions"),
-        ),
-        Binding::new("cmd-[", Outdent, Some("Editor")),
-        Binding::new("cmd-]", Indent, Some("Editor")),
-        Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
-        Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")),
-        Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")),
-        Binding::new(
-            "ctrl-alt-backspace",
-            DeleteToPreviousSubwordStart,
-            Some("Editor"),
-        ),
-        Binding::new("ctrl-alt-h", DeleteToPreviousSubwordStart, Some("Editor")),
-        Binding::new("alt-delete", DeleteToNextWordEnd, Some("Editor")),
-        Binding::new("alt-d", DeleteToNextWordEnd, Some("Editor")),
-        Binding::new("ctrl-alt-delete", DeleteToNextSubwordEnd, Some("Editor")),
-        Binding::new("ctrl-alt-d", DeleteToNextSubwordEnd, Some("Editor")),
-        Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")),
-        Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")),
-        Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")),
-        Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")),
-        Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")),
-        Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")),
-        Binding::new("cmd-x", Cut, Some("Editor")),
-        Binding::new("cmd-c", Copy, Some("Editor")),
-        Binding::new("cmd-v", Paste, Some("Editor")),
-        Binding::new("cmd-z", Undo, Some("Editor")),
-        Binding::new("cmd-shift-Z", Redo, Some("Editor")),
-        Binding::new("up", MoveUp, Some("Editor")),
-        Binding::new("down", MoveDown, Some("Editor")),
-        Binding::new("left", MoveLeft, Some("Editor")),
-        Binding::new("right", MoveRight, Some("Editor")),
-        Binding::new("ctrl-p", MoveUp, Some("Editor")),
-        Binding::new("ctrl-n", MoveDown, Some("Editor")),
-        Binding::new("ctrl-b", MoveLeft, Some("Editor")),
-        Binding::new("ctrl-f", MoveRight, Some("Editor")),
-        Binding::new("alt-left", MoveToPreviousWordStart, Some("Editor")),
-        Binding::new("alt-b", MoveToPreviousWordStart, Some("Editor")),
-        Binding::new("ctrl-alt-left", MoveToPreviousSubwordStart, Some("Editor")),
-        Binding::new("ctrl-alt-b", MoveToPreviousSubwordStart, Some("Editor")),
-        Binding::new("alt-right", MoveToNextWordEnd, Some("Editor")),
-        Binding::new("alt-f", MoveToNextWordEnd, Some("Editor")),
-        Binding::new("ctrl-alt-right", MoveToNextSubwordEnd, Some("Editor")),
-        Binding::new("ctrl-alt-f", MoveToNextSubwordEnd, Some("Editor")),
-        Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")),
-        Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")),
-        Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")),
-        Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")),
-        Binding::new("cmd-up", MoveToBeginning, Some("Editor")),
-        Binding::new("cmd-down", MoveToEnd, Some("Editor")),
-        Binding::new("shift-up", SelectUp, Some("Editor")),
-        Binding::new("ctrl-shift-P", SelectUp, Some("Editor")),
-        Binding::new("shift-down", SelectDown, Some("Editor")),
-        Binding::new("ctrl-shift-N", SelectDown, Some("Editor")),
-        Binding::new("shift-left", SelectLeft, Some("Editor")),
-        Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")),
-        Binding::new("shift-right", SelectRight, Some("Editor")),
-        Binding::new("ctrl-shift-F", SelectRight, Some("Editor")),
-        Binding::new("alt-shift-left", SelectToPreviousWordStart, Some("Editor")),
-        Binding::new("alt-shift-B", SelectToPreviousWordStart, Some("Editor")),
-        Binding::new(
-            "ctrl-alt-shift-left",
-            SelectToPreviousSubwordStart,
-            Some("Editor"),
-        ),
-        Binding::new(
-            "ctrl-alt-shift-B",
-            SelectToPreviousSubwordStart,
-            Some("Editor"),
-        ),
-        Binding::new("alt-shift-right", SelectToNextWordEnd, Some("Editor")),
-        Binding::new("alt-shift-F", SelectToNextWordEnd, Some("Editor")),
-        Binding::new(
-            "cmd-shift-left",
-            SelectToBeginningOfLine(true),
-            Some("Editor"),
-        ),
-        Binding::new(
-            "ctrl-alt-shift-right",
-            SelectToNextSubwordEnd,
-            Some("Editor"),
-        ),
-        Binding::new("ctrl-alt-shift-F", SelectToNextSubwordEnd, Some("Editor")),
-        Binding::new(
-            "ctrl-shift-A",
-            SelectToBeginningOfLine(true),
-            Some("Editor"),
-        ),
-        Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")),
-        Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")),
-        Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")),
-        Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")),
-        Binding::new("cmd-a", SelectAll, Some("Editor")),
-        Binding::new("cmd-l", SelectLine, Some("Editor")),
-        Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")),
-        Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")),
-        Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")),
-        Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")),
-        Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")),
-        Binding::new("cmd-d", SelectNext(false), Some("Editor")),
-        Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")),
-        Binding::new("cmd-/", ToggleComments, Some("Editor")),
-        Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")),
-        Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
-        Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
-        Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
-        Binding::new("cmd-u", UndoSelection, Some("Editor")),
-        Binding::new("cmd-shift-U", RedoSelection, Some("Editor")),
-        Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")),
-        Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")),
-        Binding::new("f2", Rename, Some("Editor")),
-        Binding::new("f12", GoToDefinition, Some("Editor")),
-        Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")),
-        Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
-        Binding::new("pageup", PageUp, Some("Editor")),
-        Binding::new("pagedown", PageDown, Some("Editor")),
-        Binding::new("alt-cmd-[", Fold, Some("Editor")),
-        Binding::new("alt-cmd-]", UnfoldLines, Some("Editor")),
-        Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
-        Binding::new("ctrl-space", ShowCompletions, Some("Editor")),
-        Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")),
-        Binding::new("alt-enter", OpenExcerpts, Some("Editor")),
-        Binding::new("cmd-f10", RestartLanguageServer, Some("Editor")),
-    ]);
-
     cx.add_action(Editor::open_new);
     cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
     cx.add_action(Editor::select);
@@ -315,6 +224,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::backspace);
     cx.add_action(Editor::delete);
     cx.add_action(Editor::tab);
+    cx.add_action(Editor::tab_prev);
     cx.add_action(Editor::indent);
     cx.add_action(Editor::outdent);
     cx.add_action(Editor::delete_line);
@@ -369,7 +279,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::move_to_enclosing_bracket);
     cx.add_action(Editor::undo_selection);
     cx.add_action(Editor::redo_selection);
-    cx.add_action(Editor::go_to_diagnostic);
+    cx.add_action(Editor::go_to_next_diagnostic);
+    cx.add_action(Editor::go_to_prev_diagnostic);
     cx.add_action(Editor::go_to_definition);
     cx.add_action(Editor::page_up);
     cx.add_action(Editor::page_down);
@@ -490,7 +401,7 @@ pub struct Editor {
     vertical_scroll_margin: f32,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
-    background_highlights: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
+    background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
@@ -767,7 +678,9 @@ impl CompletionsMenu {
                     )
                     .with_cursor_style(CursorStyle::PointingHand)
                     .on_mouse_down(move |cx| {
-                        cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
+                        cx.dispatch_action(ConfirmCompletion {
+                            item_ix: Some(item_ix),
+                        });
                     })
                     .boxed(),
                 );
@@ -893,7 +806,9 @@ impl CodeActionsMenu {
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
                         .on_mouse_down(move |cx| {
-                            cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
+                            cx.dispatch_action(ConfirmCodeAction {
+                                item_ix: Some(item_ix),
+                            });
                         })
                         .boxed(),
                     );
@@ -933,8 +848,13 @@ struct ClipboardSelection {
 }
 
 pub struct NavigationData {
-    anchor: Anchor,
-    offset: usize,
+    // Matching offsets for anchor and scroll_top_anchor allows us to recreate the anchor if the buffer
+    // has since been closed
+    cursor_anchor: Anchor,
+    cursor_offset: usize,
+    scroll_position: Vector2F,
+    scroll_top_anchor: Anchor,
+    scroll_top_offset: usize,
 }
 
 pub struct EditorCreated(pub ViewHandle<Editor>);
@@ -1008,7 +928,6 @@ impl Editor {
             let style = build_style(&*settings, get_field_editor_theme, None, cx);
             DisplayMap::new(
                 buffer.clone(),
-                settings.tab_size,
                 style.text.font_id,
                 style.text.font_size,
                 None,
@@ -1094,7 +1013,7 @@ impl Editor {
         if project.read(cx).is_remote() {
             cx.propagate_action();
         } else if let Some(buffer) = project
-            .update(cx, |project, cx| project.create_buffer(cx))
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
             .log_err()
         {
             workspace.add_item(
@@ -1130,8 +1049,12 @@ impl Editor {
         }
     }
 
-    pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
-        self.buffer.read(cx).language(cx)
+    pub fn language_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<&'a Arc<Language>> {
+        self.buffer.read(cx).language_at(point, cx)
     }
 
     fn style(&self, cx: &AppContext) -> EditorStyle {
@@ -1782,33 +1705,37 @@ impl Editor {
             return;
         }
 
-        if self.mode != EditorMode::Full {
-            cx.propagate_action();
-            return;
-        }
+        if self.mode == EditorMode::Full {
+            if self.active_diagnostics.is_some() {
+                self.dismiss_diagnostics(cx);
+                return;
+            }
 
-        if self.active_diagnostics.is_some() {
-            self.dismiss_diagnostics(cx);
-        } else if let Some(pending) = self.pending_selection.clone() {
-            let mut selections = self.selections.clone();
-            if selections.is_empty() {
-                selections = Arc::from([pending.selection]);
+            if let Some(pending) = self.pending_selection.clone() {
+                let mut selections = self.selections.clone();
+                if selections.is_empty() {
+                    selections = Arc::from([pending.selection]);
+                }
+                self.set_selections(selections, None, true, cx);
+                self.request_autoscroll(Autoscroll::Fit, cx);
+                return;
             }
-            self.set_selections(selections, None, true, cx);
-            self.request_autoscroll(Autoscroll::Fit, cx);
-        } else {
+
             let mut oldest_selection = self.oldest_selection::<usize>(&cx);
-            if self.selection_count() == 1 {
-                if oldest_selection.is_empty() {
-                    cx.propagate_action();
-                    return;
-                }
+            if self.selection_count() > 1 {
+                self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
+                return;
+            }
 
+            if !oldest_selection.is_empty() {
                 oldest_selection.start = oldest_selection.head().clone();
                 oldest_selection.end = oldest_selection.head().clone();
+                self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
+                return;
             }
-            self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
         }
+
+        cx.propagate_action();
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -2388,7 +2315,7 @@ impl Editor {
 
     pub fn confirm_completion(
         &mut self,
-        ConfirmCompletion(completion_ix): &ConfirmCompletion,
+        action: &ConfirmCompletion,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         use language::ToOffset as _;
@@ -2401,7 +2328,7 @@ impl Editor {
 
         let mat = completions_menu
             .matches
-            .get(completion_ix.unwrap_or(completions_menu.selected_item))?;
+            .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
         let buffer_handle = completions_menu.buffer;
         let completion = completions_menu.completions.get(mat.candidate_id)?;
 
@@ -2491,11 +2418,7 @@ impl Editor {
         }))
     }
 
-    pub fn toggle_code_actions(
-        &mut self,
-        &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions,
-        cx: &mut ViewContext<Self>,
-    ) {
+    pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
         if matches!(
             self.context_menu.as_ref(),
             Some(ContextMenu::CodeActions(_))
@@ -2505,6 +2428,7 @@ impl Editor {
             return;
         }
 
+        let deployed_from_indicator = action.deployed_from_indicator;
         let mut task = self.code_actions_task.take();
         cx.spawn_weak(|this, mut cx| async move {
             while let Some(prev_task) = task {
@@ -2539,7 +2463,7 @@ impl Editor {
 
     pub fn confirm_code_action(
         workspace: &mut Workspace,
-        ConfirmCodeAction(action_ix): &ConfirmCodeAction,
+        action: &ConfirmCodeAction,
         cx: &mut ViewContext<Workspace>,
     ) -> Option<Task<Result<()>>> {
         let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
@@ -2550,7 +2474,7 @@ impl Editor {
         } else {
             return None;
         };
-        let action_ix = action_ix.unwrap_or(actions_menu.selected_item);
+        let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
         let action = actions_menu.actions.get(action_ix)?.clone();
         let title = action.lsp_action.title.clone();
         let buffer = actions_menu.buffer;
@@ -2629,8 +2553,11 @@ impl Editor {
                 cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
             workspace.add_item(Box::new(editor.clone()), cx);
             editor.update(cx, |editor, cx| {
-                let color = editor.style(cx).highlighted_line_background;
-                editor.highlight_background::<Self>(ranges_to_highlight, color, cx);
+                editor.highlight_background::<Self>(
+                    ranges_to_highlight,
+                    |theme| theme.editor.highlighted_line_background,
+                    cx,
+                );
             });
         });
 
@@ -2697,9 +2624,6 @@ impl Editor {
                     }
 
                     let buffer_id = cursor_position.buffer_id;
-                    let style = this.style(cx);
-                    let read_background = style.document_highlight_read_background;
-                    let write_background = style.document_highlight_write_background;
                     let buffer = this.buffer.read(cx);
                     if !buffer
                         .text_anchor_for_position(cursor_position, cx)
@@ -2746,12 +2670,12 @@ impl Editor {
 
                     this.highlight_background::<DocumentHighlightRead>(
                         read_ranges,
-                        read_background,
+                        |theme| theme.editor.document_highlight_read_background,
                         cx,
                     );
                     this.highlight_background::<DocumentHighlightWrite>(
                         write_ranges,
-                        write_background,
+                        |theme| theme.editor.document_highlight_write_background,
                         cx,
                     );
                     cx.notify();
@@ -2777,7 +2701,9 @@ impl Editor {
                 .with_cursor_style(CursorStyle::PointingHand)
                 .with_padding(Padding::uniform(3.))
                 .on_mouse_down(|cx| {
-                    cx.dispatch_action(ToggleCodeActions(true));
+                    cx.dispatch_action(ToggleCodeActions {
+                        deployed_from_indicator: true,
+                    });
                 })
                 .boxed(),
             )
@@ -2871,8 +2797,8 @@ impl Editor {
         self.move_to_snippet_tabstop(Bias::Right, cx)
     }
 
-    pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) {
-        self.move_to_snippet_tabstop(Bias::Left, cx);
+    pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        self.move_to_snippet_tabstop(Bias::Left, cx)
     }
 
     pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> bool {
@@ -2945,8 +2871,9 @@ impl Editor {
                     .buffer_line_for_row(old_head.row)
                 {
                     let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
+                    let language_name = buffer.language().map(|language| language.name());
+                    let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
                     if old_head.column <= indent_column && old_head.column > 0 {
-                        let indent = buffer.indent_size();
                         new_head = cmp::min(
                             new_head,
                             Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
@@ -2976,60 +2903,58 @@ impl Editor {
         });
     }
 
-    pub fn tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext<Self>) {
-        match direction {
-            Direction::Prev => {
-                if !self.snippet_stack.is_empty() {
-                    self.move_to_prev_snippet_tabstop(cx);
-                    return;
-                }
+    pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
+        if self.move_to_prev_snippet_tabstop(cx) {
+            return;
+        }
 
-                self.outdent(&Outdent, cx);
-            }
-            Direction::Next => {
-                if self.move_to_next_snippet_tabstop(cx) {
-                    return;
-                }
+        self.outdent(&Outdent, cx);
+    }
 
-                let tab_size = cx.global::<Settings>().tab_size;
-                let mut selections = self.local_selections::<Point>(cx);
-                if selections.iter().all(|s| s.is_empty()) {
-                    self.transact(cx, |this, cx| {
-                        this.buffer.update(cx, |buffer, cx| {
-                            for selection in &mut selections {
-                                let char_column = buffer
-                                    .read(cx)
-                                    .text_for_range(
-                                        Point::new(selection.start.row, 0)..selection.start,
-                                    )
-                                    .flat_map(str::chars)
-                                    .count();
-                                let chars_to_next_tab_stop = tab_size - (char_column % tab_size);
-                                buffer.edit(
-                                    [selection.start..selection.start],
-                                    " ".repeat(chars_to_next_tab_stop),
-                                    cx,
-                                );
-                                selection.start.column += chars_to_next_tab_stop as u32;
-                                selection.end = selection.start;
-                            }
-                        });
-                        this.update_selections(selections, Some(Autoscroll::Fit), cx);
-                    });
-                } else {
-                    self.indent(&Indent, cx);
-                }
-            }
+    pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+        if self.move_to_next_snippet_tabstop(cx) {
+            return;
+        }
+
+        let mut selections = self.local_selections::<Point>(cx);
+        if selections.iter().all(|s| s.is_empty()) {
+            self.transact(cx, |this, cx| {
+                this.buffer.update(cx, |buffer, cx| {
+                    for selection in &mut selections {
+                        let language_name =
+                            buffer.language_at(selection.start, cx).map(|l| l.name());
+                        let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
+                        let char_column = buffer
+                            .read(cx)
+                            .text_for_range(Point::new(selection.start.row, 0)..selection.start)
+                            .flat_map(str::chars)
+                            .count();
+                        let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
+                        buffer.edit(
+                            [selection.start..selection.start],
+                            " ".repeat(chars_to_next_tab_stop as usize),
+                            cx,
+                        );
+                        selection.start.column += chars_to_next_tab_stop;
+                        selection.end = selection.start;
+                    }
+                });
+                this.update_selections(selections, Some(Autoscroll::Fit), cx);
+            });
+        } else {
+            self.indent(&Indent, cx);
         }
     }
 
     pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
-        let tab_size = cx.global::<Settings>().tab_size;
         let mut selections = self.local_selections::<Point>(cx);
         self.transact(cx, |this, cx| {
             let mut last_indent = None;
             this.buffer.update(cx, |buffer, cx| {
+                let snapshot = buffer.snapshot(cx);
                 for selection in &mut selections {
+                    let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
+                    let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
                     let mut start_row = selection.start.row;
                     let mut end_row = selection.end.row + 1;
 
@@ -3053,12 +2978,12 @@ impl Editor {
                     }
 
                     for row in start_row..end_row {
-                        let indent_column = buffer.read(cx).indent_column_for_line(row) as usize;
+                        let indent_column = snapshot.indent_column_for_line(row);
                         let columns_to_next_tab_stop = tab_size - (indent_column % tab_size);
                         let row_start = Point::new(row, 0);
                         buffer.edit(
                             [row_start..row_start],
-                            " ".repeat(columns_to_next_tab_stop),
+                            " ".repeat(columns_to_next_tab_stop as usize),
                             cx,
                         );
 
@@ -3080,14 +3005,16 @@ impl Editor {
     }
 
     pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) {
-        let tab_size = cx.global::<Settings>().tab_size;
         let selections = self.local_selections::<Point>(cx);
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let mut deletion_ranges = Vec::new();
         let mut last_outdent = None;
         {
-            let buffer = self.buffer.read(cx).read(cx);
+            let buffer = self.buffer.read(cx);
+            let snapshot = buffer.snapshot(cx);
             for selection in &selections {
+                let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
+                let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
                 let mut rows = selection.spanned_rows(false, &display_map);
 
                 // Avoid re-outdenting a row that has already been outdented by a
@@ -3099,11 +3026,11 @@ impl Editor {
                 }
 
                 for row in rows {
-                    let column = buffer.indent_column_for_line(row) as usize;
+                    let column = snapshot.indent_column_for_line(row);
                     if column > 0 {
-                        let mut deletion_len = (column % tab_size) as u32;
+                        let mut deletion_len = column % tab_size;
                         if deletion_len == 0 {
-                            deletion_len = tab_size as u32;
+                            deletion_len = tab_size;
                         }
                         deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len));
                         last_outdent = Some(row);
@@ -3847,12 +3774,12 @@ impl Editor {
 
     pub fn select_to_beginning_of_line(
         &mut self,
-        SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine,
+        action: &SelectToBeginningOfLine,
         cx: &mut ViewContext<Self>,
     ) {
         self.move_selection_heads(cx, |map, head, _| {
             (
-                movement::line_beginning(map, head, *stop_at_soft_boundaries),
+                movement::line_beginning(map, head, action.stop_at_soft_wraps),
                 SelectionGoal::None,
             )
         });
@@ -3864,7 +3791,12 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            this.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx);
+            this.select_to_beginning_of_line(
+                &SelectToBeginningOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
             this.backspace(&Backspace, cx);
         });
     }
@@ -3877,12 +3809,12 @@ impl Editor {
 
     pub fn select_to_end_of_line(
         &mut self,
-        SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine,
+        action: &SelectToEndOfLine,
         cx: &mut ViewContext<Self>,
     ) {
         self.move_selection_heads(cx, |map, head, _| {
             (
-                movement::line_end(map, head, *stop_at_soft_boundaries),
+                movement::line_end(map, head, action.stop_at_soft_wraps),
                 SelectionGoal::None,
             )
         });
@@ -3890,14 +3822,24 @@ impl Editor {
 
     pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
-            this.select_to_end_of_line(&SelectToEndOfLine(false), cx);
+            this.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
             this.delete(&Delete, cx);
         });
     }
 
     pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
-            this.select_to_end_of_line(&SelectToEndOfLine(false), cx);
+            this.select_to_end_of_line(
+                &SelectToEndOfLine {
+                    stop_at_soft_wraps: false,
+                },
+                cx,
+            );
             this.cut(&Cut, cx);
         });
     }
@@ -3959,6 +3901,7 @@ impl Editor {
             let buffer = self.buffer.read(cx).read(cx);
             let offset = position.to_offset(&buffer);
             let point = position.to_point(&buffer);
+            let scroll_top_offset = self.scroll_top_anchor.to_offset(&buffer);
             drop(buffer);
 
             if let Some(new_position) = new_position {
@@ -3969,8 +3912,11 @@ impl Editor {
             }
 
             nav_history.push(Some(NavigationData {
-                anchor: position,
-                offset,
+                cursor_anchor: position,
+                cursor_offset: offset,
+                scroll_position: self.scroll_position,
+                scroll_top_anchor: self.scroll_top_anchor.clone(),
+                scroll_top_offset,
             }));
         }
     }
@@ -4144,7 +4090,6 @@ impl Editor {
 
     pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) {
         self.push_to_selection_history();
-        let replace_newest = action.0;
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
         let mut selections = self.local_selections::<usize>(cx);
@@ -4183,7 +4128,7 @@ impl Editor {
                 }
 
                 if let Some(next_selected_range) = next_selected_range {
-                    if replace_newest {
+                    if action.replace_newest {
                         if let Some(newest_id) =
                             selections.iter().max_by_key(|s| s.id).map(|s| s.id)
                         {
@@ -4243,24 +4188,26 @@ impl Editor {
     }
 
     pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
-        // Get the line comment prefix. Split its trailing whitespace into a separate string,
-        // as that portion won't be used for detecting if a line is a comment.
-        let full_comment_prefix =
-            if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) {
-                prefix.to_string()
-            } else {
-                return;
-            };
-        let comment_prefix = full_comment_prefix.trim_end_matches(' ');
-        let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
-
         self.transact(cx, |this, cx| {
             let mut selections = this.local_selections::<Point>(cx);
             let mut all_selection_lines_are_comments = true;
             let mut edit_ranges = Vec::new();
             let mut last_toggled_row = None;
             this.buffer.update(cx, |buffer, cx| {
+                // TODO: Handle selections that cross excerpts
                 for selection in &mut selections {
+                    // Get the line comment prefix. Split its trailing whitespace into a separate string,
+                    // as that portion won't be used for detecting if a line is a comment.
+                    let full_comment_prefix = if let Some(prefix) = buffer
+                        .language_at(selection.start, cx)
+                        .and_then(|l| l.line_comment_prefix())
+                    {
+                        prefix.to_string()
+                    } else {
+                        return;
+                    };
+                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
                     edit_ranges.clear();
                     let snapshot = buffer.snapshot(cx);
 

crates/editor/src/element.rs 🔗

@@ -22,6 +22,7 @@ use gpui::{
 };
 use json::json;
 use language::{Bias, DiagnosticSeverity};
+use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
@@ -917,9 +918,11 @@ impl Element for EditorElement {
             let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
 
             highlighted_rows = view.highlighted_rows();
+            let theme = cx.global::<Settings>().theme.as_ref();
             highlighted_ranges = view.background_highlights_in_range(
                 start_anchor.clone()..end_anchor.clone(),
                 &display_map,
+                theme,
             );
 
             let mut remote_selections = HashMap::default();
@@ -1147,6 +1150,7 @@ impl Element for EditorElement {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         layout: &mut LayoutState,
         paint: &mut PaintState,
         cx: &mut EventContext,
@@ -1494,8 +1498,8 @@ mod tests {
         display_map::{BlockDisposition, BlockProperties},
         Editor, MultiBuffer,
     };
+    use settings::Settings;
     use util::test::sample_text;
-    use workspace::Settings;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {

crates/editor/src/items.rs 🔗

@@ -8,12 +8,11 @@ use gpui::{
 use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
 use project::{File, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
+use settings::Settings;
 use std::{fmt::Write, path::PathBuf, time::Duration};
 use text::{Point, Selection};
 use util::TryFutureExt;
-use workspace::{
-    FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
-};
+use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
@@ -248,18 +247,27 @@ impl Item for Editor {
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         if let Some(data) = data.downcast_ref::<NavigationData>() {
             let buffer = self.buffer.read(cx).read(cx);
-            let offset = if buffer.can_resolve(&data.anchor) {
-                data.anchor.to_offset(&buffer)
+            let offset = if buffer.can_resolve(&data.cursor_anchor) {
+                data.cursor_anchor.to_offset(&buffer)
             } else {
-                buffer.clip_offset(data.offset, Bias::Left)
+                buffer.clip_offset(data.cursor_offset, Bias::Left)
             };
             let newest_selection = self.newest_selection_with_snapshot::<usize>(&buffer);
+
+            let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
+                data.scroll_top_anchor.clone()
+            } else {
+                buffer.anchor_at(data.scroll_top_offset, Bias::Left)
+            };
+
             drop(buffer);
 
             if newest_selection.head() == offset {
                 false
             } else {
                 let nav_history = self.nav_history.take();
+                self.scroll_position = data.scroll_position;
+                self.scroll_top_anchor = scroll_top_anchor;
                 self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
                 self.nav_history = nav_history;
                 true

crates/editor/src/movement.rs 🔗

@@ -268,9 +268,11 @@ mod tests {
     use super::*;
     use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
     use language::Point;
+    use settings::Settings;
 
     #[gpui::test]
     fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -297,6 +299,7 @@ mod tests {
 
     #[gpui::test]
     fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -330,6 +333,7 @@ mod tests {
 
     #[gpui::test]
     fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(
             marked_text: &str,
             cx: &mut gpui::MutableAppContext,
@@ -361,6 +365,7 @@ mod tests {
 
     #[gpui::test]
     fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -384,6 +389,7 @@ mod tests {
 
     #[gpui::test]
     fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -416,6 +422,7 @@ mod tests {
 
     #[gpui::test]
     fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(
             marked_text: &str,
             cx: &mut gpui::MutableAppContext,
@@ -447,6 +454,7 @@ mod tests {
 
     #[gpui::test]
     fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -467,6 +475,7 @@ 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 font_id = cx
             .font_cache()
@@ -487,7 +496,7 @@ mod tests {
             multibuffer
         });
         let display_map =
-            cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
+            cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 
         assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");

crates/editor/src/multi_buffer.rs 🔗

@@ -11,6 +11,7 @@ use language::{
     Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _,
     ToPointUtf16 as _, TransactionId,
 };
+use settings::Settings;
 use std::{
     cell::{Ref, RefCell},
     cmp, fmt, io,
@@ -297,8 +298,10 @@ impl MultiBuffer {
                 .into_iter()
                 .map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot));
             return buffer.update(cx, |buffer, cx| {
+                let language_name = buffer.language().map(|language| language.name());
+                let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
                 if autoindent {
-                    buffer.edit_with_autoindent(ranges, new_text, cx);
+                    buffer.edit_with_autoindent(ranges, new_text, indent_size, cx);
                 } else {
                     buffer.edit(ranges, new_text, cx);
                 }
@@ -392,10 +395,12 @@ impl MultiBuffer {
                             );
                         }
                     }
+                    let language_name = buffer.language().map(|l| l.name());
+                    let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
 
                     if autoindent {
-                        buffer.edit_with_autoindent(deletions, "", cx);
-                        buffer.edit_with_autoindent(insertions, new_text.clone(), cx);
+                        buffer.edit_with_autoindent(deletions, "", indent_size, cx);
+                        buffer.edit_with_autoindent(insertions, new_text.clone(), indent_size, cx);
                     } else {
                         buffer.edit(deletions, "", cx);
                         buffer.edit(insertions, new_text.clone(), cx);
@@ -783,7 +788,7 @@ impl MultiBuffer {
             old: edit_start..edit_start,
             new: edit_start..edit_end,
         }]);
-
+        cx.emit(Event::Edited);
         cx.notify();
         ids
     }
@@ -797,10 +802,12 @@ impl MultiBuffer {
         snapshot.trailing_excerpt_update_count += 1;
         snapshot.is_dirty = false;
         snapshot.has_conflict = false;
+
         self.subscriptions.publish_mut([Edit {
             old: 0..prev_len,
             new: 0..0,
         }]);
+        cx.emit(Event::Edited);
         cx.notify();
     }
 
@@ -861,6 +868,29 @@ impl MultiBuffer {
         })
     }
 
+    // If point is at the end of the buffer, the last excerpt is returned
+    pub fn point_to_buffer_offset<'a, T: ToOffset>(
+        &'a self,
+        point: T,
+        cx: &AppContext,
+    ) -> Option<(ModelHandle<Buffer>, usize)> {
+        let snapshot = self.read(cx);
+        let offset = point.to_offset(&snapshot);
+        let mut cursor = snapshot.excerpts.cursor::<usize>();
+        cursor.seek(&offset, Bias::Right, &());
+        if cursor.item().is_none() {
+            cursor.prev(&());
+        }
+
+        cursor.item().map(|excerpt| {
+            let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+            let buffer_point = excerpt_start + offset - *cursor.start();
+            let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
+
+            (buffer, buffer_point)
+        })
+    }
+
     pub fn range_to_buffer_ranges<'a, T: ToOffset>(
         &'a self,
         range: Range<T>,
@@ -965,6 +995,7 @@ impl MultiBuffer {
         }
 
         self.subscriptions.publish_mut(edits);
+        cx.emit(Event::Edited);
         cx.notify();
     }
 
@@ -1057,12 +1088,13 @@ impl MultiBuffer {
             .unwrap_or(false)
     }
 
-    pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
-        self.buffers
-            .borrow()
-            .values()
-            .next()
-            .and_then(|state| state.buffer.read(cx).language())
+    pub fn language_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<&'a Arc<Language>> {
+        self.point_to_buffer_offset(point, cx)
+            .and_then(|(buffer, _)| buffer.read(cx).language())
     }
 
     pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> {
@@ -2899,7 +2931,7 @@ mod tests {
     use gpui::MutableAppContext;
     use language::{Buffer, Rope};
     use rand::prelude::*;
-    use std::env;
+    use std::{env, rc::Rc};
     use text::{Point, RandomCharIter};
     use util::test::sample_text;
 
@@ -2956,6 +2988,15 @@ mod tests {
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
 
+        let events = Rc::new(RefCell::new(Vec::<Event>::new()));
+        multibuffer.update(cx, |_, cx| {
+            let events = events.clone();
+            cx.subscribe(&multibuffer, move |_, _, event, _| {
+                events.borrow_mut().push(event.clone())
+            })
+            .detach();
+        });
+
         let subscription = multibuffer.update(cx, |multibuffer, cx| {
             let subscription = multibuffer.subscribe();
             multibuffer.push_excerpts(buffer_1.clone(), [Point::new(1, 2)..Point::new(2, 5)], cx);
@@ -2980,6 +3021,12 @@ mod tests {
             subscription
         });
 
+        // Adding excerpts emits an edited event.
+        assert_eq!(
+            events.borrow().as_slice(),
+            &[Event::Edited, Event::Edited, Event::Edited]
+        );
+
         let snapshot = multibuffer.read(cx).snapshot(cx);
         assert_eq!(
             snapshot.text(),
@@ -3760,6 +3807,7 @@ mod tests {
 
     #[gpui::test]
     fn test_history(cx: &mut MutableAppContext) {
+        cx.set_global(Settings::test(cx));
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

crates/editor/src/test.rs 🔗

@@ -1,8 +1,9 @@
-use util::test::marked_text;
+use gpui::ViewContext;
+use util::test::{marked_text, marked_text_ranges};
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
-    DisplayPoint, MultiBuffer,
+    DisplayPoint, Editor, MultiBuffer,
 };
 
 #[cfg(test)]
@@ -20,7 +21,6 @@ pub fn marked_display_snapshot(
 ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
     let (unmarked_text, markers) = marked_text(text);
 
-    let tab_size = 4;
     let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
     let font_id = cx
         .font_cache()
@@ -30,7 +30,7 @@ pub fn marked_display_snapshot(
 
     let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
     let display_map =
-        cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+        cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
     let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
     let markers = markers
         .into_iter()
@@ -39,3 +39,20 @@ pub fn marked_display_snapshot(
 
     (snapshot, markers)
 }
+
+pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
+    let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
+    assert_eq!(editor.text(cx), umarked_text);
+    editor.select_ranges(text_ranges, None, cx);
+}
+
+pub fn assert_text_with_selections(
+    editor: &mut Editor,
+    marked_text: &str,
+    cx: &mut ViewContext<Editor>,
+) {
+    let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
+
+    assert_eq!(editor.text(cx), unmarked_text);
+    assert_eq!(editor.selected_ranges(cx), text_ranges);
+}

crates/file_finder/Cargo.toml 🔗

@@ -11,7 +11,9 @@ doctest = false
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+picker = { path = "../picker" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }

crates/file_finder/src/file_finder.rs 🔗

@@ -1,15 +1,12 @@
-use editor::Editor;
 use fuzzy::PathMatch;
 use gpui::{
-    action,
-    elements::*,
-    keymap::{self, Binding},
-    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
+    View, ViewContext, ViewHandle,
 };
+use picker::{Picker, PickerDelegate};
 use project::{Project, ProjectPath, WorktreeId};
+use settings::Settings;
 use std::{
-    cmp,
     path::Path,
     sync::{
         atomic::{self, AtomicBool},
@@ -17,15 +14,11 @@ use std::{
     },
 };
 use util::post_inc;
-use workspace::{
-    menu::{Confirm, SelectNext, SelectPrev},
-    Settings, Workspace,
-};
+use workspace::Workspace;
 
 pub struct FileFinder {
-    handle: WeakViewHandle<Self>,
     project: ModelHandle<Project>,
-    query_editor: ViewHandle<Editor>,
+    picker: ViewHandle<Picker<Self>>,
     search_count: usize,
     latest_search_id: usize,
     latest_search_did_cancel: bool,
@@ -33,23 +26,13 @@ pub struct FileFinder {
     matches: Vec<PathMatch>,
     selected: Option<(usize, Arc<Path>)>,
     cancel_flag: Arc<AtomicBool>,
-    list_state: UniformListState,
 }
 
-action!(Toggle);
-action!(Select, ProjectPath);
+actions!(file_finder, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(FileFinder::toggle);
-    cx.add_action(FileFinder::confirm);
-    cx.add_action(FileFinder::select);
-    cx.add_action(FileFinder::select_prev);
-    cx.add_action(FileFinder::select_next);
-
-    cx.add_bindings(vec![
-        Binding::new("cmd-p", Toggle, None),
-        Binding::new("escape", Toggle, Some("FileFinder")),
-    ]);
+    Picker::<FileFinder>::init(cx);
 }
 
 pub enum Event {
@@ -66,140 +49,16 @@ impl View for FileFinder {
         "FileFinder"
     }
 
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        Align::new(
-            ConstrainedBox::new(
-                Container::new(
-                    Flex::new(Axis::Vertical)
-                        .with_child(
-                            ChildView::new(&self.query_editor)
-                                .contained()
-                                .with_style(settings.theme.selector.input_editor.container)
-                                .boxed(),
-                        )
-                        .with_child(
-                            FlexItem::new(self.render_matches(cx))
-                                .flex(1., false)
-                                .boxed(),
-                        )
-                        .boxed(),
-                )
-                .with_style(settings.theme.selector.container)
-                .boxed(),
-            )
-            .with_max_width(500.0)
-            .with_max_height(420.0)
-            .boxed(),
-        )
-        .top()
-        .named("file finder")
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.picker.clone()).boxed()
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.focus(&self.query_editor);
-    }
-
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
-        let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
-        cx
+        cx.focus(&self.picker);
     }
 }
 
 impl FileFinder {
-    fn render_matches(&self, cx: &AppContext) -> ElementBox {
-        if self.matches.is_empty() {
-            let settings = cx.global::<Settings>();
-            return Container::new(
-                Label::new(
-                    "No matches".into(),
-                    settings.theme.selector.empty.label.clone(),
-                )
-                .boxed(),
-            )
-            .with_style(settings.theme.selector.empty.container)
-            .named("empty matches");
-        }
-
-        let handle = self.handle.clone();
-        let list =
-            UniformList::new(
-                self.list_state.clone(),
-                self.matches.len(),
-                move |mut range, items, cx| {
-                    let cx = cx.as_ref();
-                    let finder = handle.upgrade(cx).unwrap();
-                    let finder = finder.read(cx);
-                    let start = range.start;
-                    range.end = cmp::min(range.end, finder.matches.len());
-                    items.extend(finder.matches[range].iter().enumerate().map(
-                        move |(i, path_match)| finder.render_match(path_match, start + i, cx),
-                    ));
-                },
-            );
-
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
-    }
-
-    fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
-        let selected_index = self.selected_index();
-        let settings = cx.global::<Settings>();
-        let style = if index == selected_index {
-            &settings.theme.selector.active_item
-        } else {
-            &settings.theme.selector.item
-        };
-        let (file_name, file_name_positions, full_path, full_path_positions) =
-            self.labels_for_match(path_match);
-        let container = Container::new(
-            Flex::row()
-                // .with_child(
-                //     Container::new(
-                //         LineBox::new(
-                //             Svg::new("icons/file-16.svg")
-                //                 .with_color(style.label.text.color)
-                //                 .boxed(),
-                //             style.label.text.clone(),
-                //         )
-                //         .boxed(),
-                //     )
-                //     .with_padding_right(6.0)
-                //     .boxed(),
-                // )
-                .with_child(
-                    Flex::column()
-                        .with_child(
-                            Label::new(file_name.to_string(), style.label.clone())
-                                .with_highlights(file_name_positions)
-                                .boxed(),
-                        )
-                        .with_child(
-                            Label::new(full_path, style.label.clone())
-                                .with_highlights(full_path_positions)
-                                .boxed(),
-                        )
-                        .flex(1., false)
-                        .boxed(),
-                )
-                .boxed(),
-        )
-        .with_style(style.container);
-
-        let action = Select(ProjectPath {
-            worktree_id: WorktreeId::from_usize(path_match.worktree_id),
-            path: path_match.path.clone(),
-        });
-        EventHandler::new(container.boxed())
-            .on_mouse_down(move |cx| {
-                cx.dispatch_action(action.clone());
-                true
-            })
-            .named("match")
-    }
-
     fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
         let path_string = path_match.path.to_string_lossy();
         let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
@@ -254,18 +113,11 @@ impl FileFinder {
     }
 
     pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        let handle = cx.weak_handle();
         cx.observe(&project, Self::project_updated).detach();
-
-        let query_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
-        });
-        cx.subscribe(&query_editor, Self::on_query_editor_event)
-            .detach();
-
         Self {
-            handle: cx.weak_handle(),
             project,
-            query_editor,
+            picker: cx.add_view(|cx| Picker::new(handle, cx)),
             search_count: 0,
             latest_search_id: 0,
             latest_search_did_cancel: false,
@@ -273,40 +125,60 @@ impl FileFinder {
             matches: Vec::new(),
             selected: None,
             cancel_flag: Arc::new(AtomicBool::new(false)),
-            list_state: Default::default(),
         }
     }
 
     fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
-        let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
-        if let Some(task) = self.spawn_search(query, cx) {
-            task.detach();
-        }
+        self.spawn_search(self.picker.read(cx).query(cx), cx)
+            .detach();
+    }
+
+    fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        let search_id = util::post_inc(&mut self.search_count);
+        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+        self.cancel_flag = Arc::new(AtomicBool::new(false));
+        let cancel_flag = self.cancel_flag.clone();
+        let project = self.project.clone();
+        cx.spawn(|this, mut cx| async move {
+            let matches = project
+                .read_with(&cx, |project, cx| {
+                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
+                })
+                .await;
+            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+            this.update(&mut cx, |this, cx| {
+                this.set_matches(search_id, did_cancel, query, matches, cx)
+            });
+        })
     }
 
-    fn on_query_editor_event(
+    fn set_matches(
         &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
+        search_id: usize,
+        did_cancel: bool,
+        query: String,
+        matches: Vec<PathMatch>,
         cx: &mut ViewContext<Self>,
     ) {
-        match event {
-            editor::Event::BufferEdited { .. } => {
-                let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
-                if query.is_empty() {
-                    self.latest_search_id = post_inc(&mut self.search_count);
-                    self.matches.clear();
-                    cx.notify();
-                } else {
-                    if let Some(task) = self.spawn_search(query, cx) {
-                        task.detach();
-                    }
-                }
+        if search_id >= self.latest_search_id {
+            self.latest_search_id = search_id;
+            if self.latest_search_did_cancel && query == self.latest_search_query {
+                util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
+            } else {
+                self.matches = matches;
             }
-            editor::Event::Blurred => cx.emit(Event::Dismissed),
-            _ => {}
+            self.latest_search_query = query;
+            self.latest_search_did_cancel = did_cancel;
+            cx.notify();
+            self.picker.update(cx, |_, cx| cx.notify());
         }
     }
+}
+
+impl PickerDelegate for FileFinder {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
 
     fn selected_index(&self) -> usize {
         if let Some(selected) = self.selected.as_ref() {
@@ -321,31 +193,24 @@ impl FileFinder {
         0
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        let mut selected_index = self.selected_index();
-        if selected_index > 0 {
-            selected_index -= 1;
-            let mat = &self.matches[selected_index];
-            self.selected = Some((mat.worktree_id, mat.path.clone()));
-        }
-        self.list_state
-            .scroll_to(ScrollTarget::Show(selected_index));
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        let mat = &self.matches[ix];
+        self.selected = Some((mat.worktree_id, mat.path.clone()));
         cx.notify();
     }
 
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        let mut selected_index = self.selected_index();
-        if selected_index + 1 < self.matches.len() {
-            selected_index += 1;
-            let mat = &self.matches[selected_index];
-            self.selected = Some((mat.worktree_id, mat.path.clone()));
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        if query.is_empty() {
+            self.latest_search_id = post_inc(&mut self.search_count);
+            self.matches.clear();
+            cx.notify();
+            Task::ready(())
+        } else {
+            self.spawn_search(query, cx)
         }
-        self.list_state
-            .scroll_to(ScrollTarget::Show(selected_index));
-        cx.notify();
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(m) = self.matches.get(self.selected_index()) {
             cx.emit(Event::Selected(ProjectPath {
                 worktree_id: WorktreeId::from_usize(m.worktree_id),
@@ -354,57 +219,45 @@ impl FileFinder {
         }
     }
 
-    fn select(&mut self, Select(project_path): &Select, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Selected(project_path.clone()));
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
     }
 
-    #[must_use]
-    fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Option<Task<()>> {
-        let search_id = util::post_inc(&mut self.search_count);
-        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
-        self.cancel_flag = Arc::new(AtomicBool::new(false));
-        let cancel_flag = self.cancel_flag.clone();
-        let project = self.project.clone();
-        Some(cx.spawn(|this, mut cx| async move {
-            let matches = project
-                .read_with(&cx, |project, cx| {
-                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
-                })
-                .await;
-            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
-            this.update(&mut cx, |this, cx| {
-                this.update_matches((search_id, did_cancel, query, matches), cx)
-            });
-        }))
-    }
-
-    fn update_matches(
-        &mut self,
-        (search_id, did_cancel, query, matches): (usize, bool, String, Vec<PathMatch>),
-        cx: &mut ViewContext<Self>,
-    ) {
-        if search_id >= self.latest_search_id {
-            self.latest_search_id = search_id;
-            if self.latest_search_did_cancel && query == self.latest_search_query {
-                util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
-            } else {
-                self.matches = matches;
-            }
-            self.latest_search_query = query;
-            self.latest_search_did_cancel = did_cancel;
-            self.list_state
-                .scroll_to(ScrollTarget::Show(self.selected_index()));
-            cx.notify();
-        }
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+        let path_match = &self.matches[ix];
+        let settings = cx.global::<Settings>();
+        let style = if selected {
+            &settings.theme.selector.active_item
+        } else {
+            &settings.theme.selector.item
+        };
+        let (file_name, file_name_positions, full_path, full_path_positions) =
+            self.labels_for_match(path_match);
+        Flex::column()
+            .with_child(
+                Label::new(file_name.to_string(), style.label.clone())
+                    .with_highlights(file_name_positions)
+                    .boxed(),
+            )
+            .with_child(
+                Label::new(full_path, style.label.clone())
+                    .with_highlights(full_path_positions)
+                    .boxed(),
+            )
+            .flex(1., false)
+            .contained()
+            .with_style(style.container)
+            .named("match")
     }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::Input;
+    use editor::{Editor, Input};
     use serde_json::json;
     use std::path::PathBuf;
+    use workspace::menu::{Confirm, SelectNext};
     use workspace::{Workspace, WorkspaceParams};
 
     #[ctor::ctor]
@@ -446,7 +299,7 @@ mod tests {
             .unwrap();
         cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
             .await;
-        cx.dispatch_action(window_id, vec![workspace.id()], Toggle);
+        cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| {
             workspace
@@ -457,19 +310,16 @@ mod tests {
                 .downcast::<FileFinder>()
                 .unwrap()
         });
-        let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone());
-
-        let chain = vec![finder.id(), query_buffer.id()];
-        cx.dispatch_action(window_id, chain.clone(), Input("b".into()));
-        cx.dispatch_action(window_id, chain.clone(), Input("n".into()));
-        cx.dispatch_action(window_id, chain.clone(), Input("a".into()));
+        cx.dispatch_action(window_id, Input("b".into()));
+        cx.dispatch_action(window_id, Input("n".into()));
+        cx.dispatch_action(window_id, Input("a".into()));
         finder
             .condition(&cx, |finder, _| finder.matches.len() == 2)
             .await;
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-        cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], SelectNext);
-        cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], Confirm);
+        cx.dispatch_action(window_id, SelectNext);
+        cx.dispatch_action(window_id, Confirm);
         active_pane
             .condition(&cx, |pane, _| pane.active_item().is_some())
             .await;
@@ -521,7 +371,6 @@ mod tests {
         let query = "hi".to_string();
         finder
             .update(cx, |f, cx| f.spawn_search(query.clone(), cx))
-            .unwrap()
             .await;
         finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5));
 
@@ -530,26 +379,22 @@ mod tests {
 
             // Simulate a search being cancelled after the time limit,
             // returning only a subset of the matches that would have been found.
-            finder.spawn_search(query.clone(), cx).unwrap().detach();
-            finder.update_matches(
-                (
-                    finder.latest_search_id,
-                    true, // did-cancel
-                    query.clone(),
-                    vec![matches[1].clone(), matches[3].clone()],
-                ),
+            drop(finder.spawn_search(query.clone(), cx));
+            finder.set_matches(
+                finder.latest_search_id,
+                true, // did-cancel
+                query.clone(),
+                vec![matches[1].clone(), matches[3].clone()],
                 cx,
             );
 
             // Simulate another cancellation.
-            finder.spawn_search(query.clone(), cx).unwrap().detach();
-            finder.update_matches(
-                (
-                    finder.latest_search_id,
-                    true, // did-cancel
-                    query.clone(),
-                    vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
-                ),
+            drop(finder.spawn_search(query.clone(), cx));
+            finder.set_matches(
+                finder.latest_search_id,
+                true, // did-cancel
+                query.clone(),
+                vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
                 cx,
             );
 
@@ -583,7 +428,6 @@ mod tests {
         // is included in the matching, because the worktree is a single file.
         finder
             .update(cx, |f, cx| f.spawn_search("thf".into(), cx))
-            .unwrap()
             .await;
         cx.read(|cx| {
             let finder = finder.read(cx);
@@ -601,7 +445,6 @@ mod tests {
         // not match anything.
         finder
             .update(cx, |f, cx| f.spawn_search("thf/".into(), cx))
-            .unwrap()
             .await;
         finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
     }
@@ -640,16 +483,15 @@ mod tests {
         // Run a search that matches two files with the same relative path.
         finder
             .update(cx, |f, cx| f.spawn_search("a.t".into(), cx))
-            .unwrap()
             .await;
 
         // Can switch between different matches with the same relative path.
         finder.update(cx, |f, cx| {
             assert_eq!(f.matches.len(), 2);
             assert_eq!(f.selected_index(), 0);
-            f.select_next(&SelectNext, cx);
+            f.set_selected_index(1, cx);
             assert_eq!(f.selected_index(), 1);
-            f.select_prev(&SelectPrev, cx);
+            f.set_selected_index(0, cx);
             assert_eq!(f.selected_index(), 0);
         });
     }

crates/go_to_line/Cargo.toml 🔗

@@ -11,5 +11,6 @@ doctest = false
 text = { path = "../text" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 postage = { version = "0.4", features = ["futures-traits"] }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -1,20 +1,15 @@
 use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
 use gpui::{
-    action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
-    MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+    actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext,
+    RenderContext, View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 use text::{Bias, Point};
-use workspace::{Settings, Workspace};
+use workspace::Workspace;
 
-action!(Toggle);
-action!(Confirm);
+actions!(go_to_line, [Toggle, Confirm]);
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([
-        Binding::new("ctrl-g", Toggle, Some("Editor")),
-        Binding::new("escape", Toggle, Some("GoToLine")),
-        Binding::new("enter", Confirm, Some("GoToLine")),
-    ]);
     cx.add_action(GoToLine::toggle);
     cx.add_action(GoToLine::confirm);
 }

crates/gpui/Cargo.toml 🔗

@@ -25,7 +25,7 @@ etagere = "0.2"
 futures = "0.3"
 image = "0.23"
 lazy_static = "1.4.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 num_cpus = "1.13"
 ordered-float = "2.1.1"
 parking = "2.0.0"
@@ -67,6 +67,6 @@ core-graphics = "0.22.2"
 core-text = "19.2"
 font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }
 foreign-types = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 metal = "0.21.0"
 objc = "0.2"

crates/gpui/examples/text.rs 🔗

@@ -104,6 +104,7 @@ impl gpui::Element for TextElement {
         &mut self,
         _: &gpui::Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut gpui::EventContext,

crates/gpui/src/app.rs 🔗

@@ -1,19 +1,23 @@
+pub mod action;
+
 use crate::{
     elements::ElementBox,
     executor::{self, Task},
-    keymap::{self, Keystroke},
+    keymap::{self, Binding, Keystroke},
     platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
 };
-use anyhow::{anyhow, Result};
+pub use action::*;
+use anyhow::{anyhow, Context, Result};
 use collections::btree_map;
 use keymap::MatchResult;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use platform::Event;
 use postage::oneshot;
+use smallvec::SmallVec;
 use smol::prelude::*;
 use std::{
     any::{type_name, Any, TypeId},
@@ -59,6 +63,9 @@ pub trait View: Entity + Sized {
         cx.set.insert(Self::ui_name().into());
         cx
     }
+    fn debug_json(&self, _: &AppContext) -> serde_json::Value {
+        serde_json::Value::Null
+    }
 }
 
 pub trait ReadModel {
@@ -142,89 +149,6 @@ pub trait ElementStateContext: DerefMut<Target = MutableAppContext> {
     }
 }
 
-pub trait Action: 'static + AnyAction {
-    type Argument: 'static + Clone;
-}
-
-pub trait AnyAction {
-    fn id(&self) -> TypeId;
-    fn name(&self) -> &'static str;
-    fn as_any(&self) -> &dyn Any;
-    fn boxed_clone(&self) -> Box<dyn AnyAction>;
-    fn boxed_clone_as_any(&self) -> Box<dyn Any>;
-}
-
-#[macro_export]
-macro_rules! action {
-    ($name:ident, $arg:ty) => {
-        #[derive(Clone)]
-        pub struct $name(pub $arg);
-
-        impl $crate::Action for $name {
-            type Argument = $arg;
-        }
-
-        impl $crate::AnyAction for $name {
-            fn id(&self) -> std::any::TypeId {
-                std::any::TypeId::of::<$name>()
-            }
-
-            fn name(&self) -> &'static str {
-                stringify!($name)
-            }
-
-            fn as_any(&self) -> &dyn std::any::Any {
-                self
-            }
-
-            fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
-                Box::new(self.clone())
-            }
-
-            fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
-                Box::new(self.clone())
-            }
-        }
-
-        impl From<$arg> for $name {
-            fn from(arg: $arg) -> Self {
-                Self(arg)
-            }
-        }
-    };
-
-    ($name:ident) => {
-        #[derive(Clone, Debug, Eq, PartialEq)]
-        pub struct $name;
-
-        impl $crate::Action for $name {
-            type Argument = ();
-        }
-
-        impl $crate::AnyAction for $name {
-            fn id(&self) -> std::any::TypeId {
-                std::any::TypeId::of::<$name>()
-            }
-
-            fn name(&self) -> &'static str {
-                stringify!($name)
-            }
-
-            fn as_any(&self) -> &dyn std::any::Any {
-                self
-            }
-
-            fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
-                Box::new(self.clone())
-            }
-
-            fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
-                Box::new(self.clone())
-            }
-        }
-    };
-}
-
 pub struct Menu<'a> {
     pub name: &'a str,
     pub items: Vec<MenuItem<'a>>,
@@ -234,7 +158,7 @@ pub enum MenuItem<'a> {
     Action {
         name: &'a str,
         keystroke: Option<&'a str>,
-        action: Box<dyn AnyAction>,
+        action: Box<dyn Action>,
     },
     Separator,
 }
@@ -324,7 +248,7 @@ impl App {
         self
     }
 
-    pub fn on_quit<F>(self, mut callback: F) -> Self
+    pub fn on_quit<F>(&mut self, mut callback: F) -> &mut Self
     where
         F: 'static + FnMut(&mut MutableAppContext),
     {
@@ -336,7 +260,7 @@ impl App {
         self
     }
 
-    pub fn on_event<F>(self, mut callback: F) -> Self
+    pub fn on_event<F>(&mut self, mut callback: F) -> &mut Self
     where
         F: 'static + FnMut(Event, &mut MutableAppContext) -> bool,
     {
@@ -350,15 +274,15 @@ impl App {
         self
     }
 
-    pub fn on_open_files<F>(self, mut callback: F) -> Self
+    pub fn on_open_urls<F>(&mut self, mut callback: F) -> &mut Self
     where
-        F: 'static + FnMut(Vec<PathBuf>, &mut MutableAppContext),
+        F: 'static + FnMut(Vec<String>, &mut MutableAppContext),
     {
         let cx = self.0.clone();
         self.0
             .borrow_mut()
             .foreground_platform
-            .on_open_files(Box::new(move |paths| {
+            .on_open_urls(Box::new(move |paths| {
                 callback(paths, &mut *cx.borrow_mut())
             }));
         self
@@ -426,15 +350,17 @@ impl TestAppContext {
         cx
     }
 
-    pub fn dispatch_action<A: Action>(
-        &self,
-        window_id: usize,
-        responder_chain: Vec<usize>,
-        action: A,
-    ) {
-        self.cx
-            .borrow_mut()
-            .dispatch_action_any(window_id, &responder_chain, &action);
+    pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
+        let mut cx = self.cx.borrow_mut();
+        let dispatch_path = cx
+            .presenters_and_platform_windows
+            .get(&window_id)
+            .unwrap()
+            .0
+            .borrow()
+            .dispatch_path(cx.as_ref());
+
+        cx.dispatch_action_any(window_id, &dispatch_path, &action);
     }
 
     pub fn dispatch_global_action<A: Action>(&self, action: A) {
@@ -455,9 +381,9 @@ impl TestAppContext {
                 .unwrap()
                 .0
                 .clone();
-            let responder_chain = presenter.borrow().dispatch_path(cx.as_ref());
+            let dispatch_path = presenter.borrow().dispatch_path(cx.as_ref());
 
-            if !cx.dispatch_keystroke(window_id, responder_chain, &keystroke) {
+            if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
                 presenter.borrow_mut().dispatch_event(
                     Event::KeyDown {
                         keystroke,
@@ -595,6 +521,15 @@ impl TestAppContext {
     pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
         self.cx.borrow().leak_detector()
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn assert_dropped(&self, handle: impl WeakHandle) {
+        self.cx
+            .borrow()
+            .leak_detector()
+            .lock()
+            .assert_dropped(handle.id())
+    }
 }
 
 impl AsyncAppContext {
@@ -776,20 +711,23 @@ impl ReadViewWith for TestAppContext {
 }
 
 type ActionCallback =
-    dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize);
-type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
+    dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
+type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
 
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
 type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
+type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
-type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
+type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
+type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
     foreground_platform: Rc<dyn platform::ForegroundPlatform>,
     assets: Arc<AssetCache>,
     cx: AppContext,
+    action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
     capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
@@ -802,6 +740,8 @@ pub struct MutableAppContext {
     global_subscriptions:
         Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
     observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>,
+    focus_observations:
+        Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<FocusObservationCallback>>>>>,
     global_observations:
         Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
     release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
@@ -809,6 +749,7 @@ pub struct MutableAppContext {
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
     foreground: Rc<executor::Foreground>,
     pending_effects: VecDeque<Effect>,
+    pending_focus_index: Option<usize>,
     pending_notifications: HashSet<usize>,
     pending_global_notifications: HashSet<TypeId>,
     pending_flushes: usize,
@@ -842,6 +783,7 @@ impl MutableAppContext {
                 font_cache,
                 platform,
             },
+            action_deserializers: HashMap::new(),
             capture_actions: HashMap::new(),
             actions: HashMap::new(),
             global_actions: HashMap::new(),
@@ -853,11 +795,13 @@ impl MutableAppContext {
             subscriptions: Default::default(),
             global_subscriptions: Default::default(),
             observations: Default::default(),
+            focus_observations: Default::default(),
             release_observations: Default::default(),
             global_observations: Default::default(),
             presenters_and_platform_windows: HashMap::new(),
             foreground,
             pending_effects: VecDeque::new(),
+            pending_focus_index: None,
             pending_notifications: HashSet::new(),
             pending_global_notifications: HashSet::new(),
             pending_flushes: 0,
@@ -926,6 +870,20 @@ impl MutableAppContext {
             .and_then(|(presenter, _)| presenter.borrow().debug_elements(self))
     }
 
+    pub fn deserialize_action(
+        &self,
+        name: &str,
+        argument: Option<&str>,
+    ) -> Result<Box<dyn Action>> {
+        let callback = self
+            .action_deserializers
+            .get(name)
+            .ok_or_else(|| anyhow!("unknown action {}", name))?
+            .1;
+        callback(argument.unwrap_or("{}"))
+            .with_context(|| format!("invalid data for action {}", name))
+    }
+
     pub fn add_action<A, V, F>(&mut self, handler: F)
     where
         A: Action,
@@ -952,7 +910,7 @@ impl MutableAppContext {
     {
         let handler = Box::new(
             move |view: &mut dyn AnyView,
-                  action: &dyn AnyAction,
+                  action: &dyn Action,
                   cx: &mut MutableAppContext,
                   window_id: usize,
                   view_id: usize| {
@@ -968,6 +926,10 @@ impl MutableAppContext {
             },
         );
 
+        self.action_deserializers
+            .entry(A::qualified_name())
+            .or_insert((TypeId::of::<A>(), A::from_json_str));
+
         let actions = if capture {
             &mut self.capture_actions
         } else {
@@ -998,17 +960,24 @@ impl MutableAppContext {
         A: Action,
         F: 'static + FnMut(&A, &mut MutableAppContext),
     {
-        let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| {
+        let handler = Box::new(move |action: &dyn Action, cx: &mut MutableAppContext| {
             let action = action.as_any().downcast_ref().unwrap();
             handler(action, cx);
         });
 
+        self.action_deserializers
+            .entry(A::qualified_name())
+            .or_insert((TypeId::of::<A>(), A::from_json_str));
+
         if self
             .global_actions
             .insert(TypeId::of::<A>(), handler)
             .is_some()
         {
-            panic!("registered multiple global handlers for the same action type");
+            panic!(
+                "registered multiple global handlers for {}",
+                type_name::<A>()
+            );
         }
     }
 
@@ -1222,6 +1191,32 @@ impl MutableAppContext {
         }
     }
 
+    fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
+    where
+        F: 'static + FnMut(ViewHandle<V>, &mut MutableAppContext) -> bool,
+        V: View,
+    {
+        let subscription_id = post_inc(&mut self.next_subscription_id);
+        let observed = handle.downgrade();
+        let view_id = handle.id();
+        self.pending_effects.push_back(Effect::FocusObservation {
+            view_id,
+            subscription_id,
+            callback: Box::new(move |cx| {
+                if let Some(observed) = observed.upgrade(cx) {
+                    callback(observed, cx)
+                } else {
+                    false
+                }
+            }),
+        });
+        Subscription::FocusObservation {
+            id: subscription_id,
+            view_id,
+            observations: Some(Arc::downgrade(&self.focus_observations)),
+        }
+    }
+
     pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
     where
         G: Any,
@@ -1250,12 +1245,12 @@ impl MutableAppContext {
         }
     }
 
-    pub fn observe_release<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+    pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
     where
         E: Entity,
         E::Event: 'static,
         H: Handle<E>,
-        F: 'static + FnMut(&E, &mut Self),
+        F: 'static + FnOnce(&E, &mut Self),
     {
         let id = post_inc(&mut self.next_subscription_id);
         self.release_observations
@@ -1311,20 +1306,71 @@ impl MutableAppContext {
         }
     }
 
+    pub fn available_actions(
+        &self,
+        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 presenter = self
+            .presenters_and_platform_windows
+            .get(&window_id)
+            .unwrap()
+            .0
+            .clone();
+        let dispatch_path = presenter.borrow().dispatch_path_from(view_id);
+        for view_id in dispatch_path {
+            if let Some(view) = self.views.get(&(window_id, view_id)) {
+                let view_type = view.as_any().type_id();
+                if let Some(actions) = self.actions.get(&view_type) {
+                    action_types.extend(actions.keys().copied());
+                }
+            }
+        }
+
+        self.action_deserializers
+            .iter()
+            .filter_map(move |(name, (type_id, deserialize))| {
+                if action_types.contains(type_id) {
+                    Some((
+                        *name,
+                        deserialize("{}").ok()?,
+                        self.keystroke_matcher
+                            .bindings_for_action_type(*type_id)
+                            .collect(),
+                    ))
+                } else {
+                    None
+                }
+            })
+    }
+
+    pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
+        let presenter = self
+            .presenters_and_platform_windows
+            .get(&window_id)
+            .unwrap()
+            .0
+            .clone();
+        let dispatch_path = presenter.borrow().dispatch_path_from(view_id);
+        self.dispatch_action_any(window_id, &dispatch_path, action);
+    }
+
     pub fn dispatch_action<A: Action>(
         &mut self,
         window_id: usize,
-        responder_chain: Vec<usize>,
+        dispatch_path: Vec<usize>,
         action: &A,
     ) {
-        self.dispatch_action_any(window_id, &responder_chain, action);
+        self.dispatch_action_any(window_id, &dispatch_path, action);
     }
 
     pub(crate) fn dispatch_action_any(
         &mut self,
         window_id: usize,
         path: &[usize],
-        action: &dyn AnyAction,
+        action: &dyn Action,
     ) -> bool {
         self.update(|this| {
             this.halt_action_dispatch = false;
@@ -1384,7 +1430,7 @@ impl MutableAppContext {
         self.dispatch_global_action_any(&action);
     }
 
-    fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) -> bool {
+    fn dispatch_global_action_any(&mut self, action: &dyn Action) -> bool {
         self.update(|this| {
             if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
                 handler(action, this);
@@ -1400,14 +1446,18 @@ impl MutableAppContext {
         self.keystroke_matcher.add_bindings(bindings);
     }
 
+    pub fn clear_bindings(&mut self) {
+        self.keystroke_matcher.clear_bindings();
+    }
+
     pub fn dispatch_keystroke(
         &mut self,
         window_id: usize,
-        responder_chain: Vec<usize>,
+        dispatch_path: Vec<usize>,
         keystroke: &Keystroke,
     ) -> bool {
         let mut context_chain = Vec::new();
-        for view_id in &responder_chain {
+        for view_id in &dispatch_path {
             let view = self
                 .cx
                 .views
@@ -1420,13 +1470,12 @@ impl MutableAppContext {
         for (i, cx) in context_chain.iter().enumerate().rev() {
             match self
                 .keystroke_matcher
-                .push_keystroke(keystroke.clone(), responder_chain[i], cx)
+                .push_keystroke(keystroke.clone(), dispatch_path[i], cx)
             {
                 MatchResult::None => {}
                 MatchResult::Pending => pending = true,
                 MatchResult::Action(action) => {
-                    if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
-                    {
+                    if self.dispatch_action_any(window_id, &dispatch_path[0..=i], action.as_ref()) {
                         self.keystroke_matcher.clear_pending();
                         return true;
                     }
@@ -1706,7 +1755,7 @@ impl MutableAppContext {
                 });
 
                 if let Some(view_id) = change_focus_to {
-                    self.focus(window_id, Some(view_id));
+                    self.handle_focus_effect(window_id, Some(view_id));
                 }
 
                 self.pending_effects
@@ -1729,6 +1778,9 @@ impl MutableAppContext {
             let mut refreshing = false;
             loop {
                 if let Some(effect) = self.pending_effects.pop_front() {
+                    if let Some(pending_focus_index) = self.pending_focus_index.as_mut() {
+                        *pending_focus_index = pending_focus_index.saturating_sub(1);
+                    }
                     match effect {
                         Effect::Subscription {
                             entity_id,
@@ -1752,13 +1804,13 @@ impl MutableAppContext {
                             callback,
                         } => self.handle_observation_effect(entity_id, subscription_id, callback),
                         Effect::ModelNotification { model_id } => {
-                            self.notify_model_observers(model_id)
+                            self.handle_model_notification_effect(model_id)
                         }
                         Effect::ViewNotification { window_id, view_id } => {
-                            self.notify_view_observers(window_id, view_id)
+                            self.handle_view_notification_effect(window_id, view_id)
                         }
                         Effect::GlobalNotification { type_id } => {
-                            self.notify_global_observers(type_id)
+                            self.handle_global_notification_effect(type_id)
                         }
                         Effect::Deferred {
                             callback,
@@ -1771,13 +1823,20 @@ impl MutableAppContext {
                             }
                         }
                         Effect::ModelRelease { model_id, model } => {
-                            self.notify_release_observers(model_id, model.as_any())
+                            self.handle_entity_release_effect(model_id, model.as_any())
                         }
                         Effect::ViewRelease { view_id, view } => {
-                            self.notify_release_observers(view_id, view.as_any())
+                            self.handle_entity_release_effect(view_id, view.as_any())
                         }
                         Effect::Focus { window_id, view_id } => {
-                            self.focus(window_id, view_id);
+                            self.handle_focus_effect(window_id, view_id);
+                        }
+                        Effect::FocusObservation {
+                            view_id,
+                            subscription_id,
+                            callback,
+                        } => {
+                            self.handle_focus_observation_effect(view_id, subscription_id, callback)
                         }
                         Effect::ResizeWindow { window_id } => {
                             if let Some(window) = self.cx.windows.get_mut(&window_id) {
@@ -2009,7 +2068,31 @@ impl MutableAppContext {
         }
     }
 
-    fn notify_model_observers(&mut self, observed_id: usize) {
+    fn handle_focus_observation_effect(
+        &mut self,
+        view_id: usize,
+        subscription_id: usize,
+        callback: FocusObservationCallback,
+    ) {
+        match self
+            .focus_observations
+            .lock()
+            .entry(view_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Observation was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
+    fn handle_model_notification_effect(&mut self, observed_id: usize) {
         let callbacks = self.observations.lock().remove(&observed_id);
         if let Some(callbacks) = callbacks {
             if self.cx.models.contains_key(&observed_id) {
@@ -2038,7 +2121,11 @@ impl MutableAppContext {
         }
     }
 
-    fn notify_view_observers(&mut self, observed_window_id: usize, observed_view_id: usize) {
+    fn handle_view_notification_effect(
+        &mut self,
+        observed_window_id: usize,
+        observed_view_id: usize,
+    ) {
         if let Some(window) = self.cx.windows.get_mut(&observed_window_id) {
             window
                 .invalidation
@@ -2079,7 +2166,7 @@ impl MutableAppContext {
         }
     }
 
-    fn notify_global_observers(&mut self, observed_type_id: TypeId) {
+    fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
         let callbacks = self.global_observations.lock().remove(&observed_type_id);
         if let Some(callbacks) = callbacks {
             if let Some(global) = self.cx.globals.remove(&observed_type_id) {
@@ -2107,16 +2194,18 @@ impl MutableAppContext {
         }
     }
 
-    fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) {
+    fn handle_entity_release_effect(&mut self, entity_id: usize, entity: &dyn Any) {
         let callbacks = self.release_observations.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
-            for (_, mut callback) in callbacks {
+            for (_, callback) in callbacks {
                 callback(entity, self);
             }
         }
     }
 
-    fn focus(&mut self, window_id: usize, focused_id: Option<usize>) {
+    fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
+        self.pending_focus_index.take();
+
         if self
             .cx
             .windows
@@ -2145,11 +2234,45 @@ impl MutableAppContext {
                 if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) {
                     focused_view.on_focus(this, window_id, focused_id);
                     this.cx.views.insert((window_id, focused_id), focused_view);
+
+                    let callbacks = this.focus_observations.lock().remove(&focused_id);
+                    if let Some(callbacks) = callbacks {
+                        for (id, callback) in callbacks {
+                            if let Some(mut callback) = callback {
+                                let alive = callback(this);
+                                if alive {
+                                    match this
+                                        .focus_observations
+                                        .lock()
+                                        .entry(focused_id)
+                                        .or_default()
+                                        .entry(id)
+                                    {
+                                        btree_map::Entry::Vacant(entry) => {
+                                            entry.insert(Some(callback));
+                                        }
+                                        btree_map::Entry::Occupied(entry) => {
+                                            entry.remove();
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
                 }
             }
         })
     }
 
+    fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
+        if let Some(pending_focus_index) = self.pending_focus_index {
+            self.pending_effects.remove(pending_focus_index);
+        }
+        self.pending_focus_index = Some(self.pending_effects.len());
+        self.pending_effects
+            .push_back(Effect::Focus { window_id, view_id });
+    }
+
     pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
     where
         F: FnOnce(AsyncAppContext) -> Fut,
@@ -2316,6 +2439,12 @@ pub struct AppContext {
 }
 
 impl AppContext {
+    pub(crate) fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
+        self.windows
+            .get(&window_id)
+            .map(|window| window.root_view.clone())
+    }
+
     pub fn root_view_id(&self, window_id: usize) -> Option<usize> {
         self.windows
             .get(&window_id)
@@ -2345,11 +2474,11 @@ impl AppContext {
     }
 
     pub fn global<T: 'static>(&self) -> &T {
-        self.globals
-            .get(&TypeId::of::<T>())
-            .expect("no app state has been added for this type")
-            .downcast_ref()
-            .unwrap()
+        if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
+            global.downcast_ref().unwrap()
+        } else {
+            panic!("no global has been added for {}", type_name::<T>());
+        }
     }
 }
 
@@ -2495,6 +2624,11 @@ pub enum Effect {
         window_id: usize,
         view_id: Option<usize>,
     },
+    FocusObservation {
+        view_id: usize,
+        subscription_id: usize,
+        callback: FocusObservationCallback,
+    },
     ResizeWindow {
         window_id: usize,
     },
@@ -2566,6 +2700,15 @@ impl Debug for Effect {
                 .field("window_id", window_id)
                 .field("view_id", view_id)
                 .finish(),
+            Effect::FocusObservation {
+                view_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::FocusObservation")
+                .field("view_id", view_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::ResizeWindow { window_id } => f
                 .debug_struct("Effect::RefreshWindow")
                 .field("window_id", window_id)
@@ -2629,6 +2772,7 @@ pub trait AnyView {
     fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
     fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
     fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
+    fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
 }
 
 impl<T> AnyView for T
@@ -2692,6 +2836,10 @@ where
     fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
         View::keymap_context(self, cx)
     }
+
+    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+        View::debug_json(self, cx)
+    }
 }
 
 pub struct ModelContext<'a, T: ?Sized> {
@@ -2973,24 +3121,15 @@ impl<'a, T: View> ViewContext<'a, T> {
         S: Into<AnyViewHandle>,
     {
         let handle = handle.into();
-        self.app.pending_effects.push_back(Effect::Focus {
-            window_id: handle.window_id,
-            view_id: Some(handle.view_id),
-        });
+        self.app.focus(handle.window_id, Some(handle.view_id));
     }
 
     pub fn focus_self(&mut self) {
-        self.app.pending_effects.push_back(Effect::Focus {
-            window_id: self.window_id,
-            view_id: Some(self.view_id),
-        });
+        self.app.focus(self.window_id, Some(self.view_id));
     }
 
     pub fn blur(&mut self) {
-        self.app.pending_effects.push_back(Effect::Focus {
-            window_id: self.window_id,
-            view_id: None,
-        });
+        self.app.focus(self.window_id, None);
     }
 
     pub fn add_model<S, F>(&mut self, build_model: F) -> ModelHandle<S>
@@ -3057,6 +3196,24 @@ impl<'a, T: View> ViewContext<'a, T> {
         })
     }
 
+    pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut T, ViewHandle<V>, &mut ViewContext<T>),
+        V: View,
+    {
+        let observer = self.weak_handle();
+        self.app.observe_focus(handle, move |observed, cx| {
+            if let Some(observer) = observer.upgrade(cx) {
+                observer.update(cx, |observer, cx| {
+                    callback(observer, observed, cx);
+                });
+                true
+            } else {
+                false
+            }
+        })
+    }
+
     pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
     where
         E: Entity,
@@ -3301,6 +3458,10 @@ pub trait Handle<T> {
         Self: Sized;
 }
 
+pub trait WeakHandle {
+    fn id(&self) -> usize;
+}
+
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub enum EntityLocation {
     Model(usize),
@@ -3375,12 +3536,10 @@ impl<T: Entity> ModelHandle<T> {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
-        use postage::prelude::{Sink as _, Stream as _};
-
-        let (mut tx, mut rx) = postage::mpsc::channel(1);
+        let (tx, mut rx) = futures::channel::mpsc::unbounded();
         let mut cx = cx.cx.borrow_mut();
         let subscription = cx.observe(self, move |_, _| {
-            tx.try_send(()).ok();
+            tx.unbounded_send(()).ok();
         });
 
         let duration = if std::env::var("CI").is_ok() {
@@ -3390,7 +3549,7 @@ impl<T: Entity> ModelHandle<T> {
         };
 
         async move {
-            let notification = crate::util::timeout(duration, rx.recv())
+            let notification = crate::util::timeout(duration, rx.next())
                 .await
                 .expect("next notification timed out");
             drop(subscription);
@@ -3403,12 +3562,10 @@ impl<T: Entity> ModelHandle<T> {
     where
         T::Event: Clone,
     {
-        use postage::prelude::{Sink as _, Stream as _};
-
-        let (mut tx, mut rx) = postage::mpsc::channel(1);
+        let (tx, mut rx) = futures::channel::mpsc::unbounded();
         let mut cx = cx.cx.borrow_mut();
         let subscription = cx.subscribe(self, move |_, event, _| {
-            tx.blocking_send(event.clone()).ok();
+            tx.unbounded_send(event.clone()).ok();
         });
 
         let duration = if std::env::var("CI").is_ok() {
@@ -3417,8 +3574,9 @@ impl<T: Entity> ModelHandle<T> {
             Duration::from_secs(1)
         };
 
+        cx.foreground.start_waiting();
         async move {
-            let event = crate::util::timeout(duration, rx.recv())
+            let event = crate::util::timeout(duration, rx.next())
                 .await
                 .expect("next event timed out");
             drop(subscription);
@@ -3432,22 +3590,20 @@ impl<T: Entity> ModelHandle<T> {
         cx: &TestAppContext,
         mut predicate: impl FnMut(&T, &AppContext) -> bool,
     ) -> impl Future<Output = ()> {
-        use postage::prelude::{Sink as _, Stream as _};
-
-        let (tx, mut rx) = postage::mpsc::channel(1024);
+        let (tx, mut rx) = futures::channel::mpsc::unbounded();
 
         let mut cx = cx.cx.borrow_mut();
         let subscriptions = (
             cx.observe(self, {
-                let mut tx = tx.clone();
+                let tx = tx.clone();
                 move |_, _| {
-                    tx.blocking_send(()).ok();
+                    tx.unbounded_send(()).ok();
                 }
             }),
             cx.subscribe(self, {
-                let mut tx = tx.clone();
+                let tx = tx.clone();
                 move |_, _, _| {
-                    tx.blocking_send(()).ok();
+                    tx.unbounded_send(()).ok();
                 }
             }),
         );
@@ -3478,7 +3634,7 @@ impl<T: Entity> ModelHandle<T> {
                     }
 
                     cx.borrow().foreground().start_waiting();
-                    rx.recv()
+                    rx.next()
                         .await
                         .expect("model dropped with pending condition");
                     cx.borrow().foreground().finish_waiting();
@@ -3575,6 +3731,12 @@ pub struct WeakModelHandle<T> {
     model_type: PhantomData<T>,
 }
 
+impl<T> WeakHandle for WeakModelHandle<T> {
+    fn id(&self) -> usize {
+        self.model_id
+    }
+}
+
 unsafe impl<T> Send for WeakModelHandle<T> {}
 unsafe impl<T> Sync for WeakModelHandle<T> {}
 
@@ -3961,6 +4123,12 @@ impl AnyViewHandle {
     pub fn view_type(&self) -> TypeId {
         self.view_type
     }
+
+    pub fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+        cx.views
+            .get(&(self.window_id, self.view_id))
+            .map_or_else(|| serde_json::Value::Null, |view| view.debug_json(cx))
+    }
 }
 
 impl Clone for AnyViewHandle {
@@ -4144,6 +4312,12 @@ pub struct WeakViewHandle<T> {
     view_type: PhantomData<T>,
 }
 
+impl<T> WeakHandle for WeakViewHandle<T> {
+    fn id(&self) -> usize {
+        self.view_id
+    }
+}
+
 impl<T: View> WeakViewHandle<T> {
     fn new(window_id: usize, view_id: usize) -> Self {
         Self {
@@ -4304,6 +4478,12 @@ pub enum Subscription {
             Weak<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
         >,
     },
+    FocusObservation {
+        id: usize,
+        view_id: usize,
+        observations:
+            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<FocusObservationCallback>>>>>>,
+    },
     ReleaseObservation {
         id: usize,
         entity_id: usize,
@@ -4330,6 +4510,9 @@ impl Subscription {
             Subscription::ReleaseObservation { observations, .. } => {
                 observations.take();
             }
+            Subscription::FocusObservation { observations, .. } => {
+                observations.take();
+            }
         }
     }
 }

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

@@ -0,0 +1,109 @@
+use std::any::{Any, TypeId};
+
+pub trait Action: 'static {
+    fn id(&self) -> TypeId;
+    fn name(&self) -> &'static str;
+    fn as_any(&self) -> &dyn Any;
+    fn boxed_clone(&self) -> Box<dyn Action>;
+
+    fn qualified_name() -> &'static str
+    where
+        Self: Sized;
+    fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
+    where
+        Self: Sized;
+}
+
+/// 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
+/// the given action types, to ensure that they have globally unique
+/// qualified names for use in keymap files.
+#[macro_export]
+macro_rules! actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+        $(
+            #[derive(Clone, Debug, Default, PartialEq, Eq)]
+            pub struct $name;
+            $crate::__impl_action! {
+                $namespace,
+                $name,
+                fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Ok(Box::new(Self))
+                }
+            }
+        )*
+    };
+}
+
+/// Implement the `Action` trait for a set of existing types.
+///
+/// The first argument is a namespace that will be associated with each of
+/// the given action types, to ensure that they have globally unique
+/// qualified names for use in keymap files.
+#[macro_export]
+macro_rules! impl_actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+        $(
+            $crate::__impl_action! {
+                $namespace,
+                $name,
+                fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
+                }
+            }
+        )*
+    };
+}
+
+/// Implement the `Action` trait for a set of existing types that are
+/// not intended to be constructed via a keymap file, but only dispatched
+/// internally.
+#[macro_export]
+macro_rules! impl_internal_actions {
+    ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+        $(
+            $crate::__impl_action! {
+                $namespace,
+                $name,
+                fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Err($crate::anyhow::anyhow!("internal action"))
+                }
+            }
+        )*
+    };
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __impl_action {
+    ($namespace:path, $name:ident, $from_json_fn:item) => {
+        impl $crate::action::Action for $name {
+            fn name(&self) -> &'static str {
+                stringify!($name)
+            }
+
+            fn qualified_name() -> &'static str {
+                concat!(
+                    stringify!($namespace),
+                    "::",
+                    stringify!($name),
+                )
+            }
+
+            fn id(&self) -> std::any::TypeId {
+                std::any::TypeId::of::<$name>()
+            }
+
+            fn as_any(&self) -> &dyn std::any::Any {
+                self
+            }
+
+            fn boxed_clone(&self) -> Box<dyn $crate::Action> {
+                Box::new(self.clone())
+            }
+
+            $from_json_fn
+        }
+    };
+}

crates/gpui/src/elements.rs 🔗

@@ -74,6 +74,7 @@ pub trait Element {
         &mut self,
         event: &Event,
         bounds: RectF,
+        visible_bounds: RectF,
         layout: &mut Self::LayoutState,
         paint: &mut Self::PaintState,
         cx: &mut EventContext,
@@ -169,6 +170,7 @@ pub enum Lifecycle<T: Element> {
         element: T,
         constraint: SizeConstraint,
         bounds: RectF,
+        visible_bounds: RectF,
         layout: T::LayoutState,
         paint: T::PaintState,
     },
@@ -222,6 +224,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
                     element,
                     constraint,
                     bounds,
+                    visible_bounds,
                     layout,
                     paint,
                 }
@@ -242,6 +245,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
                     element,
                     constraint,
                     bounds,
+                    visible_bounds,
                     layout,
                     paint,
                 }
@@ -254,12 +258,13 @@ impl<T: Element> AnyElement for Lifecycle<T> {
         if let Lifecycle::PostPaint {
             element,
             bounds,
+            visible_bounds,
             layout,
             paint,
             ..
         } = self
         {
-            element.dispatch_event(event, *bounds, layout, paint, cx)
+            element.dispatch_event(event, *bounds, *visible_bounds, layout, paint, cx)
         } else {
             panic!("invalid element lifecycle state");
         }
@@ -288,6 +293,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
                 element,
                 constraint,
                 bounds,
+                visible_bounds,
                 layout,
                 paint,
             } => {
@@ -299,6 +305,8 @@ impl<T: Element> AnyElement for Lifecycle<T> {
                         new_map.insert("type".into(), typ);
                     }
                     new_map.insert("constraint".into(), constraint.to_json());
+                    new_map.insert("bounds".into(), bounds.to_json());
+                    new_map.insert("visible_bounds".into(), visible_bounds.to_json());
                     new_map.append(map);
                     json::Value::Object(new_map)
                 } else {

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

@@ -85,7 +85,8 @@ impl Element for Align {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        _: pathfinder_geometry::rect::RectF,
+        _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -59,6 +59,7 @@ where
         &mut self,
         _: &crate::Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut crate::EventContext,

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

@@ -247,6 +247,7 @@ impl Element for Container {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -52,6 +52,7 @@ impl Element for Empty {
         &mut self,
         _: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut EventContext,

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

@@ -84,13 +84,14 @@ impl Element for EventHandler {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: RectF,
+        _: RectF,
+        visible_bounds: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
         if let Some(capture) = self.capture.as_mut() {
-            if capture(event, bounds, cx) {
+            if capture(event, visible_bounds, cx) {
                 return true;
             }
         }
@@ -101,7 +102,7 @@ impl Element for EventHandler {
             match event {
                 Event::LeftMouseDown { position, .. } => {
                     if let Some(callback) = self.mouse_down.as_mut() {
-                        if bounds.contains_point(*position) {
+                        if visible_bounds.contains_point(*position) {
                             return callback(cx);
                         }
                     }
@@ -109,7 +110,7 @@ impl Element for EventHandler {
                 }
                 Event::RightMouseDown { position, .. } => {
                     if let Some(callback) = self.right_mouse_down.as_mut() {
-                        if bounds.contains_point(*position) {
+                        if visible_bounds.contains_point(*position) {
                             return callback(cx);
                         }
                     }
@@ -121,7 +122,7 @@ impl Element for EventHandler {
                     ..
                 } => {
                     if let Some(callback) = self.navigate_mouse_down.as_mut() {
-                        if bounds.contains_point(*position) {
+                        if visible_bounds.contains_point(*position) {
                             return callback(*direction, cx);
                         }
                     }

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

@@ -66,6 +66,7 @@ impl Element for Expanded {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY};
 
 use crate::{
     json::{self, ToJson, Value},
-    Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
-    SizeConstraint, Vector2FExt,
+    Axis, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
+    EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -11,9 +11,16 @@ use pathfinder_geometry::{
 };
 use serde_json::json;
 
+#[derive(Default)]
+struct ScrollState {
+    scroll_to: Option<usize>,
+    scroll_position: f32,
+}
+
 pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
+    scroll_state: Option<ElementStateHandle<ScrollState>>,
 }
 
 impl Flex {
@@ -21,6 +28,7 @@ impl Flex {
         Self {
             axis,
             children: Default::default(),
+            scroll_state: None,
         }
     }
 
@@ -32,6 +40,22 @@ impl Flex {
         Self::new(Axis::Vertical)
     }
 
+    pub fn scrollable<Tag, C>(
+        mut self,
+        element_id: usize,
+        scroll_to: Option<usize>,
+        cx: &mut C,
+    ) -> Self
+    where
+        Tag: 'static,
+        C: ElementStateContext,
+    {
+        let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
+        scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
+        self.scroll_state = Some(scroll_state);
+        self
+    }
+
     fn layout_flex_children(
         &mut self,
         layout_expanded: bool,
@@ -167,6 +191,30 @@ impl Element for Flex {
             size.set_y(constraint.max.y());
         }
 
+        if let Some(scroll_state) = self.scroll_state.as_ref() {
+            scroll_state.update(cx, |scroll_state, _| {
+                if let Some(scroll_to) = scroll_state.scroll_to.take() {
+                    let visible_start = scroll_state.scroll_position;
+                    let visible_end = visible_start + size.along(self.axis);
+                    if let Some(child) = self.children.get(scroll_to) {
+                        let child_start: f32 = self.children[..scroll_to]
+                            .iter()
+                            .map(|c| c.size().along(self.axis))
+                            .sum();
+                        let child_end = child_start + child.size().along(self.axis);
+                        if child_start < visible_start {
+                            scroll_state.scroll_position = child_start;
+                        } else if child_end > visible_end {
+                            scroll_state.scroll_position = child_end - size.along(self.axis);
+                        }
+                    }
+                }
+
+                scroll_state.scroll_position =
+                    scroll_state.scroll_position.min(-remaining_space).max(0.);
+            });
+        }
+
         (size, remaining_space)
     }
 
@@ -181,7 +229,16 @@ impl Element for Flex {
         if overflowing {
             cx.scene.push_layer(Some(bounds));
         }
+
         let mut child_origin = bounds.origin();
+        if let Some(scroll_state) = self.scroll_state.as_ref() {
+            let scroll_position = scroll_state.read(cx).scroll_position;
+            match self.axis {
+                Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
+                Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
+            }
+        }
+
         for child in &mut self.children {
             if *remaining_space > 0. {
                 if let Some(metadata) = child.metadata::<FlexParentData>() {
@@ -208,8 +265,9 @@ impl Element for Flex {
     fn dispatch_event(
         &mut self,
         event: &Event,
+        bounds: RectF,
         _: RectF,
-        _: &mut Self::LayoutState,
+        remaining_space: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
@@ -217,6 +275,39 @@ impl Element for Flex {
         for child in &mut self.children {
             handled = child.dispatch_event(event, cx) || handled;
         }
+        if !handled {
+            if let &Event::ScrollWheel {
+                position,
+                delta,
+                precise,
+            } = event
+            {
+                if *remaining_space < 0. && bounds.contains_point(position) {
+                    if let Some(scroll_state) = self.scroll_state.as_ref() {
+                        scroll_state.update(cx, |scroll_state, cx| {
+                            let mut delta = match self.axis {
+                                Axis::Horizontal => {
+                                    if delta.x() != 0. {
+                                        delta.x()
+                                    } else {
+                                        delta.y()
+                                    }
+                                }
+                                Axis::Vertical => delta.y(),
+                            };
+                            if !precise {
+                                delta *= 20.;
+                            }
+
+                            scroll_state.scroll_position -= delta;
+
+                            handled = true;
+                            cx.notify();
+                        });
+                    }
+                }
+            }
+        }
         handled
     }
 
@@ -295,6 +386,7 @@ impl Element for FlexItem {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -57,6 +57,7 @@ impl Element for Hook {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -81,6 +81,7 @@ impl Element for Image {
         &mut self,
         _: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut EventContext,

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

@@ -166,6 +166,7 @@ impl Element for Label {
         &mut self,
         _: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut EventContext,

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

@@ -253,6 +253,7 @@ impl Element for List {
         &mut self,
         event: &Event,
         bounds: RectF,
+        _: RectF,
         scroll_top: &mut ListOffset,
         _: &mut (),
         cx: &mut EventContext,
@@ -872,6 +873,7 @@ mod tests {
             &mut self,
             _: &Event,
             _: RectF,
+            _: RectF,
             _: &mut (),
             _: &mut (),
             _: &mut EventContext,

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

@@ -99,7 +99,8 @@ impl Element for MouseEventHandler {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: RectF,
+        _: RectF,
+        visible_bounds: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
@@ -112,8 +113,8 @@ impl Element for MouseEventHandler {
         let handled_in_child = self.child.dispatch_event(event, cx);
 
         let hit_bounds = RectF::from_points(
-            bounds.origin() - vec2f(self.padding.left, self.padding.top),
-            bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
+            visible_bounds.origin() - vec2f(self.padding.left, self.padding.top),
+            visible_bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
         )
         .round_out();
 

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

@@ -44,6 +44,7 @@ impl Element for Overlay {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -51,6 +51,7 @@ impl Element for Stack {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

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

@@ -76,6 +76,7 @@ impl Element for Svg {
         &mut self,
         _: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut EventContext,

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

@@ -172,6 +172,7 @@ impl Element for Text {
         &mut self,
         _: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut EventContext,

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

@@ -8,11 +8,10 @@ use crate::{
     ElementBox,
 };
 use json::ToJson;
-use parking_lot::Mutex;
-use std::{cmp, ops::Range, sync::Arc};
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
 
 #[derive(Clone, Default)]
-pub struct UniformListState(Arc<Mutex<StateInner>>);
+pub struct UniformListState(Rc<RefCell<StateInner>>);
 
 #[derive(Debug)]
 pub enum ScrollTarget {
@@ -22,11 +21,11 @@ pub enum ScrollTarget {
 
 impl UniformListState {
     pub fn scroll_to(&self, scroll_to: ScrollTarget) {
-        self.0.lock().scroll_to = Some(scroll_to);
+        self.0.borrow_mut().scroll_to = Some(scroll_to);
     }
 
     pub fn scroll_top(&self) -> f32 {
-        self.0.lock().scroll_top
+        self.0.borrow().scroll_top
     }
 }
 
@@ -96,7 +95,7 @@ where
             delta *= 20.;
         }
 
-        let mut state = self.state.0.lock();
+        let mut state = self.state.0.borrow_mut();
         state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
         cx.notify();
 
@@ -104,7 +103,7 @@ where
     }
 
     fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
-        let mut state = self.state.0.lock();
+        let mut state = self.state.0.borrow_mut();
 
         if let Some(scroll_to) = state.scroll_to.take() {
             let item_ix;
@@ -141,7 +140,7 @@ where
     }
 
     fn scroll_top(&self) -> f32 {
-        self.state.0.lock().scroll_top
+        self.state.0.borrow().scroll_top
     }
 }
 
@@ -282,6 +281,7 @@ where
         &mut self,
         event: &Event,
         bounds: RectF,
+        _: RectF,
         layout: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,

crates/gpui/src/executor.rs 🔗

@@ -1,5 +1,6 @@
 use anyhow::{anyhow, Result};
 use async_task::Runnable;
+use futures::channel::mpsc;
 use smol::{channel, prelude::*, Executor};
 use std::{
     any::Any,
@@ -621,17 +622,13 @@ impl Background {
         Err(async { *future.await.downcast().unwrap() })
     }
 
-    pub async fn scoped<'scope, F>(&self, scheduler: F)
+    pub async fn scoped<'scope, F>(self: &Arc<Self>, scheduler: F)
     where
         F: FnOnce(&mut Scope<'scope>),
     {
-        let mut scope = Scope {
-            futures: Default::default(),
-            _phantom: PhantomData,
-        };
+        let mut scope = Scope::new(self.clone());
         (scheduler)(&mut scope);
-        let spawned = scope
-            .futures
+        let spawned = mem::take(&mut scope.futures)
             .into_iter()
             .map(|f| self.spawn(f))
             .collect::<Vec<_>>();
@@ -668,25 +665,56 @@ impl Background {
 }
 
 pub struct Scope<'a> {
+    executor: Arc<Background>,
     futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
+    tx: Option<mpsc::Sender<()>>,
+    rx: mpsc::Receiver<()>,
     _phantom: PhantomData<&'a ()>,
 }
 
 impl<'a> Scope<'a> {
+    fn new(executor: Arc<Background>) -> Self {
+        let (tx, rx) = mpsc::channel(1);
+        Self {
+            executor,
+            tx: Some(tx),
+            rx,
+            futures: Default::default(),
+            _phantom: PhantomData,
+        }
+    }
+
     pub fn spawn<F>(&mut self, f: F)
     where
         F: Future<Output = ()> + Send + 'a,
     {
+        let tx = self.tx.clone().unwrap();
+
+        // Safety: The 'a lifetime is guaranteed to outlive any of these futures because
+        // dropping this `Scope` blocks until all of the futures have resolved.
         let f = unsafe {
             mem::transmute::<
                 Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
                 Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
-            >(Box::pin(f))
+            >(Box::pin(async move {
+                f.await;
+                drop(tx);
+            }))
         };
         self.futures.push(f);
     }
 }
 
+impl<'a> Drop for Scope<'a> {
+    fn drop(&mut self) {
+        self.tx.take().unwrap();
+
+        // Wait until the channel is closed, which means that all of the spawned
+        // futures have resolved.
+        self.executor.block(self.rx.next());
+    }
+}
+
 impl<T> Task<T> {
     pub fn ready(value: T) -> Self {
         Self::Ready(Some(value))

crates/gpui/src/gpui.rs 🔗

@@ -33,3 +33,6 @@ pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, Prom
 pub use presenter::{
     Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };
+
+pub use anyhow;
+pub use serde_json;

crates/gpui/src/keymap.rs 🔗

@@ -1,13 +1,13 @@
-use anyhow::anyhow;
+use crate::Action;
+use anyhow::{anyhow, Result};
+use smallvec::SmallVec;
 use std::{
-    any::Any,
+    any::{Any, TypeId},
     collections::{HashMap, HashSet},
     fmt::Debug,
 };
 use tree_sitter::{Language, Node, Parser};
 
-use crate::{Action, AnyAction};
-
 extern "C" {
     fn tree_sitter_context_predicate() -> Language;
 }
@@ -24,11 +24,14 @@ struct Pending {
 }
 
 #[derive(Default)]
-pub struct Keymap(Vec<Binding>);
+pub struct Keymap {
+    bindings: Vec<Binding>,
+    binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
+}
 
 pub struct Binding {
     keystrokes: Vec<Keystroke>,
-    action: Box<dyn AnyAction>,
+    action: Box<dyn Action>,
     context: Option<ContextPredicate>,
 }
 
@@ -73,7 +76,7 @@ where
 pub enum MatchResult {
     None,
     Pending,
-    Action(Box<dyn AnyAction>),
+    Action(Box<dyn Action>),
 }
 
 impl Debug for MatchResult {
@@ -107,6 +110,15 @@ impl Matcher {
         self.keymap.add_bindings(bindings);
     }
 
+    pub fn clear_bindings(&mut self) {
+        self.pending.clear();
+        self.keymap.clear();
+    }
+
+    pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
+        self.keymap.bindings_for_action_type(action_type)
+    }
+
     pub fn clear_pending(&mut self) {
         self.pending.clear();
     }
@@ -128,7 +140,7 @@ impl Matcher {
         pending.keystrokes.push(keystroke);
 
         let mut retain_pending = false;
-        for binding in self.keymap.0.iter().rev() {
+        for binding in self.keymap.bindings.iter().rev() {
             if binding.keystrokes.starts_with(&pending.keystrokes)
                 && binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true)
             {
@@ -159,30 +171,73 @@ impl Default for Matcher {
 
 impl Keymap {
     pub fn new(bindings: Vec<Binding>) -> Self {
-        Self(bindings)
+        let mut binding_indices_by_action_type = HashMap::new();
+        for (ix, binding) in bindings.iter().enumerate() {
+            binding_indices_by_action_type
+                .entry(binding.action.as_any().type_id())
+                .or_insert_with(|| SmallVec::new())
+                .push(ix);
+        }
+        Self {
+            binding_indices_by_action_type,
+            bindings,
+        }
+    }
+
+    fn bindings_for_action_type<'a>(
+        &'a self,
+        action_type: TypeId,
+    ) -> impl Iterator<Item = &'a Binding> {
+        self.binding_indices_by_action_type
+            .get(&action_type)
+            .map(SmallVec::as_slice)
+            .unwrap_or(&[])
+            .iter()
+            .map(|ix| &self.bindings[*ix])
     }
 
     fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
-        self.0.extend(bindings.into_iter());
+        for binding in bindings {
+            self.binding_indices_by_action_type
+                .entry(binding.action.as_any().type_id())
+                .or_default()
+                .push(self.bindings.len());
+            self.bindings.push(binding);
+        }
+    }
+
+    fn clear(&mut self) {
+        self.bindings.clear();
+        self.binding_indices_by_action_type.clear();
     }
 }
 
 impl Binding {
     pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
+        Self::load(keystrokes, Box::new(action), context).unwrap()
+    }
+
+    pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
         let context = if let Some(context) = context {
-            Some(ContextPredicate::parse(context).unwrap())
+            Some(ContextPredicate::parse(context)?)
         } else {
             None
         };
 
-        Self {
-            keystrokes: keystrokes
-                .split_whitespace()
-                .map(|key| Keystroke::parse(key).unwrap())
-                .collect(),
-            action: Box::new(action),
+        let keystrokes = keystrokes
+            .split_whitespace()
+            .map(|key| Keystroke::parse(key))
+            .collect::<Result<_>>()?;
+
+        Ok(Self {
+            keystrokes,
+            action,
             context,
-        }
+        })
+    }
+
+    pub fn keystrokes(&self) -> &[Keystroke] {
+        &self.keystrokes
     }
 }
 
@@ -329,7 +384,9 @@ impl ContextPredicate {
 
 #[cfg(test)]
 mod tests {
-    use crate::action;
+    use serde::Deserialize;
+
+    use crate::{actions, impl_actions};
 
     use super::*;
 
@@ -420,29 +477,18 @@ mod tests {
 
     #[test]
     fn test_matcher() -> anyhow::Result<()> {
-        action!(A, &'static str);
-        action!(B);
-        action!(Ab);
-
-        impl PartialEq for A {
-            fn eq(&self, other: &Self) -> bool {
-                self.0 == other.0
-            }
-        }
-        impl Eq for A {}
-        impl Debug for A {
-            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-                write!(f, "A({:?})", &self.0)
-            }
-        }
+        #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+        pub struct A(pub String);
+        impl_actions!(test, [A]);
+        actions!(test, [B, Ab]);
 
         #[derive(Clone, Debug, Eq, PartialEq)]
         struct ActionArg {
             a: &'static str,
         }
 
-        let keymap = Keymap(vec![
-            Binding::new("a", A("x"), Some("a")),
+        let keymap = Keymap::new(vec![
+            Binding::new("a", A("x".to_string()), Some("a")),
             Binding::new("b", B, Some("a")),
             Binding::new("a b", Ab, Some("a || b")),
         ]);
@@ -456,40 +502,54 @@ mod tests {
         let mut matcher = Matcher::new(keymap);
 
         // Basic match
-        assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
+        assert_eq!(
+            downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
+            Some(&A("x".to_string()))
+        );
 
         // Multi-keystroke match
-        assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
-        assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
+        assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
+        assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
 
         // Failed matches don't interfere with matching subsequent keys
-        assert_eq!(matcher.test_keystroke::<A>("x", 1, &ctx_a), None);
-        assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
+        assert!(matcher.test_keystroke("x", 1, &ctx_a).is_none());
+        assert_eq!(
+            downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
+            Some(&A("x".to_string()))
+        );
 
         // Pending keystrokes are cleared when the context changes
-        assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
-        assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B));
+        assert!(&matcher.test_keystroke("a", 1, &ctx_b).is_none());
+        assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_a)), Some(&B));
 
         let mut ctx_c = Context::default();
         ctx_c.set.insert("c".into());
 
         // Pending keystrokes are maintained per-view
-        assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
-        assert_eq!(matcher.test_keystroke::<A>("a", 2, &ctx_c), None);
-        assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
+        assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
+        assert!(matcher.test_keystroke("a", 2, &ctx_c).is_none());
+        assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
 
         Ok(())
     }
 
+    fn downcast<'a, A: Action>(action: &'a Option<Box<dyn Action>>) -> Option<&'a A> {
+        action
+            .as_ref()
+            .and_then(|action| action.as_any().downcast_ref())
+    }
+
     impl Matcher {
-        fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
-        where
-            A: Action + Debug + Eq,
-        {
+        fn test_keystroke(
+            &mut self,
+            keystroke: &str,
+            view_id: usize,
+            cx: &Context,
+        ) -> Option<Box<dyn Action>> {
             if let MatchResult::Action(action) =
                 self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
             {
-                Some(*action.boxed_clone_as_any().downcast().unwrap())
+                Some(action.boxed_clone())
             } else {
                 None
             }

crates/gpui/src/platform.rs 🔗

@@ -15,7 +15,7 @@ use crate::{
         vector::Vector2F,
     },
     text_layout::{LineLayout, RunStyle},
-    AnyAction, ClipboardItem, Menu, Scene,
+    Action, ClipboardItem, Menu, Scene,
 };
 use anyhow::{anyhow, Result};
 use async_task::Runnable;
@@ -56,7 +56,7 @@ pub trait Platform: Send + Sync {
 
     fn local_timezone(&self) -> UtcOffset;
 
-    fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf>;
+    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
     fn app_path(&self) -> Result<PathBuf>;
     fn app_version(&self) -> Result<AppVersion>;
 }
@@ -66,10 +66,10 @@ pub(crate) trait ForegroundPlatform {
     fn on_resign_active(&self, callback: Box<dyn FnMut()>);
     fn on_quit(&self, callback: Box<dyn FnMut()>);
     fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
-    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
+    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
     fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
 
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>);
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
     fn set_menus(&self, menus: Vec<Menu>);
     fn prompt_for_paths(
         &self,
@@ -164,6 +164,12 @@ impl FromStr for AppVersion {
     }
 }
 
+#[derive(Copy, Clone, Debug)]
+pub enum RasterizationOptions {
+    Alpha,
+    Bgra,
+}
+
 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>>;
@@ -183,6 +189,7 @@ pub trait FontSystem: Send + Sync {
         glyph_id: GlyphId,
         subpixel_shift: Vector2F,
         scale_factor: f32,
+        options: RasterizationOptions,
     ) -> Option<(RectI, Vec<u8>)>;
     fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout;
     fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize>;

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

@@ -61,3 +61,20 @@ pub enum Event {
         left_mouse_down: bool,
     },
 }
+
+impl Event {
+    pub fn position(&self) -> Option<Vector2F> {
+        match self {
+            Event::KeyDown { .. } => None,
+            Event::ScrollWheel { position, .. }
+            | Event::LeftMouseDown { position, .. }
+            | Event::LeftMouseUp { position }
+            | Event::LeftMouseDragged { position }
+            | Event::RightMouseDown { position, .. }
+            | Event::RightMouseUp { position }
+            | Event::NavigateMouseDown { position, .. }
+            | Event::NavigateMouseUp { position, .. }
+            | Event::MouseMoved { position, .. } => Some(*position),
+        }
+    }
+}

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

@@ -4,6 +4,7 @@ use crate::geometry::{
 };
 use etagere::BucketedAtlasAllocator;
 use foreign_types::ForeignType;
+use log::warn;
 use metal::{Device, TextureDescriptor};
 use objc::{msg_send, sel, sel_impl};
 
@@ -40,31 +41,40 @@ impl AtlasAllocator {
         )
     }
 
-    pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
-        let (alloc_id, origin) = self
+    pub fn allocate(&mut self, requested_size: Vector2I) -> Option<(AllocId, Vector2I)> {
+        let allocation = self
             .atlases
             .last_mut()
             .unwrap()
             .allocate(requested_size)
-            .unwrap_or_else(|| {
+            .or_else(|| {
                 let mut atlas = self.new_atlas(requested_size);
-                let (id, origin) = atlas.allocate(requested_size).unwrap();
+                let (id, origin) = atlas.allocate(requested_size)?;
                 self.atlases.push(atlas);
-                (id, origin)
+                Some((id, origin))
             });
 
+        if allocation.is_none() {
+            warn!(
+                "allocation of size {:?} could not be created",
+                requested_size,
+            );
+        }
+
+        let (alloc_id, origin) = allocation?;
+
         let id = AllocId {
             atlas_id: self.atlases.len() - 1,
             alloc_id,
         };
-        (id, origin)
+        Some((id, origin))
     }
 
-    pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
-        let (alloc_id, origin) = self.allocate(size);
+    pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> Option<(AllocId, RectI)> {
+        let (alloc_id, origin) = self.allocate(size)?;
         let bounds = RectI::new(origin, size);
         self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
-        (alloc_id, bounds)
+        Some((alloc_id, bounds))
     }
 
     pub fn deallocate(&mut self, id: AllocId) {

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

@@ -3,26 +3,27 @@ use crate::{
     geometry::{
         rect::{RectF, RectI},
         transform2d::Transform2F,
-        vector::{vec2f, vec2i, Vector2F},
+        vector::{vec2f, Vector2F},
     },
-    platform,
+    platform::{self, RasterizationOptions},
     text_layout::{Glyph, LineLayout, Run, RunStyle},
 };
 use cocoa::appkit::{CGFloat, CGPoint};
+use collections::HashMap;
 use core_foundation::{
     array::CFIndex,
     attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
     base::{CFRange, TCFType},
-    number::CFNumber,
     string::CFString,
 };
 use core_graphics::{
-    base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
+    base::{kCGImageAlphaPremultipliedLast, CGGlyph},
+    color_space::CGColorSpace,
+    context::CGContext,
 };
-use core_text::{line::CTLine, string_attributes::kCTFontAttributeName};
+use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
 use font_kit::{
-    canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource,
-    sources::mem::MemSource,
+    handle::Handle, hinting::HintingOptions, source::SystemSource, sources::mem::MemSource,
 };
 use parking_lot::RwLock;
 use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
@@ -36,6 +37,8 @@ struct FontSystemState {
     memory_source: MemSource,
     system_source: SystemSource,
     fonts: Vec<font_kit::font::Font>,
+    font_ids_by_postscript_name: HashMap<String, FontId>,
+    postscript_names_by_font_id: HashMap<FontId, String>,
 }
 
 impl FontSystem {
@@ -44,6 +47,8 @@ impl FontSystem {
             memory_source: MemSource::empty(),
             system_source: SystemSource::new(),
             fonts: Vec::new(),
+            font_ids_by_postscript_name: Default::default(),
+            postscript_names_by_font_id: Default::default(),
         }))
     }
 }
@@ -84,14 +89,20 @@ impl platform::FontSystem for FontSystem {
         glyph_id: GlyphId,
         subpixel_shift: Vector2F,
         scale_factor: f32,
+        options: RasterizationOptions,
     ) -> Option<(RectI, Vec<u8>)> {
-        self.0
-            .read()
-            .rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor)
+        self.0.read().rasterize_glyph(
+            font_id,
+            font_size,
+            glyph_id,
+            subpixel_shift,
+            scale_factor,
+            options,
+        )
     }
 
     fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
-        self.0.read().layout_line(text, font_size, runs)
+        self.0.write().layout_line(text, font_size, runs)
     }
 
     fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize> {
@@ -118,7 +129,13 @@ impl FontSystemState {
             .or_else(|_| self.system_source.select_family_by_name(name))?;
         for font in family.fonts() {
             let font = font.load()?;
-            font_ids.push(FontId(self.fonts.len()));
+            let font_id = FontId(self.fonts.len());
+            font_ids.push(font_id);
+            let postscript_name = font.postscript_name().unwrap();
+            self.font_ids_by_postscript_name
+                .insert(postscript_name.clone(), font_id);
+            self.postscript_names_by_font_id
+                .insert(font_id, postscript_name);
             self.fonts.push(font);
         }
         Ok(font_ids)
@@ -149,6 +166,32 @@ impl FontSystemState {
         self.fonts[font_id.0].glyph_for_char(ch)
     }
 
+    fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
+        let postscript_name = requested_font.postscript_name();
+        if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
+            *font_id
+        } else {
+            let font_id = FontId(self.fonts.len());
+            self.font_ids_by_postscript_name
+                .insert(postscript_name.clone(), font_id);
+            self.postscript_names_by_font_id
+                .insert(font_id, postscript_name);
+            self.fonts
+                .push(font_kit::font::Font::from_core_graphics_font(
+                    requested_font.copy_to_CGFont(),
+                ));
+            font_id
+        }
+    }
+
+    fn is_emoji(&self, font_id: FontId) -> bool {
+        self.postscript_names_by_font_id
+            .get(&font_id)
+            .map_or(false, |postscript_name| {
+                postscript_name == "AppleColorEmoji"
+            })
+    }
+
     fn rasterize_glyph(
         &self,
         font_id: FontId,
@@ -156,65 +199,103 @@ impl FontSystemState {
         glyph_id: GlyphId,
         subpixel_shift: Vector2F,
         scale_factor: f32,
+        options: RasterizationOptions,
     ) -> Option<(RectI, Vec<u8>)> {
         let font = &self.fonts[font_id.0];
         let scale = Transform2F::from_scale(scale_factor);
-        let bounds = font
+        let glyph_bounds = font
             .raster_bounds(
                 glyph_id,
                 font_size,
                 scale,
                 HintingOptions::None,
-                RasterizationOptions::GrayscaleAa,
+                font_kit::canvas::RasterizationOptions::GrayscaleAa,
             )
             .ok()?;
 
-        if bounds.width() == 0 || bounds.height() == 0 {
+        if glyph_bounds.width() == 0 || glyph_bounds.height() == 0 {
             None
         } else {
             // Make room for subpixel variants.
-            let bounds = RectI::new(bounds.origin(), bounds.size() + vec2i(1, 1));
-            let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
-            let cx = CGContext::create_bitmap_context(
-                Some(pixels.as_mut_ptr() as *mut _),
-                bounds.width() as usize,
-                bounds.height() as usize,
-                8,
-                bounds.width() as usize,
-                &CGColorSpace::create_device_gray(),
-                kCGImageAlphaOnly,
+            let subpixel_padding = subpixel_shift.ceil().to_i32();
+            let cx_bounds = RectI::new(
+                glyph_bounds.origin(),
+                glyph_bounds.size() + subpixel_padding,
             );
 
+            let mut bytes;
+            let cx;
+            match options {
+                RasterizationOptions::Alpha => {
+                    bytes = vec![0; cx_bounds.width() as usize * cx_bounds.height() as usize];
+                    cx = CGContext::create_bitmap_context(
+                        Some(bytes.as_mut_ptr() as *mut _),
+                        cx_bounds.width() as usize,
+                        cx_bounds.height() as usize,
+                        8,
+                        cx_bounds.width() as usize,
+                        &CGColorSpace::create_device_gray(),
+                        kCGImageAlphaOnly,
+                    );
+                }
+                RasterizationOptions::Bgra => {
+                    bytes = vec![0; cx_bounds.width() as usize * 4 * cx_bounds.height() as usize];
+                    cx = CGContext::create_bitmap_context(
+                        Some(bytes.as_mut_ptr() as *mut _),
+                        cx_bounds.width() as usize,
+                        cx_bounds.height() as usize,
+                        8,
+                        cx_bounds.width() as usize * 4,
+                        &CGColorSpace::create_device_rgb(),
+                        kCGImageAlphaPremultipliedLast,
+                    );
+                }
+            }
+
             // Move the origin to bottom left and account for scaling, this
             // makes drawing text consistent with the font-kit's raster_bounds.
-            cx.translate(0.0, bounds.height() as CGFloat);
-            let transform = scale.translate(-bounds.origin().to_f32());
-            cx.set_text_matrix(&CGAffineTransform {
-                a: transform.matrix.m11() as CGFloat,
-                b: -transform.matrix.m21() as CGFloat,
-                c: -transform.matrix.m12() as CGFloat,
-                d: transform.matrix.m22() as CGFloat,
-                tx: transform.vector.x() as CGFloat,
-                ty: -transform.vector.y() as CGFloat,
-            });
-
-            cx.set_font(&font.native_font().copy_to_CGFont());
-            cx.set_font_size(font_size as CGFloat);
-            cx.show_glyphs_at_positions(
-                &[glyph_id as CGGlyph],
-                &[CGPoint::new(
-                    (subpixel_shift.x() / scale_factor) as CGFloat,
-                    (subpixel_shift.y() / scale_factor) as CGFloat,
-                )],
+            cx.translate(
+                -glyph_bounds.origin_x() as CGFloat,
+                (glyph_bounds.origin_y() + glyph_bounds.height()) as CGFloat,
             );
+            cx.scale(scale_factor as CGFloat, scale_factor as CGFloat);
+
+            cx.set_allows_font_subpixel_positioning(true);
+            cx.set_should_subpixel_position_fonts(true);
+            cx.set_allows_font_subpixel_quantization(false);
+            cx.set_should_subpixel_quantize_fonts(false);
+            font.native_font()
+                .clone_with_font_size(font_size as CGFloat)
+                .draw_glyphs(
+                    &[glyph_id as CGGlyph],
+                    &[CGPoint::new(
+                        (subpixel_shift.x() / scale_factor) as CGFloat,
+                        (subpixel_shift.y() / scale_factor) as CGFloat,
+                    )],
+                    cx,
+                );
+
+            if let RasterizationOptions::Bgra = options {
+                // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
+                for pixel in bytes.chunks_exact_mut(4) {
+                    pixel.swap(0, 2);
+                    let a = pixel[3] as f32 / 255.;
+                    pixel[0] = (pixel[0] as f32 / a) as u8;
+                    pixel[1] = (pixel[1] as f32 / a) as u8;
+                    pixel[2] = (pixel[2] as f32 / a) as u8;
+                }
+            }
 
-            Some((bounds, pixels))
+            Some((cx_bounds, bytes))
         }
     }
 
-    fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
-        let font_id_attr_name = CFString::from_static_string("zed_font_id");
-
+    fn layout_line(
+        &mut self,
+        text: &str,
+        font_size: f32,
+        runs: &[(usize, RunStyle)],
+    ) -> LineLayout {
         // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
         let mut string = CFMutableAttributedString::new();
         {
@@ -264,11 +345,6 @@ impl FontSystemState {
                         kCTFontAttributeName,
                         &font.native_font().clone_with_font_size(font_size as f64),
                     );
-                    string.set_attribute(
-                        cf_range,
-                        font_id_attr_name.as_concrete_TypeRef(),
-                        &CFNumber::from(font_id.0 as i64),
-                    );
                 }
 
                 if utf16_end == utf16_line_len {
@@ -282,15 +358,14 @@ impl FontSystemState {
 
         let mut runs = Vec::new();
         for run in line.glyph_runs().into_iter() {
-            let font_id = FontId(
-                run.attributes()
+            let attributes = run.attributes().unwrap();
+            let font = unsafe {
+                attributes
+                    .get(kCTFontAttributeName)
+                    .downcast::<CTFont>()
                     .unwrap()
-                    .get(&font_id_attr_name)
-                    .downcast::<CFNumber>()
-                    .unwrap()
-                    .to_i64()
-                    .unwrap() as usize,
-            );
+            };
+            let font_id = self.id_for_native_font(font);
 
             let mut ix_converter = StringIndexConverter::new(text);
             let mut glyphs = Vec::new();
@@ -306,6 +381,7 @@ impl FontSystemState {
                     id: *glyph_id as GlyphId,
                     position: vec2f(position.x as f32, position.y as f32),
                     index: ix_converter.utf8_ix,
+                    is_emoji: self.is_emoji(font_id),
                 });
             }
 
@@ -510,7 +586,14 @@ mod tests {
         for i in 0..VARIANTS {
             let variant = i as f32 / VARIANTS as f32;
             let (bounds, bytes) = fonts
-                .rasterize_glyph(font_id, 16.0, glyph_id, vec2f(variant, variant), 2.)
+                .rasterize_glyph(
+                    font_id,
+                    16.0,
+                    glyph_id,
+                    vec2f(variant, variant),
+                    2.,
+                    RasterizationOptions::Alpha,
+                )
                 .unwrap();
 
             let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);

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

@@ -1,20 +1,39 @@
-use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
-
 use super::atlas::{AllocId, AtlasAllocator};
 use crate::{
+    fonts::{FontId, GlyphId},
     geometry::{rect::RectI, vector::Vector2I},
-    ImageData,
+    platform::RasterizationOptions,
+    scene::ImageGlyph,
+    FontSystem, ImageData,
 };
-use std::{collections::HashMap, mem};
+use anyhow::anyhow;
+use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
+use ordered_float::OrderedFloat;
+use std::{collections::HashMap, mem, sync::Arc};
+
+#[derive(Hash, Eq, PartialEq)]
+struct GlyphDescriptor {
+    font_id: FontId,
+    font_size: OrderedFloat<f32>,
+    glyph_id: GlyphId,
+}
 
 pub struct ImageCache {
     prev_frame: HashMap<usize, (AllocId, RectI)>,
     curr_frame: HashMap<usize, (AllocId, RectI)>,
+    image_glyphs: HashMap<GlyphDescriptor, Option<(AllocId, RectI, Vector2I)>>,
     atlases: AtlasAllocator,
+    scale_factor: f32,
+    fonts: Arc<dyn FontSystem>,
 }
 
 impl ImageCache {
-    pub fn new(device: metal::Device, size: Vector2I) -> Self {
+    pub fn new(
+        device: metal::Device,
+        size: Vector2I,
+        scale_factor: f32,
+        fonts: Arc<dyn FontSystem>,
+    ) -> Self {
         let descriptor = TextureDescriptor::new();
         descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
         descriptor.set_width(size.x() as u64);
@@ -22,7 +41,21 @@ impl ImageCache {
         Self {
             prev_frame: Default::default(),
             curr_frame: Default::default(),
+            image_glyphs: Default::default(),
             atlases: AtlasAllocator::new(device, descriptor),
+            scale_factor,
+            fonts,
+        }
+    }
+
+    pub fn set_scale_factor(&mut self, scale_factor: f32) {
+        if scale_factor != self.scale_factor {
+            self.scale_factor = scale_factor;
+            for (_, glyph) in self.image_glyphs.drain() {
+                if let Some((alloc_id, _, _)) = glyph {
+                    self.atlases.deallocate(alloc_id);
+                }
+            }
         }
     }
 
@@ -31,11 +64,44 @@ impl ImageCache {
             .prev_frame
             .remove(&image.id)
             .or_else(|| self.curr_frame.get(&image.id).copied())
-            .unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes()));
+            .or_else(|| self.atlases.upload(image.size(), image.as_bytes()))
+            .ok_or_else(|| anyhow!("could not upload image of size {:?}", image.size()))
+            .unwrap();
         self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
         (alloc_id, atlas_bounds)
     }
 
+    pub fn render_glyph(&mut self, image_glyph: &ImageGlyph) -> Option<(AllocId, RectI, Vector2I)> {
+        *self
+            .image_glyphs
+            .entry(GlyphDescriptor {
+                font_id: image_glyph.font_id,
+                font_size: OrderedFloat(image_glyph.font_size),
+                glyph_id: image_glyph.id,
+            })
+            .or_insert_with(|| {
+                let (glyph_bounds, bytes) = self.fonts.rasterize_glyph(
+                    image_glyph.font_id,
+                    image_glyph.font_size,
+                    image_glyph.id,
+                    Default::default(),
+                    self.scale_factor,
+                    RasterizationOptions::Bgra,
+                )?;
+                let (alloc_id, atlas_bounds) = self
+                    .atlases
+                    .upload(glyph_bounds.size(), &bytes)
+                    .ok_or_else(|| {
+                        anyhow!(
+                            "could not upload image glyph of size {:?}",
+                            glyph_bounds.size()
+                        )
+                    })
+                    .unwrap();
+                Some((alloc_id, atlas_bounds, glyph_bounds.origin()))
+            })
+    }
+
     pub fn finish_frame(&mut self) {
         mem::swap(&mut self.prev_frame, &mut self.curr_frame);
         for (_, (id, _)) in self.curr_frame.drain() {

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

@@ -3,7 +3,7 @@ use crate::{
     executor,
     keymap::Keystroke,
     platform::{self, CursorStyle},
-    AnyAction, ClipboardItem, Event, Menu, MenuItem,
+    Action, ClipboardItem, Event, Menu, MenuItem,
 };
 use anyhow::{anyhow, Result};
 use block::ConcreteBlock;
@@ -38,8 +38,8 @@ use ptr::null_mut;
 use std::{
     cell::{Cell, RefCell},
     convert::TryInto,
-    ffi::{c_void, CStr},
-    os::raw::c_char,
+    ffi::{c_void, CStr, OsStr},
+    os::{raw::c_char, unix::ffi::OsStrExt},
     path::{Path, PathBuf},
     ptr,
     rc::Rc,
@@ -91,8 +91,8 @@ unsafe fn build_classes() {
             handle_menu_item as extern "C" fn(&mut Object, Sel, id),
         );
         decl.add_method(
-            sel!(application:openFiles:),
-            open_files as extern "C" fn(&mut Object, Sel, id, id),
+            sel!(application:openURLs:),
+            open_urls as extern "C" fn(&mut Object, Sel, id, id),
         );
         decl.register()
     }
@@ -107,10 +107,10 @@ pub struct MacForegroundPlatformState {
     resign_active: Option<Box<dyn FnMut()>>,
     quit: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
-    menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
-    open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
+    menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
-    menu_actions: Vec<Box<dyn AnyAction>>,
+    menu_actions: Vec<Box<dyn Action>>,
 }
 
 impl MacForegroundPlatform {
@@ -210,8 +210,8 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().event = Some(callback);
     }
 
-    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
-        self.0.borrow_mut().open_files = Some(callback);
+    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
+        self.0.borrow_mut().open_urls = Some(callback);
     }
 
     fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
@@ -235,7 +235,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         }
     }
 
-    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
+    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
         self.0.borrow_mut().menu_command = Some(callback);
     }
 
@@ -265,10 +265,9 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
                     for i in 0..urls.count() {
                         let url = urls.objectAtIndex(i);
                         if url.isFileURL() == YES {
-                            let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
-                                .to_string_lossy()
-                                .to_string();
-                            result.push(PathBuf::from(path));
+                            if let Ok(path) = ns_url_to_path(url) {
+                                result.push(path)
+                            }
                         }
                     }
                     Some(result)
@@ -296,19 +295,13 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
             let (done_tx, done_rx) = oneshot::channel();
             let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |response: NSModalResponse| {
-                let result = if response == NSModalResponse::NSModalResponseOk {
+                let mut result = None;
+                if response == NSModalResponse::NSModalResponseOk {
                     let url = panel.URL();
                     if url.isFileURL() == YES {
-                        let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
-                            .to_string_lossy()
-                            .to_string();
-                        Some(PathBuf::from(path))
-                    } else {
-                        None
+                        result = ns_url_to_path(panel.URL()).ok()
                     }
-                } else {
-                    None
-                };
+                }
 
                 if let Some(mut done_tx) = done_tx.take() {
                     let _ = postage::sink::Sink::try_send(&mut done_tx, result);
@@ -603,19 +596,18 @@ impl platform::Platform for MacPlatform {
         }
     }
 
-    fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf> {
+    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
         unsafe {
             let bundle: id = NSBundle::mainBundle();
             if bundle.is_null() {
                 Err(anyhow!("app is not running inside a bundle"))
             } else {
-                let name = name.map_or(nil, |name| ns_string(name));
-                let extension = extension.map_or(nil, |extension| ns_string(extension));
-                let path: id = msg_send![bundle, pathForResource: name ofType: extension];
-                if path.is_null() {
-                    Err(anyhow!("resource could not be found"))
+                let name = ns_string(name);
+                let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
+                if url.is_null() {
+                    Err(anyhow!("resource not found"))
                 } else {
-                    Ok(path_from_objc(path))
+                    ns_url_to_path(url)
                 }
             }
         }
@@ -710,14 +702,14 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
     }
 }
 
-extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
-    let paths = unsafe {
-        (0..paths.count())
+extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
+    let urls = unsafe {
+        (0..urls.count())
             .into_iter()
             .filter_map(|i| {
-                let path = paths.objectAtIndex(i);
-                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
-                    Ok(string) => Some(PathBuf::from(string)),
+                let path = urls.objectAtIndex(i);
+                match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() {
+                    Ok(string) => Some(string.to_string()),
                     Err(err) => {
                         log::error!("error converting path to string: {}", err);
                         None
@@ -727,8 +719,8 @@ extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
             .collect::<Vec<_>>()
     };
     let platform = unsafe { get_foreground_platform(this) };
-    if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() {
-        callback(paths);
+    if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() {
+        callback(urls);
     }
 }
 
@@ -751,6 +743,20 @@ unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }
 
+unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
+    let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+    if path.is_null() {
+        Err(anyhow!(
+            "url is not a file path: {}",
+            CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+        ))
+    } else {
+        Ok(PathBuf::from(OsStr::from_bytes(
+            CStr::from_ptr(path).to_bytes(),
+        )))
+    }
+}
+
 mod security {
     #![allow(non_upper_case_globals)]
     use super::*;

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

@@ -6,9 +6,10 @@ use crate::{
         vector::{vec2f, vec2i, Vector2F},
     },
     platform,
-    scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow, Underline},
+    scene::{Glyph, Icon, Image, ImageGlyph, Layer, Quad, Scene, Shadow, Underline},
 };
 use cocoa::foundation::NSUInteger;
+use log::warn;
 use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
 use shaders::ToFloat2 as _;
 use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
@@ -66,8 +67,13 @@ impl Renderer {
             MTLResourceOptions::StorageModeManaged,
         );
 
-        let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
-        let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
+        let sprite_cache = SpriteCache::new(
+            device.clone(),
+            vec2i(1024, 768),
+            scale_factor,
+            fonts.clone(),
+        );
+        let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
         let path_atlases =
             AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
         let quad_pipeline_state = build_pipeline_state(
@@ -140,6 +146,9 @@ impl Renderer {
         command_buffer: &metal::CommandBufferRef,
         output: &metal::TextureRef,
     ) {
+        self.sprite_cache.set_scale_factor(scene.scale_factor());
+        self.image_cache.set_scale_factor(scene.scale_factor());
+
         let mut offset = 0;
 
         let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
@@ -172,7 +181,14 @@ impl Renderer {
             for path in layer.paths() {
                 let origin = path.bounds.origin() * scene.scale_factor();
                 let size = (path.bounds.size() * scene.scale_factor()).ceil();
-                let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
+
+                let path_allocation = self.path_atlases.allocate(size.to_i32());
+                if path_allocation.is_none() {
+                    // Path size was likely zero.
+                    warn!("could not allocate path texture of size {:?}", size);
+                    continue;
+                }
+                let (alloc_id, atlas_origin) = path_allocation.unwrap();
                 let atlas_origin = atlas_origin.to_f32();
                 sprites.push(PathSprite {
                     layer_id,
@@ -351,6 +367,7 @@ impl Renderer {
             );
             self.render_images(
                 layer.images(),
+                layer.image_glyphs(),
                 scale_factor,
                 offset,
                 drawable_size,
@@ -533,8 +550,6 @@ impl Renderer {
             return;
         }
 
-        self.sprite_cache.set_scale_factor(scale_factor);
-
         let mut sprites_by_atlas = HashMap::new();
 
         for glyph in glyphs {
@@ -569,6 +584,10 @@ impl Renderer {
             let sprite =
                 self.sprite_cache
                     .render_icon(source_size, icon.path.clone(), icon.svg.clone());
+            if sprite.is_none() {
+                continue;
+            }
+            let sprite = sprite.unwrap();
 
             sprites_by_atlas
                 .entry(sprite.atlas_id)
@@ -641,12 +660,13 @@ impl Renderer {
     fn render_images(
         &mut self,
         images: &[Image],
+        image_glyphs: &[ImageGlyph],
         scale_factor: f32,
         offset: &mut usize,
         drawable_size: Vector2F,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) {
-        if images.is_empty() {
+        if images.is_empty() && image_glyphs.is_empty() {
             return;
         }
 
@@ -674,6 +694,31 @@ impl Renderer {
                 });
         }
 
+        for image_glyph in image_glyphs {
+            let origin = (image_glyph.origin * scale_factor).floor();
+            if let Some((alloc_id, atlas_bounds, glyph_origin)) =
+                self.image_cache.render_glyph(image_glyph)
+            {
+                images_by_atlas
+                    .entry(alloc_id.atlas_id)
+                    .or_insert_with(Vec::new)
+                    .push(shaders::GPUIImage {
+                        origin: (origin + glyph_origin.to_f32()).to_float2(),
+                        target_size: atlas_bounds.size().to_float2(),
+                        source_size: atlas_bounds.size().to_float2(),
+                        atlas_origin: atlas_bounds.origin().to_float2(),
+                        border_top: 0.,
+                        border_right: 0.,
+                        border_bottom: 0.,
+                        border_left: 0.,
+                        border_color: Default::default(),
+                        corner_radius: 0.,
+                    });
+            } else {
+                log::warn!("could not render glyph with id {}", image_glyph.id);
+            }
+        }
+
         command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
         command_encoder.set_vertex_buffer(
             shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,

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

@@ -2,8 +2,9 @@ use super::atlas::AtlasAllocator;
 use crate::{
     fonts::{FontId, GlyphId},
     geometry::vector::{vec2f, Vector2F, Vector2I},
-    platform,
+    platform::{self, RasterizationOptions},
 };
+use collections::hash_map::Entry;
 use metal::{MTLPixelFormat, TextureDescriptor};
 use ordered_float::OrderedFloat;
 use std::{borrow::Cow, collections::HashMap, sync::Arc};
@@ -112,9 +113,12 @@ impl SpriteCache {
                     glyph_id,
                     subpixel_shift,
                     scale_factor,
+                    RasterizationOptions::Alpha,
                 )?;
 
-                let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask);
+                let (alloc_id, atlas_bounds) = atlases
+                    .upload(glyph_bounds.size(), &mask)
+                    .expect("could not upload glyph");
                 Some(GlyphSprite {
                     atlas_id: alloc_id.atlas_id,
                     atlas_origin: atlas_bounds.origin(),
@@ -130,31 +134,31 @@ impl SpriteCache {
         size: Vector2I,
         path: Cow<'static, str>,
         svg: usvg::Tree,
-    ) -> IconSprite {
+    ) -> Option<IconSprite> {
         let atlases = &mut self.atlases;
-        self.icons
-            .entry(IconDescriptor {
-                path,
-                width: size.x(),
-                height: size.y(),
-            })
-            .or_insert_with(|| {
-                let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32).unwrap();
+        match self.icons.entry(IconDescriptor {
+            path,
+            width: size.x(),
+            height: size.y(),
+        }) {
+            Entry::Occupied(entry) => Some(entry.get().clone()),
+            Entry::Vacant(entry) => {
+                let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32)?;
                 resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
                 let mask = pixmap
                     .pixels()
                     .iter()
                     .map(|a| a.alpha())
                     .collect::<Vec<_>>();
-
-                let (alloc_id, atlas_bounds) = atlases.upload(size, &mask);
-                IconSprite {
+                let (alloc_id, atlas_bounds) = atlases.upload(size, &mask)?;
+                let icon_sprite = IconSprite {
                     atlas_id: alloc_id.atlas_id,
                     atlas_origin: atlas_bounds.origin(),
                     size,
-                }
-            })
-            .clone()
+                };
+                Some(entry.insert(icon_sprite).clone())
+            }
+        }
     }
 
     pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {

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

@@ -361,9 +361,10 @@ impl platform::Window for Window {
                 }
             });
             let block = block.copy();
+            let native_window = self.0.borrow().native_window;
             let _: () = msg_send![
                 alert,
-                beginSheetModalForWindow: self.0.borrow().native_window
+                beginSheetModalForWindow: native_window
                 completionHandler: block
             ];
             done_rx

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

@@ -1,7 +1,7 @@
 use super::{AppVersion, CursorStyle, WindowBounds};
 use crate::{
     geometry::vector::{vec2f, Vector2F},
-    AnyAction, ClipboardItem,
+    Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
 use parking_lot::Mutex;
@@ -66,13 +66,13 @@ impl super::ForegroundPlatform for ForegroundPlatform {
 
     fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
 
-    fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
+    fn on_open_urls(&self, _: Box<dyn FnMut(Vec<String>)>) {}
 
     fn run(&self, _on_finish_launching: Box<dyn FnOnce() -> ()>) {
         unimplemented!()
     }
 
-    fn on_menu_command(&self, _: Box<dyn FnMut(&dyn AnyAction)>) {}
+    fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
 
     fn set_menus(&self, _: Vec<crate::Menu>) {}
 
@@ -161,7 +161,7 @@ impl super::Platform for Platform {
         UtcOffset::UTC
     }
 
-    fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result<PathBuf> {
+    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
         Err(anyhow!("app not running inside a bundle"))
     }
 

crates/gpui/src/presenter.rs 🔗

@@ -6,7 +6,7 @@ use crate::{
     json::{self, ToJson},
     platform::Event,
     text_layout::TextLayoutCache,
-    Action, AnyAction, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
+    Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
     ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene,
     UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
@@ -51,15 +51,21 @@ impl Presenter {
     }
 
     pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
+        if let Some(view_id) = app.focused_view_id(self.window_id) {
+            self.dispatch_path_from(view_id)
+        } else {
+            Vec::new()
+        }
+    }
+
+    pub(crate) fn dispatch_path_from(&self, mut view_id: usize) -> Vec<usize> {
         let mut path = Vec::new();
-        if let Some(mut view_id) = app.focused_view_id(self.window_id) {
-            path.push(view_id);
-            while let Some(parent_id) = self.parents.get(&view_id).copied() {
-                path.push(parent_id);
-                view_id = parent_id;
-            }
-            path.reverse();
+        path.push(view_id);
+        while let Some(parent_id) = self.parents.get(&view_id).copied() {
+            path.push(parent_id);
+            view_id = parent_id;
         }
+        path.reverse();
         path
     }
 
@@ -209,21 +215,24 @@ impl Presenter {
     }
 
     pub fn debug_elements(&self, cx: &AppContext) -> Option<json::Value> {
-        cx.root_view_id(self.window_id)
-            .and_then(|root_view_id| self.rendered_views.get(&root_view_id))
-            .map(|root_element| {
-                root_element.debug(&DebugContext {
-                    rendered_views: &self.rendered_views,
-                    font_cache: &self.font_cache,
-                    app: cx,
+        let view = cx.root_view(self.window_id)?;
+        Some(json!({
+            "root_view": view.debug_json(cx),
+            "root_element": self.rendered_views.get(&view.id())
+                .map(|root_element| {
+                    root_element.debug(&DebugContext {
+                        rendered_views: &self.rendered_views,
+                        font_cache: &self.font_cache,
+                        app: cx,
+                    })
                 })
-            })
+        }))
     }
 }
 
 pub struct DispatchDirective {
     pub path: Vec<usize>,
-    pub action: Box<dyn AnyAction>,
+    pub action: Box<dyn Action>,
 }
 
 pub struct LayoutContext<'a> {
@@ -535,6 +544,7 @@ impl Element for ChildView {
         &mut self,
         event: &Event,
         _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
@@ -553,6 +563,7 @@ impl Element for ChildView {
             "type": "ChildView",
             "view_id": self.view.id(),
             "bounds": bounds.to_json(),
+            "view": self.view.debug_json(cx.app),
             "child": if let Some(view) = cx.rendered_views.get(&self.view.id()) {
                 view.debug(cx)
             } else {

crates/gpui/src/scene.rs 🔗

@@ -29,6 +29,7 @@ pub struct Layer {
     images: Vec<Image>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
+    image_glyphs: Vec<ImageGlyph>,
     icons: Vec<Icon>,
     paths: Vec<Path>,
 }
@@ -58,6 +59,14 @@ pub struct Glyph {
     pub color: Color,
 }
 
+#[derive(Debug)]
+pub struct ImageGlyph {
+    pub font_id: FontId,
+    pub font_size: f32,
+    pub id: GlyphId,
+    pub origin: Vector2F,
+}
+
 pub struct Icon {
     pub bounds: RectF,
     pub svg: usvg::Tree,
@@ -204,6 +213,10 @@ impl Scene {
         self.active_layer().push_glyph(glyph)
     }
 
+    pub fn push_image_glyph(&mut self, image_glyph: ImageGlyph) {
+        self.active_layer().push_image_glyph(image_glyph)
+    }
+
     pub fn push_icon(&mut self, icon: Icon) {
         self.active_layer().push_icon(icon)
     }
@@ -264,13 +277,14 @@ impl Layer {
     pub fn new(clip_bounds: Option<RectF>) -> Self {
         Self {
             clip_bounds,
-            quads: Vec::new(),
-            underlines: Vec::new(),
-            images: Vec::new(),
-            shadows: Vec::new(),
-            glyphs: Vec::new(),
-            icons: Vec::new(),
-            paths: Vec::new(),
+            quads: Default::default(),
+            underlines: Default::default(),
+            images: Default::default(),
+            shadows: Default::default(),
+            image_glyphs: Default::default(),
+            glyphs: Default::default(),
+            icons: Default::default(),
+            paths: Default::default(),
         }
     }
 
@@ -318,6 +332,14 @@ impl Layer {
         self.shadows.as_slice()
     }
 
+    fn push_image_glyph(&mut self, glyph: ImageGlyph) {
+        self.image_glyphs.push(glyph);
+    }
+
+    pub fn image_glyphs(&self) -> &[ImageGlyph] {
+        self.image_glyphs.as_slice()
+    }
+
     fn push_glyph(&mut self, glyph: Glyph) {
         self.glyphs.push(glyph);
     }

crates/gpui/src/text_layout.rs 🔗

@@ -191,6 +191,7 @@ pub struct Glyph {
     pub id: GlyphId,
     pub position: Vector2F,
     pub index: usize,
+    pub is_emoji: bool,
 }
 
 impl Line {
@@ -323,13 +324,22 @@ impl Line {
                     });
                 }
 
-                cx.scene.push_glyph(scene::Glyph {
-                    font_id: run.font_id,
-                    font_size: self.layout.font_size,
-                    id: glyph.id,
-                    origin: glyph_origin,
-                    color,
-                });
+                if glyph.is_emoji {
+                    cx.scene.push_image_glyph(scene::ImageGlyph {
+                        font_id: run.font_id,
+                        font_size: self.layout.font_size,
+                        id: glyph.id,
+                        origin: glyph_origin,
+                    });
+                } else {
+                    cx.scene.push_glyph(scene::Glyph {
+                        font_id: run.font_id,
+                        font_size: self.layout.font_size,
+                        id: glyph.id,
+                        origin: glyph_origin,
+                        color,
+                    });
+                }
             }
         }
 
@@ -389,13 +399,22 @@ impl Line {
                         .bounding_box(run.font_id, self.layout.font_size),
                 );
                 if glyph_bounds.intersects(visible_bounds) {
-                    cx.scene.push_glyph(scene::Glyph {
-                        font_id: run.font_id,
-                        font_size: self.layout.font_size,
-                        id: glyph.id,
-                        origin: glyph_bounds.origin() + baseline_origin,
-                        color,
-                    });
+                    if glyph.is_emoji {
+                        cx.scene.push_image_glyph(scene::ImageGlyph {
+                            font_id: run.font_id,
+                            font_size: self.layout.font_size,
+                            id: glyph.id,
+                            origin: glyph_bounds.origin() + baseline_origin,
+                        });
+                    } else {
+                        cx.scene.push_glyph(scene::Glyph {
+                            font_id: run.font_id,
+                            font_size: self.layout.font_size,
+                            id: glyph.id,
+                            origin: glyph_bounds.origin() + baseline_origin,
+                            color,
+                        });
+                    }
                 }
             }
         }

crates/gpui/src/views/select.rs 🔗

@@ -1,6 +1,8 @@
+use serde::Deserialize;
+
 use crate::{
-    action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext,
-    WeakViewHandle,
+    actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View,
+    ViewContext, WeakViewHandle,
 };
 
 pub struct Select {
@@ -25,8 +27,11 @@ pub enum ItemType {
     Unselected,
 }
 
-action!(ToggleSelect);
-action!(SelectItem, usize);
+#[derive(Clone, Deserialize)]
+pub struct SelectItem(pub usize);
+
+actions!(select, [ToggleSelect]);
+impl_actions!(select, [SelectItem]);
 
 pub enum Event {}
 

crates/gpui_macros/src/gpui_macros.rs 🔗

@@ -75,68 +75,65 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
                     match last_segment.map(|s| s.ident.to_string()).as_deref() {
                         Some("StdRng") => {
                             inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
+                            continue;
                         }
                         Some("bool") => {
                             inner_fn_args.extend(quote!(is_last_iteration,));
+                            continue;
                         }
-                        _ => {
-                            return TokenStream::from(
-                                syn::Error::new_spanned(arg, "invalid argument")
-                                    .into_compile_error(),
-                            )
-                        }
-                    }
-                } else if let Type::Reference(ty) = &*arg.ty {
-                    match &*ty.elem {
-                        Type::Path(ty) => {
-                            let last_segment = ty.path.segments.last();
-                            match last_segment.map(|s| s.ident.to_string()).as_deref() {
-                                Some("TestAppContext") => {
-                                    let first_entity_id = ix * 100_000;
-                                    let cx_varname = format_ident!("cx_{}", ix);
-                                    cx_vars.extend(quote!(
-                                        let mut #cx_varname = #namespace::TestAppContext::new(
-                                            foreground_platform.clone(),
-                                            cx.platform().clone(),
-                                            deterministic.build_foreground(#ix),
-                                            deterministic.build_background(),
-                                            cx.font_cache().clone(),
-                                            cx.leak_detector(),
-                                            #first_entity_id,
-                                        );
-                                    ));
-                                    cx_teardowns.extend(quote!(
-                                        #cx_varname.update(|cx| cx.remove_all_windows());
-                                        deterministic.run_until_parked();
-                                        #cx_varname.update(|_| {}); // flush effects
-                                    ));
-                                    inner_fn_args.extend(quote!(&mut #cx_varname,));
-                                }
-                                _ => {
-                                    return TokenStream::from(
-                                        syn::Error::new_spanned(arg, "invalid argument")
-                                            .into_compile_error(),
-                                    )
+                        Some("Arc") => {
+                            if let syn::PathArguments::AngleBracketed(args) =
+                                &last_segment.unwrap().arguments
+                            {
+                                if let Some(syn::GenericArgument::Type(syn::Type::Path(ty))) =
+                                    args.args.last()
+                                {
+                                    let last_segment = ty.path.segments.last();
+                                    if let Some("Deterministic") =
+                                        last_segment.map(|s| s.ident.to_string()).as_deref()
+                                    {
+                                        inner_fn_args.extend(quote!(deterministic.clone(),));
+                                        continue;
+                                    }
                                 }
                             }
                         }
-                        _ => {
-                            return TokenStream::from(
-                                syn::Error::new_spanned(arg, "invalid argument")
-                                    .into_compile_error(),
-                            )
+                        _ => {}
+                    }
+                } else if let Type::Reference(ty) = &*arg.ty {
+                    if let Type::Path(ty) = &*ty.elem {
+                        let last_segment = ty.path.segments.last();
+                        if let Some("TestAppContext") =
+                            last_segment.map(|s| s.ident.to_string()).as_deref()
+                        {
+                            let first_entity_id = ix * 100_000;
+                            let cx_varname = format_ident!("cx_{}", ix);
+                            cx_vars.extend(quote!(
+                                let mut #cx_varname = #namespace::TestAppContext::new(
+                                    foreground_platform.clone(),
+                                    cx.platform().clone(),
+                                    deterministic.build_foreground(#ix),
+                                    deterministic.build_background(),
+                                    cx.font_cache().clone(),
+                                    cx.leak_detector(),
+                                    #first_entity_id,
+                                );
+                            ));
+                            cx_teardowns.extend(quote!(
+                                #cx_varname.update(|cx| cx.remove_all_windows());
+                                deterministic.run_until_parked();
+                                #cx_varname.update(|_| {}); // flush effects
+                            ));
+                            inner_fn_args.extend(quote!(&mut #cx_varname,));
+                            continue;
                         }
                     }
-                } else {
-                    return TokenStream::from(
-                        syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
-                    );
                 }
-            } else {
-                return TokenStream::from(
-                    syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
-                );
             }
+
+            return TokenStream::from(
+                syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
+            );
         }
 
         parse_quote! {

crates/journal/Cargo.toml 🔗

@@ -14,4 +14,4 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 chrono = "0.4"
 dirs = "4.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/journal/src/journal.rs 🔗

@@ -1,14 +1,13 @@
 use chrono::{Datelike, Local, Timelike};
 use editor::{Autoscroll, Editor};
-use gpui::{action, keymap::Binding, MutableAppContext};
+use gpui::{actions, MutableAppContext};
 use std::{fs::OpenOptions, sync::Arc};
 use util::TryFutureExt as _;
 use workspace::AppState;
 
-action!(NewJournalEntry);
+actions!(journal, [NewJournalEntry]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
-    cx.add_bindings(vec![Binding::new("ctrl-alt-cmd-j", NewJournalEntry, None)]);
     cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
 }
 
@@ -44,7 +43,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     cx.spawn(|mut cx| {
         async move {
             let (journal_dir, entry_path) = create_entry.await?;
-            let workspace = cx
+            let (workspace, _) = cx
                 .update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
                 .await;
 

crates/language/Cargo.toml 🔗

@@ -35,7 +35,7 @@ async-broadcast = "0.3.4"
 async-trait = "0.1"
 futures = "0.3"
 lazy_static = "1.4"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = { version = "0.8.3", optional = true }
@@ -57,5 +57,6 @@ util = { path = "../util", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.8"
 rand = "0.8.3"
+tree-sitter-json = "0.19.0"
 tree-sitter-rust = "0.20.0"
 unindent = "0.1.7"

crates/language/src/buffer.rs 🔗

@@ -66,7 +66,6 @@ pub struct Buffer {
     file_update_count: usize,
     completion_triggers: Vec<String>,
     deferred_ops: OperationQueue<Operation>,
-    indent_size: u32,
 }
 
 pub struct BufferSnapshot {
@@ -80,7 +79,6 @@ pub struct BufferSnapshot {
     selections_update_count: usize,
     language: Option<Arc<Language>>,
     parse_count: usize,
-    indent_size: u32,
 }
 
 #[derive(Clone, Debug)]
@@ -214,6 +212,7 @@ struct AutoindentRequest {
     before_edit: BufferSnapshot,
     edited: Vec<Anchor>,
     inserted: Option<Vec<Range<Anchor>>>,
+    indent_size: u32,
 }
 
 #[derive(Debug)]
@@ -427,8 +426,6 @@ impl Buffer {
             file_update_count: 0,
             completion_triggers: Default::default(),
             deferred_ops: OperationQueue::new(),
-            // TODO: make this configurable
-            indent_size: 4,
         }
     }
 
@@ -444,7 +441,6 @@ impl Buffer {
             language: self.language.clone(),
             parse_count: self.parse_count,
             selections_update_count: self.selections_update_count,
-            indent_size: self.indent_size,
         }
     }
 
@@ -486,6 +482,7 @@ impl Buffer {
     }
 
     pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
+        *self.syntax_tree.lock() = None;
         self.language = language;
         self.reparse(cx);
     }
@@ -785,7 +782,7 @@ impl Buffer {
                                     .indent_column_for_line(suggestion.basis_row)
                             });
                         let delta = if suggestion.indent {
-                            snapshot.indent_size
+                            request.indent_size
                         } else {
                             0
                         };
@@ -808,7 +805,7 @@ impl Buffer {
                         .flatten();
                     for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
                         let delta = if suggestion.indent {
-                            snapshot.indent_size
+                            request.indent_size
                         } else {
                             0
                         };
@@ -844,7 +841,7 @@ impl Buffer {
                             .flatten();
                         for (row, suggestion) in inserted_row_range.zip(suggestions) {
                             let delta = if suggestion.indent {
-                                snapshot.indent_size
+                                request.indent_size
                             } else {
                                 0
                             };
@@ -1054,7 +1051,7 @@ impl Buffer {
     where
         T: Into<String>,
     {
-        self.edit_internal([0..self.len()], text, false, cx)
+        self.edit_internal([0..self.len()], text, None, cx)
     }
 
     pub fn edit<I, S, T>(
@@ -1068,13 +1065,14 @@ impl Buffer {
         S: ToOffset,
         T: Into<String>,
     {
-        self.edit_internal(ranges_iter, new_text, false, cx)
+        self.edit_internal(ranges_iter, new_text, None, cx)
     }
 
     pub fn edit_with_autoindent<I, S, T>(
         &mut self,
         ranges_iter: I,
         new_text: T,
+        indent_size: u32,
         cx: &mut ModelContext<Self>,
     ) -> Option<clock::Local>
     where
@@ -1082,14 +1080,14 @@ impl Buffer {
         S: ToOffset,
         T: Into<String>,
     {
-        self.edit_internal(ranges_iter, new_text, true, cx)
+        self.edit_internal(ranges_iter, new_text, Some(indent_size), cx)
     }
 
     pub fn edit_internal<I, S, T>(
         &mut self,
         ranges_iter: I,
         new_text: T,
-        autoindent: bool,
+        autoindent_size: Option<u32>,
         cx: &mut ModelContext<Self>,
     ) -> Option<clock::Local>
     where
@@ -1121,23 +1119,27 @@ impl Buffer {
 
         self.start_transaction();
         self.pending_autoindent.take();
-        let autoindent_request = if autoindent && self.language.is_some() {
-            let before_edit = self.snapshot();
-            let edited = ranges
-                .iter()
-                .filter_map(|range| {
-                    let start = range.start.to_point(self);
-                    if new_text.starts_with('\n') && start.column == self.line_len(start.row) {
-                        None
-                    } else {
-                        Some(self.anchor_before(range.start))
-                    }
-                })
-                .collect();
-            Some((before_edit, edited))
-        } else {
-            None
-        };
+        let autoindent_request =
+            self.language
+                .as_ref()
+                .and_then(|_| autoindent_size)
+                .map(|autoindent_size| {
+                    let before_edit = self.snapshot();
+                    let edited = ranges
+                        .iter()
+                        .filter_map(|range| {
+                            let start = range.start.to_point(self);
+                            if new_text.starts_with('\n')
+                                && start.column == self.line_len(start.row)
+                            {
+                                None
+                            } else {
+                                Some(self.anchor_before(range.start))
+                            }
+                        })
+                        .collect();
+                    (before_edit, edited, autoindent_size)
+                });
 
         let first_newline_ix = new_text.find('\n');
         let new_text_len = new_text.len();
@@ -1145,7 +1147,7 @@ impl Buffer {
         let edit = self.text.edit(ranges.iter().cloned(), new_text);
         let edit_id = edit.local_timestamp();
 
-        if let Some((before_edit, edited)) = autoindent_request {
+        if let Some((before_edit, edited, size)) = autoindent_request {
             let mut inserted = None;
             if let Some(first_newline_ix) = first_newline_ix {
                 let mut delta = 0isize;
@@ -1168,6 +1170,7 @@ impl Buffer {
                 before_edit,
                 edited,
                 inserted,
+                indent_size: size,
             }));
         }
 
@@ -1924,10 +1927,6 @@ impl BufferSnapshot {
     pub fn file_update_count(&self) -> usize {
         self.file_update_count
     }
-
-    pub fn indent_size(&self) -> u32 {
-        self.indent_size
-    }
 }
 
 impl Clone for BufferSnapshot {
@@ -1943,7 +1942,6 @@ impl Clone for BufferSnapshot {
             file_update_count: self.file_update_count,
             language: self.language.clone(),
             parse_count: self.parse_count,
-            indent_size: self.indent_size,
         }
     }
 }

crates/language/src/language.rs 🔗

@@ -234,6 +234,14 @@ impl LanguageRegistry {
             .cloned()
     }
 
+    pub fn language_names(&self) -> Vec<String> {
+        self.languages
+            .read()
+            .iter()
+            .map(|language| language.name().to_string())
+            .collect()
+    }
+
     pub fn select_language(&self, path: impl AsRef<Path>) -> Option<Arc<Language>> {
         let path = path.as_ref();
         let filename = path.file_name().and_then(|name| name.to_str());

crates/language/src/tests.rs 🔗

@@ -276,12 +276,32 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
             "arguments: (arguments (identifier)))))))",
         )
     );
+}
 
-    fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
-        buffer.read_with(cx, |buffer, _| {
-            buffer.syntax_tree().unwrap().root_node().to_sexp()
-        })
-    }
+#[gpui::test]
+async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
+    let buffer = cx.add_model(|cx| {
+        let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx);
+        buffer.set_sync_parse_timeout(Duration::ZERO);
+        buffer
+    });
+
+    // Wait for the initial text to parse
+    buffer
+        .condition(&cx, |buffer, _| !buffer.is_parsing())
+        .await;
+    assert_eq!(
+        get_tree_sexp(&buffer, &cx),
+        "(source_file (expression_statement (block)))"
+    );
+
+    buffer.update(cx, |buffer, cx| {
+        buffer.set_language(Some(Arc::new(json_lang())), cx)
+    });
+    buffer
+        .condition(&cx, |buffer, _| !buffer.is_parsing())
+        .await;
+    assert_eq!(get_tree_sexp(&buffer, &cx), "(document (object))");
 }
 
 #[gpui::test]
@@ -556,13 +576,13 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
         let text = "fn a() {}";
         let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
-        buffer.edit_with_autoindent([8..8], "\n\n", cx);
+        buffer.edit_with_autoindent([8..8], "\n\n", 4, cx);
         assert_eq!(buffer.text(), "fn a() {\n    \n}");
 
-        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
+        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", 4, cx);
         assert_eq!(buffer.text(), "fn a() {\n    b()\n    \n}");
 
-        buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
+        buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", 4, cx);
         assert_eq!(buffer.text(), "fn a() {\n    b()\n        .c\n}");
 
         buffer
@@ -584,7 +604,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
 
         // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
         // their indentation is not adjusted.
-        buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
+        buffer.edit_with_autoindent(
+            [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
+            "()",
+            4,
+            cx,
+        );
         assert_eq!(
             buffer.text(),
             "
@@ -601,6 +626,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
         buffer.edit_with_autoindent(
             [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
             "\n.f\n.g",
+            4,
             cx,
         );
         assert_eq!(
@@ -631,7 +657,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
 
         let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
-        buffer.edit_with_autoindent([5..5], "\nb", cx);
+        buffer.edit_with_autoindent([5..5], "\nb", 4, cx);
         assert_eq!(
             buffer.text(),
             "
@@ -643,7 +669,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
 
         // The indentation suggestion changed because `@end` node (a close paren)
         // is now at the beginning of the line.
-        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
+        buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", 4, cx);
         assert_eq!(
             buffer.text(),
             "
@@ -795,7 +821,10 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
             }
             50..=59 if replica_ids.len() < max_peers => {
                 let old_buffer = buffer.read(cx).to_proto();
-                let new_replica_id = replica_ids.len() as ReplicaId;
+                let new_replica_id = (0..=replica_ids.len() as ReplicaId)
+                    .filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
+                    .choose(&mut rng)
+                    .unwrap();
                 log::info!(
                     "Adding new replica {} (replicating from {})",
                     new_replica_id,
@@ -804,6 +833,11 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
                 new_buffer = Some(cx.add_model(|cx| {
                     let mut new_buffer =
                         Buffer::from_proto(new_replica_id, old_buffer, None, cx).unwrap();
+                    log::info!(
+                        "New replica {} text: {:?}",
+                        new_buffer.replica_id(),
+                        new_buffer.text()
+                    );
                     new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
                     let network = network.clone();
                     cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
@@ -817,8 +851,33 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
                     .detach();
                     new_buffer
                 }));
-                replica_ids.push(new_replica_id);
                 network.borrow_mut().replicate(replica_id, new_replica_id);
+
+                if new_replica_id as usize == replica_ids.len() {
+                    replica_ids.push(new_replica_id);
+                } else {
+                    let new_buffer = new_buffer.take().unwrap();
+                    while network.borrow().has_unreceived(new_replica_id) {
+                        let ops = network
+                            .borrow_mut()
+                            .receive(new_replica_id)
+                            .into_iter()
+                            .map(|op| proto::deserialize_operation(op).unwrap());
+                        if ops.len() > 0 {
+                            log::info!(
+                                "peer {} (version: {:?}) applying {} ops from the network. {:?}",
+                                new_replica_id,
+                                buffer.read(cx).version(),
+                                ops.len(),
+                                ops
+                            );
+                            new_buffer.update(cx, |new_buffer, cx| {
+                                new_buffer.apply_ops(ops, cx).unwrap();
+                            });
+                        }
+                    }
+                    buffers[new_replica_id as usize] = new_buffer;
+                }
             }
             60..=69 if mutation_count != 0 => {
                 buffer.update(cx, |buffer, cx| {
@@ -835,9 +894,11 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
                     .map(|op| proto::deserialize_operation(op).unwrap());
                 if ops.len() > 0 {
                     log::info!(
-                        "peer {} applying {} ops from the network.",
+                        "peer {} (version: {:?}) applying {} ops from the network. {:?}",
                         replica_id,
-                        ops.len()
+                        buffer.read(cx).version(),
+                        ops.len(),
+                        ops
                     );
                     buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx).unwrap());
                 }
@@ -860,6 +921,12 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
     let first_buffer = buffers[0].read(cx).snapshot();
     for buffer in &buffers[1..] {
         let buffer = buffer.read(cx).snapshot();
+        assert_eq!(
+            buffer.version(),
+            first_buffer.version(),
+            "Replica {} version != Replica 0 version",
+            buffer.replica_id()
+        );
         assert_eq!(
             buffer.text(),
             first_buffer.text(),
@@ -889,7 +956,12 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
             .filter(|(replica_id, _)| **replica_id != buffer.replica_id())
             .map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
             .collect::<Vec<_>>();
-        assert_eq!(actual_remote_selections, expected_remote_selections);
+        assert_eq!(
+            actual_remote_selections,
+            expected_remote_selections,
+            "Replica {} remote selections != expected selections",
+            buffer.replica_id()
+        );
     }
 }
 
@@ -978,6 +1050,23 @@ fn rust_lang() -> Language {
     .unwrap()
 }
 
+fn json_lang() -> Language {
+    Language::new(
+        LanguageConfig {
+            name: "Json".into(),
+            path_suffixes: vec!["js".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_json::language()),
+    )
+}
+
+fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
+    buffer.read_with(cx, |buffer, _| {
+        buffer.syntax_tree().unwrap().root_node().to_sexp()
+    })
+}
+
 fn empty(point: Point) -> Range<Point> {
     point..point
 }

crates/lsp/Cargo.toml 🔗

@@ -17,7 +17,7 @@ util = { path = "../util" }
 anyhow = "1.0"
 async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
 futures = "0.3"
-log = "0.4"
+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"] }

crates/lsp/src/lsp.rs 🔗

@@ -201,6 +201,9 @@ impl LanguageServer {
                             std::str::from_utf8(&buffer)?
                         ));
                     }
+
+                    // Don't starve the main thread when receiving lots of messages at once.
+                    smol::future::yield_now().await;
                 }
             }
             .log_err()

crates/outline/Cargo.toml 🔗

@@ -12,6 +12,8 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+picker = { path = "../picker" }
+settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 ordered-float = "2.1.1"

crates/outline/src/outline.rs 🔗

@@ -4,45 +4,31 @@ use editor::{
 };
 use fuzzy::StringMatch;
 use gpui::{
-    action,
-    elements::*,
-    geometry::vector::Vector2F,
-    keymap::{self, Binding},
-    AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext,
+    RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
 use std::cmp::{self, Reverse};
-use workspace::{
-    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Settings, Workspace,
-};
+use workspace::Workspace;
 
-action!(Toggle);
+actions!(outline, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([
-        Binding::new("cmd-shift-O", Toggle, Some("Editor")),
-        Binding::new("escape", Toggle, Some("OutlineView")),
-    ]);
     cx.add_action(OutlineView::toggle);
-    cx.add_action(OutlineView::confirm);
-    cx.add_action(OutlineView::select_prev);
-    cx.add_action(OutlineView::select_next);
-    cx.add_action(OutlineView::select_first);
-    cx.add_action(OutlineView::select_last);
+    Picker::<OutlineView>::init(cx);
 }
 
 struct OutlineView {
-    handle: WeakViewHandle<Self>,
+    picker: ViewHandle<Picker<Self>>,
     active_editor: ViewHandle<Editor>,
     outline: Outline<Anchor>,
     selected_match_index: usize,
     prev_scroll_position: Option<Vector2F>,
     matches: Vec<StringMatch>,
-    query_editor: ViewHandle<Editor>,
-    list_state: UniformListState,
+    last_query: String,
 }
 
 pub enum Event {
@@ -62,38 +48,12 @@ impl View for OutlineView {
         "OutlineView"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
-        let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
-        cx
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.global::<Settings>();
-
-        Flex::new(Axis::Vertical)
-            .with_child(
-                Container::new(ChildView::new(&self.query_editor).boxed())
-                    .with_style(settings.theme.selector.input_editor.container)
-                    .boxed(),
-            )
-            .with_child(
-                FlexItem::new(self.render_matches(cx))
-                    .flex(1.0, false)
-                    .boxed(),
-            )
-            .contained()
-            .with_style(settings.theme.selector.container)
-            .constrained()
-            .with_max_width(800.0)
-            .with_max_height(1200.0)
-            .aligned()
-            .top()
-            .named("outline view")
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.picker.clone()).boxed()
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.focus(&self.query_editor);
+        cx.focus(&self.picker);
     }
 }
 
@@ -103,24 +63,16 @@ impl OutlineView {
         editor: ViewHandle<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let query_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
-        });
-        cx.subscribe(&query_editor, Self::on_query_editor_event)
-            .detach();
-
-        let mut this = Self {
-            handle: cx.weak_handle(),
+        let handle = cx.weak_handle();
+        Self {
+            picker: cx.add_view(|cx| Picker::new(handle, cx).with_max_size(800., 1200.)),
+            last_query: Default::default(),
             matches: Default::default(),
             selected_match_index: 0,
             prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
             active_editor: editor,
             outline,
-            query_editor,
-            list_state: Default::default(),
-        };
-        this.update_matches(cx);
-        this
+        }
     }
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
@@ -144,34 +96,18 @@ impl OutlineView {
         }
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index > 0 {
-            self.select(self.selected_match_index - 1, true, false, cx);
-        }
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index + 1 < self.matches.len() {
-            self.select(self.selected_match_index + 1, true, false, cx);
-        }
-    }
-
-    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        self.select(0, true, false, cx);
-    }
-
-    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
-        self.select(self.matches.len().saturating_sub(1), true, false, cx);
+    fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
+        self.active_editor.update(cx, |editor, cx| {
+            editor.highlight_rows(None);
+            if let Some(scroll_position) = self.prev_scroll_position {
+                editor.set_scroll_position(scroll_position, cx);
+            }
+        })
     }
 
-    fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
-        self.selected_match_index = index;
-        self.list_state.scroll_to(if center {
-            ScrollTarget::Center(index)
-        } else {
-            ScrollTarget::Show(index)
-        });
-        if navigate {
+    fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<Self>) {
+        self.selected_match_index = ix;
+        if navigate && !self.matches.is_empty() {
             let selected_match = &self.matches[self.selected_match_index];
             let outline_item = &self.outline.items[selected_match.candidate_id];
             self.active_editor.update(cx, |active_editor, cx| {
@@ -188,27 +124,6 @@ impl OutlineView {
         cx.notify();
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        self.prev_scroll_position.take();
-        self.active_editor.update(cx, |active_editor, cx| {
-            if let Some(rows) = active_editor.highlighted_rows() {
-                let snapshot = active_editor.snapshot(cx).display_snapshot;
-                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
-                active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
-            }
-        });
-        cx.emit(Event::Dismissed);
-    }
-
-    fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
-        self.active_editor.update(cx, |editor, cx| {
-            editor.highlight_rows(None);
-            if let Some(scroll_position) = self.prev_scroll_position {
-                editor.set_scroll_position(scroll_position, cx);
-            }
-        })
-    }
-
     fn on_event(
         workspace: &mut Workspace,
         _: ViewHandle<Self>,
@@ -219,24 +134,27 @@ impl OutlineView {
             Event::Dismissed => workspace.dismiss_modal(cx),
         }
     }
+}
 
-    fn on_query_editor_event(
-        &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::BufferEdited { .. } => self.update_matches(cx),
-            _ => {}
-        }
+impl PickerDelegate for OutlineView {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_match_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        self.set_selected_index(ix, true, cx);
+    }
+
+    fn center_selection_after_match_updates(&self) -> bool {
+        true
     }
 
-    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
         let selected_index;
-        let navigate_to_selected_index;
-        let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
         if query.is_empty() {
             self.restore_active_editor(cx);
             self.matches = self
@@ -276,9 +194,8 @@ impl OutlineView {
                     (ix, depth, distance_to_closest_endpoint)
                 })
                 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
-                .unwrap()
-                .0;
-            navigate_to_selected_index = false;
+                .map(|(ix, _, _)| ix)
+                .unwrap_or(0);
         } else {
             self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
             selected_index = self
@@ -288,57 +205,33 @@ impl OutlineView {
                 .max_by_key(|(_, m)| OrderedFloat(m.score))
                 .map(|(ix, _)| ix)
                 .unwrap_or(0);
-            navigate_to_selected_index = !self.matches.is_empty();
         }
-        self.select(selected_index, navigate_to_selected_index, true, cx);
+        self.last_query = query;
+        self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
+        Task::ready(())
     }
 
-    fn render_matches(&self, cx: &AppContext) -> ElementBox {
-        if self.matches.is_empty() {
-            let settings = cx.global::<Settings>();
-            return Container::new(
-                Label::new(
-                    "No matches".into(),
-                    settings.theme.selector.empty.label.clone(),
-                )
-                .boxed(),
-            )
-            .with_style(settings.theme.selector.empty.container)
-            .named("empty matches");
-        }
-
-        let handle = self.handle.clone();
-        let list = UniformList::new(
-            self.list_state.clone(),
-            self.matches.len(),
-            move |mut range, items, cx| {
-                let cx = cx.as_ref();
-                let view = handle.upgrade(cx).unwrap();
-                let view = view.read(cx);
-                let start = range.start;
-                range.end = cmp::min(range.end, view.matches.len());
-                items.extend(
-                    view.matches[range]
-                        .iter()
-                        .enumerate()
-                        .map(move |(ix, m)| view.render_match(m, start + ix, cx)),
-                );
-            },
-        );
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        self.prev_scroll_position.take();
+        self.active_editor.update(cx, |active_editor, cx| {
+            if let Some(rows) = active_editor.highlighted_rows() {
+                let snapshot = active_editor.snapshot(cx).display_snapshot;
+                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+                active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
+            }
+        });
+        cx.emit(Event::Dismissed);
+    }
 
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        self.restore_active_editor(cx);
+        cx.emit(Event::Dismissed);
     }
 
-    fn render_match(
-        &self,
-        string_match: &StringMatch,
-        index: usize,
-        cx: &AppContext,
-    ) -> ElementBox {
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
         let settings = cx.global::<Settings>();
-        let style = if index == self.selected_match_index {
+        let string_match = &self.matches[ix];
+        let style = if selected {
             &settings.theme.selector.active_item
         } else {
             &settings.theme.selector.item

crates/picker/Cargo.toml 🔗

@@ -0,0 +1,23 @@
+[package]
+name = "picker"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/picker.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"

crates/picker/src/picker.rs 🔗

@@ -0,0 +1,277 @@
+use editor::Editor;
+use gpui::{
+    elements::{
+        ChildView, EventHandler, Flex, Label, ParentElement, ScrollTarget, UniformList,
+        UniformListState,
+    },
+    geometry::vector::{vec2f, Vector2F},
+    keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use settings::Settings;
+use std::cmp;
+use workspace::menu::{
+    Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev,
+};
+
+pub struct Picker<D: PickerDelegate> {
+    delegate: WeakViewHandle<D>,
+    query_editor: ViewHandle<Editor>,
+    list_state: UniformListState,
+    max_size: Vector2F,
+    confirmed: bool,
+}
+
+pub trait PickerDelegate: View {
+    fn match_count(&self) -> usize;
+    fn selected_index(&self) -> usize;
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>);
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()>;
+    fn confirm(&mut self, cx: &mut ViewContext<Self>);
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>);
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox;
+    fn center_selection_after_match_updates(&self) -> bool {
+        false
+    }
+}
+
+impl<D: PickerDelegate> Entity for Picker<D> {
+    type Event = ();
+}
+
+impl<D: PickerDelegate> View for Picker<D> {
+    fn ui_name() -> &'static str {
+        "Picker"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
+        let settings = cx.global::<Settings>();
+        let delegate = self.delegate.clone();
+        let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
+            delegate.read(cx).match_count()
+        } else {
+            0
+        };
+
+        Flex::new(Axis::Vertical)
+            .with_child(
+                ChildView::new(&self.query_editor)
+                    .contained()
+                    .with_style(settings.theme.selector.input_editor.container)
+                    .boxed(),
+            )
+            .with_child(
+                if match_count == 0 {
+                    Label::new(
+                        "No matches".into(),
+                        settings.theme.selector.empty.label.clone(),
+                    )
+                    .contained()
+                    .with_style(settings.theme.selector.empty.container)
+                } else {
+                    UniformList::new(
+                        self.list_state.clone(),
+                        match_count,
+                        move |mut range, items, cx| {
+                            let cx = cx.as_ref();
+                            let delegate = delegate.upgrade(cx).unwrap();
+                            let delegate = delegate.read(cx);
+                            let selected_ix = delegate.selected_index();
+                            range.end = cmp::min(range.end, delegate.match_count());
+                            items.extend(range.map(move |ix| {
+                                EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx))
+                                    .on_mouse_down(move |cx| {
+                                        cx.dispatch_action(SelectIndex(ix));
+                                        true
+                                    })
+                                    .boxed()
+                            }));
+                        },
+                    )
+                    .contained()
+                    .with_margin_top(6.0)
+                }
+                .flex(1., false)
+                .boxed(),
+            )
+            .contained()
+            .with_style(settings.theme.selector.container)
+            .constrained()
+            .with_max_width(self.max_size.x())
+            .with_max_height(self.max_size.y())
+            .aligned()
+            .top()
+            .named("picker")
+    }
+
+    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+        let mut cx = Self::default_keymap_context();
+        cx.set.insert("menu".into());
+        cx
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.focus(&self.query_editor);
+    }
+}
+
+impl<D: PickerDelegate> Picker<D> {
+    pub fn init(cx: &mut MutableAppContext) {
+        cx.add_action(Self::select_first);
+        cx.add_action(Self::select_last);
+        cx.add_action(Self::select_next);
+        cx.add_action(Self::select_prev);
+        cx.add_action(Self::select_index);
+        cx.add_action(Self::confirm);
+        cx.add_action(Self::cancel);
+    }
+
+    pub fn new(delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self {
+        let query_editor = cx.add_view(|cx| {
+            Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
+        });
+        cx.subscribe(&query_editor, Self::on_query_editor_event)
+            .detach();
+        let this = Self {
+            query_editor,
+            list_state: Default::default(),
+            delegate,
+            max_size: vec2f(540., 420.),
+            confirmed: false,
+        };
+        cx.defer(|this, cx| {
+            if let Some(delegate) = this.delegate.upgrade(cx) {
+                cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
+                this.update_matches(String::new(), cx)
+            }
+        });
+        this
+    }
+
+    pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
+        self.max_size = vec2f(width, height);
+        self
+    }
+
+    pub fn query(&self, cx: &AppContext) -> String {
+        self.query_editor.read(cx).text(cx)
+    }
+
+    fn on_query_editor_event(
+        &mut self,
+        _: ViewHandle<Editor>,
+        event: &editor::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
+            editor::Event::Blurred if !self.confirmed => {
+                if let Some(delegate) = self.delegate.upgrade(cx) {
+                    delegate.update(cx, |delegate, cx| {
+                        delegate.dismiss(cx);
+                    })
+                }
+            }
+            _ => {}
+        }
+    }
+
+    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
+        if let Some(delegate) = self.delegate.upgrade(cx) {
+            let update = delegate.update(cx, |d, cx| d.update_matches(query, cx));
+            cx.spawn(|this, mut cx| async move {
+                update.await;
+                this.update(&mut cx, |this, cx| {
+                    if let Some(delegate) = this.delegate.upgrade(cx) {
+                        let delegate = delegate.read(cx);
+                        let index = delegate.selected_index();
+                        let target = if delegate.center_selection_after_match_updates() {
+                            ScrollTarget::Center(index)
+                        } else {
+                            ScrollTarget::Show(index)
+                        };
+                        this.list_state.scroll_to(target);
+                        cx.notify();
+                    }
+                });
+            })
+            .detach()
+        }
+    }
+
+    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));
+            cx.notify();
+        }
+    }
+
+    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);
+            });
+        }
+    }
+
+    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| {
+                let match_count = delegate.match_count();
+                let index = if match_count > 0 { match_count - 1 } else { 0 };
+                delegate.set_selected_index(index, cx);
+                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);
+                }
+                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| {
+                let mut selected_index = delegate.selected_index();
+                if selected_index > 0 {
+                    selected_index -= 1;
+                    delegate.set_selected_index(selected_index, cx);
+                }
+                selected_index
+            });
+            self.list_state.scroll_to(ScrollTarget::Show(index));
+            cx.notify();
+        }
+    }
+
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if let Some(delegate) = self.delegate.upgrade(cx) {
+            self.confirmed = true;
+            delegate.update(cx, |delegate, cx| delegate.confirm(cx));
+        }
+    }
+
+    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        if let Some(delegate) = self.delegate.upgrade(cx) {
+            delegate.update(cx, |delegate, cx| delegate.dismiss(cx));
+        }
+    }
+}

crates/project/Cargo.toml 🔗

@@ -25,6 +25,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 util = { path = "../util" }
 aho-corasick = "0.7"
@@ -34,7 +35,7 @@ futures = "0.3"
 ignore = "0.4"
 lazy_static = "1.4.0"
 libc = "0.2"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = "0.8.3"

crates/project/src/project.rs 🔗

@@ -28,6 +28,8 @@ use parking_lot::Mutex;
 use postage::watch;
 use rand::prelude::*;
 use search::SearchQuery;
+use serde::Serialize;
+use settings::Settings;
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
 use std::{
@@ -73,7 +75,6 @@ pub struct Project {
     client_state: ProjectClientState,
     collaborators: HashMap<PeerId, Collaborator>,
     subscriptions: Vec<client::Subscription>,
-    language_servers_with_diagnostics_running: isize,
     opened_buffer: (Rc<RefCell<watch::Sender<()>>>, watch::Receiver<()>),
     shared_buffers: HashMap<PeerId, HashSet<u64>>,
     loading_buffers: HashMap<
@@ -132,16 +133,18 @@ pub enum Event {
     CollaboratorLeft(PeerId),
 }
 
+#[derive(Serialize)]
 pub struct LanguageServerStatus {
     pub name: String,
     pub pending_work: BTreeMap<String, LanguageServerProgress>,
-    pending_diagnostic_updates: isize,
+    pub pending_diagnostic_updates: isize,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize)]
 pub struct LanguageServerProgress {
     pub message: Option<String>,
     pub percentage: Option<usize>,
+    #[serde(skip_serializing)]
     pub last_update_at: Instant,
 }
 
@@ -151,12 +154,10 @@ pub struct ProjectPath {
     pub path: Arc<Path>,
 }
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)]
 pub struct DiagnosticSummary {
     pub error_count: usize,
     pub warning_count: usize,
-    pub info_count: usize,
-    pub hint_count: usize,
 }
 
 #[derive(Debug)]
@@ -192,8 +193,6 @@ impl DiagnosticSummary {
         let mut this = Self {
             error_count: 0,
             warning_count: 0,
-            info_count: 0,
-            hint_count: 0,
         };
 
         for entry in diagnostics {
@@ -201,8 +200,6 @@ impl DiagnosticSummary {
                 match entry.diagnostic.severity {
                     DiagnosticSeverity::ERROR => this.error_count += 1,
                     DiagnosticSeverity::WARNING => this.warning_count += 1,
-                    DiagnosticSeverity::INFORMATION => this.info_count += 1,
-                    DiagnosticSeverity::HINT => this.hint_count += 1,
                     _ => {}
                 }
             }
@@ -211,13 +208,15 @@ impl DiagnosticSummary {
         this
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.error_count == 0 && self.warning_count == 0
+    }
+
     pub fn to_proto(&self, path: &Path) -> proto::DiagnosticSummary {
         proto::DiagnosticSummary {
             path: path.to_string_lossy().to_string(),
             error_count: self.error_count as u32,
             warning_count: self.warning_count as u32,
-            info_count: self.info_count as u32,
-            hint_count: self.hint_count as u32,
         }
     }
 }
@@ -329,7 +328,6 @@ impl Project {
                 user_store,
                 fs,
                 next_entry_id: Default::default(),
-                language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
                 language_server_statuses: Default::default(),
@@ -403,7 +401,6 @@ impl Project {
                         .log_err()
                     }),
                 },
-                language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
                 language_server_settings: Default::default(),
@@ -469,7 +466,6 @@ impl Project {
             .and_then(|buffer| buffer.upgrade(cx))
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn languages(&self) -> &Arc<LanguageRegistry> {
         &self.languages
     }
@@ -815,13 +811,19 @@ impl Project {
         !self.is_local()
     }
 
-    pub fn create_buffer(&mut self, cx: &mut ModelContext<Self>) -> Result<ModelHandle<Buffer>> {
+    pub fn create_buffer(
+        &mut self,
+        text: &str,
+        language: Option<Arc<Language>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<ModelHandle<Buffer>> {
         if self.is_remote() {
             return Err(anyhow!("creating buffers as a guest is not supported yet"));
         }
 
         let buffer = cx.add_model(|cx| {
-            Buffer::new(self.replica_id(), "", cx).with_language(language::PLAIN_TEXT.clone(), cx)
+            Buffer::new(self.replica_id(), text, cx)
+                .with_language(language.unwrap_or(language::PLAIN_TEXT.clone()), cx)
         });
         self.register_buffer(&buffer, cx)?;
         Ok(buffer)
@@ -1019,7 +1021,14 @@ impl Project {
         cx: &mut ModelContext<Project>,
     ) -> Task<Result<()>> {
         let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
+        let old_path =
+            File::from_dyn(buffer.read(cx).file()).and_then(|f| Some(f.as_local()?.abs_path(cx)));
         cx.spawn(|this, mut cx| async move {
+            if let Some(old_path) = old_path {
+                this.update(&mut cx, |this, cx| {
+                    this.unregister_buffer_from_language_server(&buffer, old_path, cx);
+                });
+            }
             let (worktree, path) = worktree_task.await?;
             worktree
                 .update(&mut cx, |worktree, cx| {
@@ -1091,6 +1100,23 @@ impl Project {
 
         self.assign_language_to_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()) {
+                if file.is_local() {
+                    let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+                    if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) {
+                        server
+                            .notify::<lsp::notification::DidCloseTextDocument>(
+                                lsp::DidCloseTextDocumentParams {
+                                    text_document: lsp::TextDocumentIdentifier::new(uri.clone()),
+                                },
+                            )
+                            .log_err();
+                    }
+                }
+            }
+        })
+        .detach();
 
         Ok(())
     }
@@ -1143,30 +1169,33 @@ impl Project {
                     self.buffer_snapshots
                         .insert(buffer_id, vec![(0, initial_snapshot)]);
                 }
-
-                cx.observe_release(buffer_handle, |this, buffer, cx| {
-                    if let Some(file) = File::from_dyn(buffer.file()) {
-                        if file.is_local() {
-                            let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                            if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) {
-                                server
-                                    .notify::<lsp::notification::DidCloseTextDocument>(
-                                        lsp::DidCloseTextDocumentParams {
-                                            text_document: lsp::TextDocumentIdentifier::new(
-                                                uri.clone(),
-                                            ),
-                                        },
-                                    )
-                                    .log_err();
-                            }
-                        }
-                    }
-                })
-                .detach();
             }
         }
     }
 
+    fn unregister_buffer_from_language_server(
+        &mut self,
+        buffer: &ModelHandle<Buffer>,
+        old_path: PathBuf,
+        cx: &mut ModelContext<Self>,
+    ) {
+        buffer.update(cx, |buffer, cx| {
+            buffer.update_diagnostics(Default::default(), cx);
+            self.buffer_snapshots.remove(&buffer.remote_id());
+            if let Some((_, language_server)) = self.language_server_for_buffer(buffer, cx) {
+                language_server
+                    .notify::<lsp::notification::DidCloseTextDocument>(
+                        lsp::DidCloseTextDocumentParams {
+                            text_document: lsp::TextDocumentIdentifier::new(
+                                lsp::Url::from_file_path(old_path).unwrap(),
+                            ),
+                        },
+                    )
+                    .log_err();
+            }
+        });
+    }
+
     fn on_buffer_event(
         &mut self,
         buffer: ModelHandle<Buffer>,
@@ -1191,7 +1220,7 @@ impl Project {
                 let file = File::from_dyn(buffer.file())?;
                 let abs_path = file.as_local()?.abs_path(cx);
                 let uri = lsp::Url::from_file_path(abs_path).unwrap();
-                let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default();
+                let buffer_snapshots = self.buffer_snapshots.get_mut(&buffer.remote_id())?;
                 let (version, prev_snapshot) = buffer_snapshots.last()?;
                 let next_snapshot = buffer.text_snapshot();
                 let next_version = version + 1;
@@ -1605,93 +1634,84 @@ impl Project {
                 return;
             }
         };
-
-        match progress.value {
-            lsp::ProgressParamsValue::WorkDone(progress) => match progress {
-                lsp::WorkDoneProgress::Begin(_) => {
-                    let language_server_status =
-                        if let Some(status) = self.language_server_statuses.get_mut(&server_id) {
-                            status
-                        } else {
-                            return;
-                        };
-
-                    if Some(token.as_str()) == disk_based_diagnostics_progress_token {
-                        language_server_status.pending_diagnostic_updates += 1;
-                        if language_server_status.pending_diagnostic_updates == 1 {
-                            self.disk_based_diagnostics_started(cx);
-                            self.broadcast_language_server_update(
-                                                            server_id,
-                                                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
-                                                                proto::LspDiskBasedDiagnosticsUpdating {},
-                                                            ),
-                                                        );
-                        }
-                    } else {
-                        self.on_lsp_work_start(server_id, token.clone(), cx);
+        let progress = match progress.value {
+            lsp::ProgressParamsValue::WorkDone(value) => value,
+        };
+        let language_server_status =
+            if let Some(status) = self.language_server_statuses.get_mut(&server_id) {
+                status
+            } else {
+                return;
+            };
+        match progress {
+            lsp::WorkDoneProgress::Begin(_) => {
+                if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+                    language_server_status.pending_diagnostic_updates += 1;
+                    if language_server_status.pending_diagnostic_updates == 1 {
+                        self.disk_based_diagnostics_started(cx);
                         self.broadcast_language_server_update(
                             server_id,
-                            proto::update_language_server::Variant::WorkStart(
-                                proto::LspWorkStart { token },
+                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+                                proto::LspDiskBasedDiagnosticsUpdating {},
                             ),
                         );
                     }
+                } else {
+                    self.on_lsp_work_start(server_id, token.clone(), cx);
+                    self.broadcast_language_server_update(
+                        server_id,
+                        proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
+                            token,
+                        }),
+                    );
                 }
-                lsp::WorkDoneProgress::Report(report) => {
-                    if Some(token.as_str()) != disk_based_diagnostics_progress_token {
-                        self.on_lsp_work_progress(
-                            server_id,
-                            token.clone(),
-                            LanguageServerProgress {
-                                message: report.message.clone(),
-                                percentage: report.percentage.map(|p| p as usize),
-                                last_update_at: Instant::now(),
+            }
+            lsp::WorkDoneProgress::Report(report) => {
+                if Some(token.as_str()) != disk_based_diagnostics_progress_token {
+                    self.on_lsp_work_progress(
+                        server_id,
+                        token.clone(),
+                        LanguageServerProgress {
+                            message: report.message.clone(),
+                            percentage: report.percentage.map(|p| p as usize),
+                            last_update_at: Instant::now(),
+                        },
+                        cx,
+                    );
+                    self.broadcast_language_server_update(
+                        server_id,
+                        proto::update_language_server::Variant::WorkProgress(
+                            proto::LspWorkProgress {
+                                token,
+                                message: report.message,
+                                percentage: report.percentage.map(|p| p as u32),
                             },
-                            cx,
-                        );
-                        self.broadcast_language_server_update(
-                            server_id,
-                            proto::update_language_server::Variant::WorkProgress(
-                                proto::LspWorkProgress {
-                                    token,
-                                    message: report.message,
-                                    percentage: report.percentage.map(|p| p as u32),
-                                },
-                            ),
-                        );
-                    }
+                        ),
+                    );
                 }
-                lsp::WorkDoneProgress::End(_) => {
-                    if Some(token.as_str()) == disk_based_diagnostics_progress_token {
-                        let language_server_status = if let Some(status) =
-                            self.language_server_statuses.get_mut(&server_id)
-                        {
-                            status
-                        } else {
-                            return;
-                        };
-
-                        language_server_status.pending_diagnostic_updates -= 1;
-                        if language_server_status.pending_diagnostic_updates == 0 {
-                            self.disk_based_diagnostics_finished(cx);
-                            self.broadcast_language_server_update(
-                                server_id,
-                                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
-                                    proto::LspDiskBasedDiagnosticsUpdated {},
-                                ),
-                            );
-                        }
-                    } else {
-                        self.on_lsp_work_end(server_id, token.clone(), cx);
+            }
+            lsp::WorkDoneProgress::End(_) => {
+                if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+                    language_server_status.pending_diagnostic_updates -= 1;
+                    if language_server_status.pending_diagnostic_updates == 0 {
+                        self.disk_based_diagnostics_finished(cx);
                         self.broadcast_language_server_update(
                             server_id,
-                            proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
-                                token,
-                            }),
+                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                                proto::LspDiskBasedDiagnosticsUpdated {},
+                            ),
                         );
                     }
+                } else {
+                    self.on_lsp_work_end(server_id, token.clone(), cx);
+                    self.broadcast_language_server_update(
+                        server_id,
+                        proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
+                            token,
+                        }),
+                    );
                 }
-            },
+            }
         }
     }
 
@@ -1937,26 +1957,19 @@ impl Project {
             worktree_id: worktree.read(cx).id(),
             path: relative_path.into(),
         };
-
-        for buffer in self.opened_buffers.values() {
-            if let Some(buffer) = buffer.upgrade(cx) {
-                if buffer
-                    .read(cx)
-                    .file()
-                    .map_or(false, |file| *file.path() == project_path.path)
-                {
-                    self.update_buffer_diagnostics(&buffer, diagnostics.clone(), version, cx)?;
-                    break;
-                }
-            }
+        if let Some(buffer) = self.get_open_buffer(&project_path, cx) {
+            self.update_buffer_diagnostics(&buffer, diagnostics.clone(), version, cx)?;
         }
-        worktree.update(cx, |worktree, cx| {
+
+        let updated = worktree.update(cx, |worktree, cx| {
             worktree
                 .as_local_mut()
                 .ok_or_else(|| anyhow!("not a local worktree"))?
                 .update_diagnostics(project_path.path.clone(), diagnostics, cx)
         })?;
-        cx.emit(Event::DiagnosticsUpdated(project_path));
+        if updated {
+            cx.emit(Event::DiagnosticsUpdated(project_path));
+        }
         Ok(())
     }
 
@@ -2146,6 +2159,10 @@ impl Project {
                     lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
                 );
                 let capabilities = &language_server.capabilities();
+                let tab_size = cx.update(|cx| {
+                    let language_name = buffer.read(cx).language().map(|language| language.name());
+                    cx.global::<Settings>().tab_size(language_name.as_deref())
+                });
                 let lsp_edits = if capabilities
                     .document_formatting_provider
                     .as_ref()
@@ -2155,7 +2172,7 @@ impl Project {
                         .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
                             text_document,
                             options: lsp::FormattingOptions {
-                                tab_size: 4,
+                                tab_size,
                                 insert_spaces: true,
                                 insert_final_newline: Some(true),
                                 ..Default::default()
@@ -2250,86 +2267,81 @@ impl Project {
 
     pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         if self.is_local() {
-            let mut language_servers = HashMap::default();
+            let mut requests = Vec::new();
             for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() {
+                let worktree_id = *worktree_id;
                 if let Some(worktree) = self
-                    .worktree_for_id(*worktree_id, cx)
+                    .worktree_for_id(worktree_id, cx)
                     .and_then(|worktree| worktree.read(cx).as_local())
                 {
-                    language_servers
-                        .entry(Arc::as_ptr(language_server))
-                        .or_insert((
-                            lsp_adapter.clone(),
-                            language_server.clone(),
-                            *worktree_id,
-                            worktree.abs_path().clone(),
-                        ));
+                    let lsp_adapter = lsp_adapter.clone();
+                    let worktree_abs_path = worktree.abs_path().clone();
+                    requests.push(
+                        language_server
+                            .request::<lsp::request::WorkspaceSymbol>(lsp::WorkspaceSymbolParams {
+                                query: query.to_string(),
+                                ..Default::default()
+                            })
+                            .log_err()
+                            .map(move |response| {
+                                (
+                                    lsp_adapter,
+                                    worktree_id,
+                                    worktree_abs_path,
+                                    response.unwrap_or_default(),
+                                )
+                            }),
+                    );
                 }
             }
 
-            let mut requests = Vec::new();
-            for (_, language_server, _, _) in language_servers.values() {
-                requests.push(language_server.request::<lsp::request::WorkspaceSymbol>(
-                    lsp::WorkspaceSymbolParams {
-                        query: query.to_string(),
-                        ..Default::default()
-                    },
-                ));
-            }
-
             cx.spawn_weak(|this, cx| async move {
-                let responses = futures::future::try_join_all(requests).await?;
-
-                let mut symbols = Vec::new();
-                if let Some(this) = this.upgrade(&cx) {
-                    this.read_with(&cx, |this, cx| {
-                        for ((adapter, _, source_worktree_id, worktree_abs_path), lsp_symbols) in
-                            language_servers.into_values().zip(responses)
-                        {
-                            symbols.extend(lsp_symbols.into_iter().flatten().filter_map(
-                                |lsp_symbol| {
-                                    let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
-                                    let mut worktree_id = source_worktree_id;
-                                    let path;
-                                    if let Some((worktree, rel_path)) =
-                                        this.find_local_worktree(&abs_path, cx)
-                                    {
-                                        worktree_id = worktree.read(cx).id();
-                                        path = rel_path;
-                                    } else {
-                                        path = relativize_path(&worktree_abs_path, &abs_path);
-                                    }
-
-                                    let label = this
-                                        .languages
-                                        .select_language(&path)
-                                        .and_then(|language| {
-                                            language
-                                                .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
-                                        })
-                                        .unwrap_or_else(|| {
-                                            CodeLabel::plain(lsp_symbol.name.clone(), None)
-                                        });
-                                    let signature = this.symbol_signature(worktree_id, &path);
-
-                                    Some(Symbol {
-                                        source_worktree_id,
-                                        worktree_id,
-                                        language_server_name: adapter.name(),
-                                        name: lsp_symbol.name,
-                                        kind: lsp_symbol.kind,
-                                        label,
-                                        path,
-                                        range: range_from_lsp(lsp_symbol.location.range),
-                                        signature,
-                                    })
-                                },
-                            ));
-                        }
-                    })
-                }
+                let responses = futures::future::join_all(requests).await;
+                let this = if let Some(this) = this.upgrade(&cx) {
+                    this
+                } else {
+                    return Ok(Default::default());
+                };
+                this.read_with(&cx, |this, cx| {
+                    let mut symbols = Vec::new();
+                    for (adapter, source_worktree_id, worktree_abs_path, response) in responses {
+                        symbols.extend(response.into_iter().flatten().filter_map(|lsp_symbol| {
+                            let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
+                            let mut worktree_id = source_worktree_id;
+                            let path;
+                            if let Some((worktree, rel_path)) =
+                                this.find_local_worktree(&abs_path, cx)
+                            {
+                                worktree_id = worktree.read(cx).id();
+                                path = rel_path;
+                            } else {
+                                path = relativize_path(&worktree_abs_path, &abs_path);
+                            }
 
-                Ok(symbols)
+                            let label = this
+                                .languages
+                                .select_language(&path)
+                                .and_then(|language| {
+                                    language.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
+                                })
+                                .unwrap_or_else(|| CodeLabel::plain(lsp_symbol.name.clone(), None));
+                            let signature = this.symbol_signature(worktree_id, &path);
+
+                            Some(Symbol {
+                                source_worktree_id,
+                                worktree_id,
+                                language_server_name: adapter.name(),
+                                name: lsp_symbol.name,
+                                kind: lsp_symbol.kind,
+                                label,
+                                path,
+                                range: range_from_lsp(lsp_symbol.location.range),
+                                signature,
+                            })
+                        }));
+                    }
+                    Ok(symbols)
+                })
             })
         } else if let Some(project_id) = self.remote_id() {
             let request = self.client.request(proto::GetProjectSymbols {
@@ -3387,6 +3399,7 @@ impl Project {
     ) {
         let snapshot = worktree_handle.read(cx).snapshot();
         let mut buffers_to_delete = Vec::new();
+        let mut renamed_buffers = Vec::new();
         for (buffer_id, buffer) in &self.opened_buffers {
             if let Some(buffer) = buffer.upgrade(cx) {
                 buffer.update(cx, |buffer, cx| {
@@ -3426,6 +3439,11 @@ impl Project {
                             }
                         };
 
+                        let old_path = old_file.abs_path(cx);
+                        if new_file.abs_path(cx) != old_path {
+                            renamed_buffers.push((cx.handle(), old_path));
+                        }
+
                         if let Some(project_id) = self.remote_id() {
                             self.client
                                 .send(proto::UpdateBufferFile {
@@ -3446,6 +3464,12 @@ impl Project {
         for buffer_id in buffers_to_delete {
             self.opened_buffers.remove(&buffer_id);
         }
+
+        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.register_buffer_with_language_server(&buffer, cx);
+        }
     }
 
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
@@ -3461,7 +3485,9 @@ impl Project {
     }
 
     pub fn is_running_disk_based_diagnostics(&self) -> bool {
-        self.language_servers_with_diagnostics_running > 0
+        self.language_server_statuses
+            .values()
+            .any(|status| status.pending_diagnostic_updates > 0)
     }
 
     pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
@@ -3469,8 +3495,6 @@ impl Project {
         for (_, path_summary) in self.diagnostic_summaries(cx) {
             summary.error_count += path_summary.error_count;
             summary.warning_count += path_summary.warning_count;
-            summary.info_count += path_summary.info_count;
-            summary.hint_count += path_summary.hint_count;
         }
         summary
     }
@@ -3489,16 +3513,26 @@ impl Project {
     }
 
     pub fn disk_based_diagnostics_started(&mut self, cx: &mut ModelContext<Self>) {
-        self.language_servers_with_diagnostics_running += 1;
-        if self.language_servers_with_diagnostics_running == 1 {
+        if self
+            .language_server_statuses
+            .values()
+            .map(|status| status.pending_diagnostic_updates)
+            .sum::<isize>()
+            == 1
+        {
             cx.emit(Event::DiskBasedDiagnosticsStarted);
         }
     }
 
     pub fn disk_based_diagnostics_finished(&mut self, cx: &mut ModelContext<Self>) {
         cx.emit(Event::DiskBasedDiagnosticsUpdated);
-        self.language_servers_with_diagnostics_running -= 1;
-        if self.language_servers_with_diagnostics_running == 0 {
+        if self
+            .language_server_statuses
+            .values()
+            .map(|status| status.pending_diagnostic_updates)
+            .sum::<isize>()
+            == 0
+        {
             cx.emit(Event::DiskBasedDiagnosticsFinished);
         }
     }
@@ -3806,7 +3840,7 @@ impl Project {
             let buffer = this
                 .opened_buffers
                 .get(&buffer_id)
-                .map(|buffer| buffer.upgrade(cx).unwrap())
+                .and_then(|buffer| buffer.upgrade(cx))
                 .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?;
             Ok::<_, anyhow::Error>((project_id, buffer))
         })?;
@@ -3838,7 +3872,7 @@ impl Project {
                 buffers.insert(
                     this.opened_buffers
                         .get(buffer_id)
-                        .map(|buffer| buffer.upgrade(cx).unwrap())
+                        .and_then(|buffer| buffer.upgrade(cx))
                         .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
                 );
             }
@@ -3867,7 +3901,7 @@ impl Project {
                 buffers.insert(
                     this.opened_buffers
                         .get(buffer_id)
-                        .map(|buffer| buffer.upgrade(cx).unwrap())
+                        .and_then(|buffer| buffer.upgrade(cx))
                         .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
                 );
             }
@@ -3898,7 +3932,7 @@ impl Project {
         let buffer = this.read_with(&cx, |this, cx| {
             this.opened_buffers
                 .get(&envelope.payload.buffer_id)
-                .map(|buffer| buffer.upgrade(cx).unwrap())
+                .and_then(|buffer| buffer.upgrade(cx))
                 .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
         })?;
         buffer
@@ -3928,7 +3962,7 @@ impl Project {
             let buffer = this
                 .opened_buffers
                 .get(&envelope.payload.buffer_id)
-                .map(|buffer| buffer.upgrade(cx).unwrap())
+                .and_then(|buffer| buffer.upgrade(cx))
                 .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
             let language = buffer.read(cx).language();
             let completion = language::proto::deserialize_completion(
@@ -3970,7 +4004,7 @@ impl Project {
         let buffer = this.update(&mut cx, |this, cx| {
             this.opened_buffers
                 .get(&envelope.payload.buffer_id)
-                .map(|buffer| buffer.upgrade(cx).unwrap())
+                .and_then(|buffer| buffer.upgrade(cx))
                 .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
         })?;
         buffer
@@ -4011,7 +4045,7 @@ impl Project {
             let buffer = this
                 .opened_buffers
                 .get(&envelope.payload.buffer_id)
-                .map(|buffer| buffer.upgrade(cx).unwrap())
+                .and_then(|buffer| buffer.upgrade(cx))
                 .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
             Ok::<_, anyhow::Error>(this.apply_code_action(buffer, action, false, cx))
         })?;
@@ -4847,7 +4881,7 @@ mod tests {
     };
     use lsp::Url;
     use serde_json::json;
-    use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc};
+    use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
     use unindent::Unindent as _;
     use util::{assert_set_eq, test::temp_tree};
     use worktree::WorktreeHandle as _;
@@ -4970,7 +5004,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, cx);
+        let project = Project::test(fs.clone(), cx);
         project.update(cx, |project, _| {
             project.languages.add(Arc::new(rust_language));
             project.languages.add(Arc::new(json_language));
@@ -5122,6 +5156,110 @@ mod tests {
             )
         );
 
+        // Renames are reported only to servers matching the buffer's language.
+        fs.rename(
+            Path::new("/the-root/test2.rs"),
+            Path::new("/the-root/test3.rs"),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+        assert_eq!(
+            fake_rust_server
+                .receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await
+                .text_document,
+            lsp::TextDocumentIdentifier::new(
+                lsp::Url::from_file_path("/the-root/test2.rs").unwrap()
+            ),
+        );
+        assert_eq!(
+            fake_rust_server
+                .receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await
+                .text_document,
+            lsp::TextDocumentItem {
+                uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),
+                version: 0,
+                text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+                language_id: Default::default()
+            },
+        );
+
+        rust_buffer2.update(cx, |buffer, cx| {
+            buffer.update_diagnostics(
+                DiagnosticSet::from_sorted_entries(
+                    vec![DiagnosticEntry {
+                        diagnostic: Default::default(),
+                        range: Anchor::MIN..Anchor::MAX,
+                    }],
+                    &buffer.snapshot(),
+                ),
+                cx,
+            );
+            assert_eq!(
+                buffer
+                    .snapshot()
+                    .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+                    .count(),
+                1
+            );
+        });
+
+        // When the rename changes the extension of the file, the buffer gets closed on the old
+        // language server and gets opened on the new one.
+        fs.rename(
+            Path::new("/the-root/test3.rs"),
+            Path::new("/the-root/test3.json"),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+        assert_eq!(
+            fake_rust_server
+                .receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await
+                .text_document,
+            lsp::TextDocumentIdentifier::new(
+                lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),
+            ),
+        );
+        assert_eq!(
+            fake_json_server
+                .receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await
+                .text_document,
+            lsp::TextDocumentItem {
+                uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
+                version: 0,
+                text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+                language_id: Default::default()
+            },
+        );
+        // We clear the diagnostics, since the language has changed.
+        rust_buffer2.read_with(cx, |buffer, _| {
+            assert_eq!(
+                buffer
+                    .snapshot()
+                    .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+                    .count(),
+                0
+            );
+        });
+
+        // The renamed file's version resets after changing language server.
+        rust_buffer2.update(cx, |buffer, cx| buffer.edit([0..0], "// ", cx));
+        assert_eq!(
+            fake_json_server
+                .receive_notification::<lsp::notification::DidChangeTextDocument>()
+                .await
+                .text_document,
+            lsp::VersionedTextDocumentIdentifier::new(
+                lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
+                1
+            )
+        );
+
         // Restart language servers
         project.update(cx, |project, cx| {
             project.restart_language_servers_for_buffers(
@@ -5139,48 +5277,48 @@ mod tests {
         let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
         let mut fake_json_server = fake_json_servers.next().await.unwrap();
 
-        // Ensure both rust documents are reopened in new rust language server without worrying about order
+        // Ensure rust document is reopened in new rust language server
+        assert_eq!(
+            fake_rust_server
+                .receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await
+                .text_document,
+            lsp::TextDocumentItem {
+                uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
+                version: 1,
+                text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
+                language_id: Default::default()
+            }
+        );
+
+        // Ensure json documents are reopened in new json language server
         assert_set_eq!(
             [
-                fake_rust_server
+                fake_json_server
                     .receive_notification::<lsp::notification::DidOpenTextDocument>()
                     .await
                     .text_document,
-                fake_rust_server
+                fake_json_server
                     .receive_notification::<lsp::notification::DidOpenTextDocument>()
                     .await
                     .text_document,
             ],
             [
                 lsp::TextDocumentItem {
-                    uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
-                    version: 1,
-                    text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
+                    uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
+                    version: 0,
+                    text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
                     language_id: Default::default()
                 },
                 lsp::TextDocumentItem {
-                    uri: lsp::Url::from_file_path("/the-root/test2.rs").unwrap(),
+                    uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
                     version: 1,
                     text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
                     language_id: Default::default()
-                },
+                }
             ]
         );
 
-        // Ensure json document is reopened in new json language server
-        assert_eq!(
-            fake_json_server
-                .receive_notification::<lsp::notification::DidOpenTextDocument>()
-                .await
-                .text_document,
-            lsp::TextDocumentItem {
-                uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
-                version: 0,
-                text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
-                language_id: Default::default()
-            }
-        );
-
         // Close notifications are reported only to servers matching the buffer's language.
         cx.update(|_| drop(json_buffer));
         let close_message = lsp::DidCloseTextDocumentParams {
@@ -5196,6 +5334,122 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/dir",
+            json!({
+                "a.rs": "let a = 1;",
+                "b.rs": "let b = 2;"
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, cx);
+        let worktree_a_id = project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/dir/a.rs", true, cx)
+            })
+            .await
+            .unwrap()
+            .0
+            .read_with(cx, |tree, _| tree.id());
+        let worktree_b_id = project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/dir/b.rs", true, cx)
+            })
+            .await
+            .unwrap()
+            .0
+            .read_with(cx, |tree, _| tree.id());
+
+        let buffer_a = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_a_id, ""), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_b = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_b_id, ""), cx)
+            })
+            .await
+            .unwrap();
+
+        project.update(cx, |project, cx| {
+            project
+                .update_diagnostics(
+                    lsp::PublishDiagnosticsParams {
+                        uri: Url::from_file_path("/dir/a.rs").unwrap(),
+                        version: None,
+                        diagnostics: vec![lsp::Diagnostic {
+                            range: lsp::Range::new(
+                                lsp::Position::new(0, 4),
+                                lsp::Position::new(0, 5),
+                            ),
+                            severity: Some(lsp::DiagnosticSeverity::ERROR),
+                            message: "error 1".to_string(),
+                            ..Default::default()
+                        }],
+                    },
+                    &[],
+                    cx,
+                )
+                .unwrap();
+            project
+                .update_diagnostics(
+                    lsp::PublishDiagnosticsParams {
+                        uri: Url::from_file_path("/dir/b.rs").unwrap(),
+                        version: None,
+                        diagnostics: vec![lsp::Diagnostic {
+                            range: lsp::Range::new(
+                                lsp::Position::new(0, 4),
+                                lsp::Position::new(0, 5),
+                            ),
+                            severity: Some(lsp::DiagnosticSeverity::WARNING),
+                            message: "error 2".to_string(),
+                            ..Default::default()
+                        }],
+                    },
+                    &[],
+                    cx,
+                )
+                .unwrap();
+        });
+
+        buffer_a.read_with(cx, |buffer, _| {
+            let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
+            assert_eq!(
+                chunks
+                    .iter()
+                    .map(|(s, d)| (s.as_str(), *d))
+                    .collect::<Vec<_>>(),
+                &[
+                    ("let ", None),
+                    ("a", Some(DiagnosticSeverity::ERROR)),
+                    (" = 1;", None),
+                ]
+            );
+        });
+        buffer_b.read_with(cx, |buffer, _| {
+            let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
+            assert_eq!(
+                chunks
+                    .iter()
+                    .map(|(s, d)| (s.as_str(), *d))
+                    .collect::<Vec<_>>(),
+                &[
+                    ("let ", None),
+                    ("b", Some(DiagnosticSeverity::WARNING)),
+                    (" = 2;", None),
+                ]
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();

crates/project/src/worktree.rs 🔗

@@ -11,7 +11,10 @@ use client::{proto, Client, TypedEnvelope};
 use clock::ReplicaId;
 use collections::HashMap;
 use futures::{
-    channel::mpsc::{self, UnboundedSender},
+    channel::{
+        mpsc::{self, UnboundedSender},
+        oneshot,
+    },
     Stream, StreamExt,
 };
 use fuzzy::CharBag;
@@ -26,7 +29,6 @@ use language::{
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use postage::{
-    oneshot,
     prelude::{Sink as _, Stream as _},
     watch,
 };
@@ -231,8 +233,6 @@ impl Worktree {
                             DiagnosticSummary {
                                 error_count: summary.error_count as usize,
                                 warning_count: summary.warning_count as usize,
-                                info_count: summary.info_count as usize,
-                                hint_count: summary.hint_count as usize,
                             },
                         )
                     }),
@@ -564,29 +564,37 @@ impl LocalWorktree {
         worktree_path: Arc<Path>,
         diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
         _: &mut ModelContext<Worktree>,
-    ) -> Result<()> {
-        let summary = DiagnosticSummary::new(&diagnostics);
-        self.diagnostic_summaries
-            .insert(PathKey(worktree_path.clone()), summary.clone());
-        self.diagnostics.insert(worktree_path.clone(), diagnostics);
-
-        if let Some(share) = self.share.as_ref() {
-            self.client
-                .send(proto::UpdateDiagnosticSummary {
-                    project_id: share.project_id,
-                    worktree_id: self.id().to_proto(),
-                    summary: Some(proto::DiagnosticSummary {
-                        path: worktree_path.to_string_lossy().to_string(),
-                        error_count: summary.error_count as u32,
-                        warning_count: summary.warning_count as u32,
-                        info_count: summary.info_count as u32,
-                        hint_count: summary.hint_count as u32,
-                    }),
-                })
-                .log_err();
+    ) -> Result<bool> {
+        self.diagnostics.remove(&worktree_path);
+        let old_summary = self
+            .diagnostic_summaries
+            .remove(&PathKey(worktree_path.clone()))
+            .unwrap_or_default();
+        let new_summary = DiagnosticSummary::new(&diagnostics);
+        if !new_summary.is_empty() {
+            self.diagnostic_summaries
+                .insert(PathKey(worktree_path.clone()), new_summary);
+            self.diagnostics.insert(worktree_path.clone(), diagnostics);
+        }
+
+        let updated = !old_summary.is_empty() || !new_summary.is_empty();
+        if updated {
+            if let Some(share) = self.share.as_ref() {
+                self.client
+                    .send(proto::UpdateDiagnosticSummary {
+                        project_id: share.project_id,
+                        worktree_id: self.id().to_proto(),
+                        summary: Some(proto::DiagnosticSummary {
+                            path: worktree_path.to_string_lossy().to_string(),
+                            error_count: new_summary.error_count as u32,
+                            warning_count: new_summary.warning_count as u32,
+                        }),
+                    })
+                    .log_err();
+            }
         }
 
-        Ok(())
+        Ok(updated)
     }
 
     pub fn scan_complete(&self) -> impl Future<Output = ()> {
@@ -727,11 +735,11 @@ impl LocalWorktree {
 
     pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
         let register = self.register(project_id, cx);
-        let (mut share_tx, mut share_rx) = oneshot::channel();
+        let (share_tx, share_rx) = oneshot::channel();
         let (snapshots_to_send_tx, snapshots_to_send_rx) =
             smol::channel::unbounded::<LocalSnapshot>();
         if self.share.is_some() {
-            let _ = share_tx.try_send(Ok(()));
+            let _ = share_tx.send(Ok(()));
         } else {
             let rpc = self.client.clone();
             let worktree_id = cx.model_id() as u64;
@@ -756,15 +764,15 @@ impl LocalWorktree {
                                 })
                                 .await
                             {
-                                let _ = share_tx.try_send(Err(error));
+                                let _ = share_tx.send(Err(error));
                                 return Err(anyhow!("failed to send initial update worktree"));
                             } else {
-                                let _ = share_tx.try_send(Ok(()));
+                                let _ = share_tx.send(Ok(()));
                                 snapshot
                             }
                         }
                         Err(error) => {
-                            let _ = share_tx.try_send(Err(error.into()));
+                            let _ = share_tx.send(Err(error.into()));
                             return Err(anyhow!("failed to send initial update worktree"));
                         }
                     };
@@ -804,9 +812,8 @@ impl LocalWorktree {
                 });
             }
             share_rx
-                .next()
                 .await
-                .unwrap_or_else(|| Err(anyhow!("share ended")))
+                .unwrap_or_else(|_| Err(anyhow!("share ended")))
         })
     }
 
@@ -845,15 +852,16 @@ impl RemoteWorktree {
         path: Arc<Path>,
         summary: &proto::DiagnosticSummary,
     ) {
-        self.diagnostic_summaries.insert(
-            PathKey(path.clone()),
-            DiagnosticSummary {
-                error_count: summary.error_count as usize,
-                warning_count: summary.warning_count as usize,
-                info_count: summary.info_count as usize,
-                hint_count: summary.hint_count as usize,
-            },
-        );
+        let summary = DiagnosticSummary {
+            error_count: summary.error_count as usize,
+            warning_count: summary.warning_count as usize,
+        };
+        if summary.is_empty() {
+            self.diagnostic_summaries.remove(&PathKey(path.clone()));
+        } else {
+            self.diagnostic_summaries
+                .insert(PathKey(path.clone()), summary);
+        }
     }
 }
 

crates/project_panel/Cargo.toml 🔗

@@ -10,6 +10,7 @@ doctest = false
 [dependencies]
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/project_panel/src/project_panel.rs 🔗

@@ -1,15 +1,16 @@
 use gpui::{
-    action,
+    actions,
     elements::{
         Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
         Svg, UniformList, UniformListState,
     },
-    keymap::{self, Binding},
+    impl_internal_actions, keymap,
     platform::CursorStyle,
     AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
     ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use settings::Settings;
 use std::{
     collections::{hash_map, HashMap},
     ffi::OsStr,
@@ -17,7 +18,7 @@ use std::{
 };
 use workspace::{
     menu::{SelectNext, SelectPrev},
-    Settings, Workspace,
+    Workspace,
 };
 
 pub struct ProjectPanel {
@@ -45,10 +46,14 @@ struct EntryDetails {
     is_selected: bool,
 }
 
-action!(ExpandSelectedEntry);
-action!(CollapseSelectedEntry);
-action!(ToggleExpanded, ProjectEntryId);
-action!(Open, ProjectEntryId);
+#[derive(Clone)]
+pub struct ToggleExpanded(pub ProjectEntryId);
+
+#[derive(Clone)]
+pub struct Open(pub ProjectEntryId);
+
+actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]);
+impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::expand_selected_entry);
@@ -57,10 +62,6 @@ 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_bindings([
-        Binding::new("right", ExpandSelectedEntry, Some("ProjectPanel")),
-        Binding::new("left", CollapseSelectedEntry, Some("ProjectPanel")),
-    ]);
 }
 
 pub enum Event {

crates/project_symbols/Cargo.toml 🔗

@@ -11,11 +11,21 @@ doctest = false
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+picker = { path = "../picker" }
 project = { path = "../project" }
 text = { path = "../text" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 util = { path = "../util" }
 anyhow = "1.0.38"
 ordered-float = "2.1.1"
 postage = { version = "0.4", features = ["futures-traits"] }
 smol = "1.2"
+
+[dev-dependencies]
+futures = "0.3"
+settings = { path = "../settings", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -3,49 +3,33 @@ use editor::{
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    action,
-    elements::*,
-    keymap::{self, Binding},
-    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
+    View, ViewContext, ViewHandle,
 };
 use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
 use project::{Project, Symbol};
-use std::{
-    borrow::Cow,
-    cmp::{self, Reverse},
-};
+use settings::Settings;
+use std::{borrow::Cow, cmp::Reverse};
 use util::ResultExt;
-use workspace::{
-    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Settings, Workspace,
-};
+use workspace::Workspace;
 
-action!(Toggle);
+actions!(project_symbols, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([
-        Binding::new("cmd-t", Toggle, None),
-        Binding::new("escape", Toggle, Some("ProjectSymbolsView")),
-    ]);
     cx.add_action(ProjectSymbolsView::toggle);
-    cx.add_action(ProjectSymbolsView::confirm);
-    cx.add_action(ProjectSymbolsView::select_prev);
-    cx.add_action(ProjectSymbolsView::select_next);
-    cx.add_action(ProjectSymbolsView::select_first);
-    cx.add_action(ProjectSymbolsView::select_last);
+    Picker::<ProjectSymbolsView>::init(cx);
 }
 
 pub struct ProjectSymbolsView {
-    handle: WeakViewHandle<Self>,
+    picker: ViewHandle<Picker<Self>>,
     project: ModelHandle<Project>,
     selected_match_index: usize,
-    list_state: UniformListState,
     symbols: Vec<Symbol>,
     match_candidates: Vec<StringMatchCandidate>,
+    show_worktree_root_name: bool,
+    pending_update: Task<()>,
     matches: Vec<StringMatch>,
-    pending_symbols_task: Task<Option<()>>,
-    query_editor: ViewHandle<Editor>,
 }
 
 pub enum Event {
@@ -62,60 +46,28 @@ impl View for ProjectSymbolsView {
         "ProjectSymbolsView"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
-        let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
-        cx
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        Flex::new(Axis::Vertical)
-            .with_child(
-                Container::new(ChildView::new(&self.query_editor).boxed())
-                    .with_style(settings.theme.selector.input_editor.container)
-                    .boxed(),
-            )
-            .with_child(
-                FlexItem::new(self.render_matches(cx))
-                    .flex(1., false)
-                    .boxed(),
-            )
-            .contained()
-            .with_style(settings.theme.selector.container)
-            .constrained()
-            .with_max_width(500.0)
-            .with_max_height(420.0)
-            .aligned()
-            .top()
-            .named("project symbols view")
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.picker.clone()).boxed()
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.focus(&self.query_editor);
+        cx.focus(&self.picker);
     }
 }
 
 impl ProjectSymbolsView {
     fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-        let query_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
-        });
-        cx.subscribe(&query_editor, Self::on_query_editor_event)
-            .detach();
-        let mut this = Self {
-            handle: cx.weak_handle(),
+        let handle = cx.weak_handle();
+        Self {
             project,
+            picker: cx.add_view(|cx| Picker::new(handle, cx)),
             selected_match_index: 0,
-            list_state: Default::default(),
             symbols: Default::default(),
             match_candidates: Default::default(),
             matches: Default::default(),
-            pending_symbols_task: Task::ready(None),
-            query_editor,
-        };
-        this.update_matches(cx);
-        this
+            show_worktree_root_name: false,
+            pending_update: Task::ready(()),
+        }
     }
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
@@ -127,72 +79,7 @@ impl ProjectSymbolsView {
         });
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index > 0 {
-            self.select(self.selected_match_index - 1, cx);
-        }
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index + 1 < self.matches.len() {
-            self.select(self.selected_match_index + 1, cx);
-        }
-    }
-
-    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        self.select(0, cx);
-    }
-
-    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
-        self.select(self.matches.len().saturating_sub(1), cx);
-    }
-
-    fn select(&mut self, index: usize, cx: &mut ViewContext<Self>) {
-        self.selected_match_index = index;
-        self.list_state.scroll_to(ScrollTarget::Show(index));
-        cx.notify();
-    }
-
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some(symbol) = self
-            .matches
-            .get(self.selected_match_index)
-            .map(|mat| self.symbols[mat.candidate_id].clone())
-        {
-            cx.emit(Event::Selected(symbol));
-        }
-    }
-
-    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
-        self.filter(cx);
-        let query = self.query_editor.read(cx).text(cx);
-        let symbols = self
-            .project
-            .update(cx, |project, cx| project.symbols(&query, cx));
-        self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move {
-            let symbols = symbols.await.log_err()?;
-            if let Some(this) = this.upgrade(&cx) {
-                this.update(&mut cx, |this, cx| {
-                    this.match_candidates = symbols
-                        .iter()
-                        .enumerate()
-                        .map(|(id, symbol)| {
-                            StringMatchCandidate::new(
-                                id,
-                                symbol.label.text[symbol.label.filter_range.clone()].to_string(),
-                            )
-                        })
-                        .collect();
-                    this.symbols = symbols;
-                    this.filter(cx);
-                });
-            }
-            None
-        });
-    }
-
-    fn filter(&mut self, cx: &mut ViewContext<Self>) {
-        let query = self.query_editor.read(cx).text(cx);
+    fn filter(&mut self, query: &str, cx: &mut ViewContext<Self>) {
         let mut matches = if query.is_empty() {
             self.match_candidates
                 .iter()
@@ -205,9 +92,9 @@ impl ProjectSymbolsView {
                 })
                 .collect()
         } else {
-            smol::block_on(fuzzy::match_strings(
+            cx.background_executor().block(fuzzy::match_strings(
                 &self.match_candidates,
-                &query,
+                query,
                 false,
                 100,
                 &Default::default(),
@@ -231,57 +118,112 @@ impl ProjectSymbolsView {
         }
 
         self.matches = matches;
-        self.select_first(&SelectFirst, cx);
+        self.set_selected_index(0, cx);
         cx.notify();
     }
 
-    fn render_matches(&self, cx: &AppContext) -> ElementBox {
-        if self.matches.is_empty() {
-            let settings = cx.global::<Settings>();
-            return Container::new(
-                Label::new(
-                    "No matches".into(),
-                    settings.theme.selector.empty.label.clone(),
-                )
-                .boxed(),
-            )
-            .with_style(settings.theme.selector.empty.container)
-            .named("empty matches");
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<Self>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => workspace.dismiss_modal(cx),
+            Event::Selected(symbol) => {
+                let buffer = workspace
+                    .project()
+                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+
+                let symbol = symbol.clone();
+                cx.spawn(|workspace, mut cx| async move {
+                    let buffer = buffer.await?;
+                    workspace.update(&mut cx, |workspace, cx| {
+                        let position = buffer
+                            .read(cx)
+                            .clip_point_utf16(symbol.range.start, Bias::Left);
+
+                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
+                        editor.update(cx, |editor, cx| {
+                            editor.select_ranges(
+                                [position..position],
+                                Some(Autoscroll::Center),
+                                cx,
+                            );
+                        });
+                    });
+                    Ok::<_, anyhow::Error>(())
+                })
+                .detach_and_log_err(cx);
+                workspace.dismiss_modal(cx);
+            }
         }
+    }
+}
 
-        let handle = self.handle.clone();
-        let list = UniformList::new(
-            self.list_state.clone(),
-            self.matches.len(),
-            move |mut range, items, cx| {
-                let cx = cx.as_ref();
-                let view = handle.upgrade(cx).unwrap();
-                let view = view.read(cx);
-                let start = range.start;
-                range.end = cmp::min(range.end, view.matches.len());
-
-                let show_worktree_root_name =
-                    view.project.read(cx).visible_worktrees(cx).count() > 1;
-                items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| {
-                    view.render_match(m, start + ix, show_worktree_root_name, cx)
-                }));
-            },
-        );
+impl PickerDelegate for ProjectSymbolsView {
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(symbol) = self
+            .matches
+            .get(self.selected_match_index)
+            .map(|mat| self.symbols[mat.candidate_id].clone())
+        {
+            cx.emit(Event::Selected(symbol));
+        }
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
 
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_match_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        self.selected_match_index = ix;
+        cx.notify();
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        self.filter(&query, cx);
+        self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
+        let symbols = self
+            .project
+            .update(cx, |project, cx| project.symbols(&query, cx));
+        self.pending_update = cx.spawn_weak(|this, mut cx| async move {
+            let symbols = symbols.await.log_err();
+            if let Some(this) = this.upgrade(&cx) {
+                if let Some(symbols) = symbols {
+                    this.update(&mut cx, |this, cx| {
+                        this.match_candidates = symbols
+                            .iter()
+                            .enumerate()
+                            .map(|(id, symbol)| {
+                                StringMatchCandidate::new(
+                                    id,
+                                    symbol.label.text[symbol.label.filter_range.clone()]
+                                        .to_string(),
+                                )
+                            })
+                            .collect();
+                        this.symbols = symbols;
+                        this.filter(&query, cx);
+                    });
+                }
+            }
+        });
+        Task::ready(())
     }
 
-    fn render_match(
-        &self,
-        string_match: &StringMatch,
-        index: usize,
-        show_worktree_root_name: bool,
-        cx: &AppContext,
-    ) -> ElementBox {
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+        let string_match = &self.matches[ix];
         let settings = cx.global::<Settings>();
-        let style = if index == self.selected_match_index {
+        let style = if selected {
             &settings.theme.selector.active_item
         } else {
             &settings.theme.selector.item
@@ -290,7 +232,7 @@ impl ProjectSymbolsView {
         let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
 
         let mut path = symbol.path.to_string_lossy();
-        if show_worktree_root_name {
+        if self.show_worktree_root_name {
             let project = self.project.read(cx);
             if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
                 path = Cow::Owned(format!(
@@ -323,55 +265,139 @@ impl ProjectSymbolsView {
             .with_style(style.container)
             .boxed()
     }
+}
 
-    fn on_query_editor_event(
-        &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::BufferEdited { .. } => self.update_matches(cx),
-            _ => {}
-        }
-    }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use futures::StreamExt;
+    use gpui::{serde_json::json, TestAppContext};
+    use language::{FakeLspAdapter, Language, LanguageConfig};
+    use project::FakeFs;
+    use std::sync::Arc;
+
+    #[gpui::test]
+    async fn test_project_symbols(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| cx.set_global(Settings::test(cx)));
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            None,
+        );
+        let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter::default());
 
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<Self>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => workspace.dismiss_modal(cx),
-            Event::Selected(symbol) => {
-                let buffer = workspace
-                    .project()
-                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
 
-                let symbol = symbol.clone();
-                cx.spawn(|workspace, mut cx| async move {
-                    let buffer = buffer.await?;
-                    workspace.update(&mut cx, |workspace, cx| {
-                        let position = buffer
-                            .read(cx)
-                            .clip_point_utf16(symbol.range.start, Bias::Left);
+        let project = Project::test(fs.clone(), cx);
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::new(language));
+        });
 
-                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
-                        editor.update(cx, |editor, cx| {
-                            editor.select_ranges(
-                                [position..position],
-                                Some(Autoscroll::Center),
-                                cx,
-                            );
-                        });
-                    });
-                    Ok::<_, anyhow::Error>(())
-                })
-                .detach_and_log_err(cx);
-                workspace.dismiss_modal(cx);
-            }
+        let worktree_id = project
+            .update(cx, |project, cx| {
+                project.find_or_create_local_worktree("/dir", true, cx)
+            })
+            .await
+            .unwrap()
+            .0
+            .read_with(cx, |tree, _| tree.id());
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "test.rs"), cx)
+            })
+            .await
+            .unwrap();
+
+        // Set up fake langauge server to return fuzzy matches against
+        // a fixed set of symbol names.
+        let fake_symbol_names = ["one", "ton", "uno"];
+        let fake_server = fake_servers.next().await.unwrap();
+        fake_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
+            move |params: lsp::WorkspaceSymbolParams, cx| {
+                let executor = cx.background();
+                async move {
+                    let candidates = fake_symbol_names
+                        .into_iter()
+                        .map(|name| StringMatchCandidate::new(0, name.into()))
+                        .collect::<Vec<_>>();
+                    let matches = fuzzy::match_strings(
+                        &candidates,
+                        &params.query,
+                        true,
+                        100,
+                        &Default::default(),
+                        executor.clone(),
+                    )
+                    .await;
+                    Ok(Some(
+                        matches.into_iter().map(|mat| symbol(&mat.string)).collect(),
+                    ))
+                }
+            },
+        );
+
+        // Create the project symbols view.
+        let (_, symbols_view) = cx.add_window(|cx| ProjectSymbolsView::new(project.clone(), cx));
+        let picker = symbols_view.read_with(cx, |symbols_view, _| symbols_view.picker.clone());
+
+        // Spawn multiples updates before the first update completes,
+        // such that in the end, there are no matches. Testing for regression:
+        // https://github.com/zed-industries/zed/issues/861
+        picker.update(cx, |p, cx| {
+            p.update_matches("o".to_string(), cx);
+            p.update_matches("on".to_string(), cx);
+            p.update_matches("onex".to_string(), cx);
+        });
+
+        cx.foreground().run_until_parked();
+        symbols_view.read_with(cx, |symbols_view, _| {
+            assert_eq!(symbols_view.matches.len(), 0);
+        });
+
+        // Spawn more updates such that in the end, there are matches.
+        picker.update(cx, |p, cx| {
+            p.update_matches("one".to_string(), cx);
+            p.update_matches("on".to_string(), cx);
+        });
+
+        cx.foreground().run_until_parked();
+        symbols_view.read_with(cx, |symbols_view, _| {
+            assert_eq!(symbols_view.matches.len(), 2);
+            assert_eq!(symbols_view.matches[0].string, "one");
+            assert_eq!(symbols_view.matches[1].string, "ton");
+        });
+
+        // Spawn more updates such that in the end, there are again no matches.
+        picker.update(cx, |p, cx| {
+            p.update_matches("o".to_string(), cx);
+            p.update_matches("".to_string(), cx);
+        });
+
+        cx.foreground().run_until_parked();
+        symbols_view.read_with(cx, |symbols_view, _| {
+            assert_eq!(symbols_view.matches.len(), 0);
+        });
+    }
+
+    fn symbol(name: &str) -> lsp::SymbolInformation {
+        #[allow(deprecated)]
+        lsp::SymbolInformation {
+            name: name.to_string(),
+            kind: lsp::SymbolKind::FUNCTION,
+            tags: None,
+            deprecated: None,
+            container_name: None,
+            location: lsp::Location::new(
+                lsp::Url::from_file_path("/a/b").unwrap(),
+                lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+            ),
         }
     }
 }

crates/rpc/Cargo.toml 🔗

@@ -9,31 +9,32 @@ path = "src/rpc.rs"
 doctest = false
 
 [features]
-test-support = ["gpui/test-support"]
+test-support = ["collections/test-support", "gpui/test-support"]
 
 [dependencies]
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+gpui = { path = "../gpui", optional = true }
+util = { path = "../util" }
 anyhow = "1.0"
 async-lock = "2.4"
 async-tungstenite = "0.16"
 base64 = "0.13"
 futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
 prost = "0.8"
 rand = "0.8"
 rsa = "0.4"
 serde = { version = "1", features = ["derive"] }
 smol-timeout = "0.6"
 zstd = "0.9"
-clock = { path = "../clock" }
-gpui = { path = "../gpui", optional = true }
-util = { path = "../util" }
 
 [build-dependencies]
 prost-build = "0.8"
 
 [dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 smol = "1.2.5"
 tempdir = "0.3.7"

crates/rpc/proto/zed.proto 🔗

@@ -453,8 +453,6 @@ message DiagnosticSummary {
     string path = 1;
     uint32 error_count = 2;
     uint32 warning_count = 3;
-    uint32 info_count = 4;
-    uint32 hint_count = 5;
 }
 
 message UpdateLanguageServer {

crates/rpc/src/conn.rs 🔗

@@ -35,21 +35,24 @@ impl Connection {
     #[cfg(any(test, feature = "test-support"))]
     pub fn in_memory(
         executor: std::sync::Arc<gpui::executor::Background>,
-    ) -> (Self, Self, postage::barrier::Sender) {
-        use postage::prelude::Stream;
+    ) -> (Self, Self, std::sync::Arc<std::sync::atomic::AtomicBool>) {
+        use std::sync::{
+            atomic::{AtomicBool, Ordering::SeqCst},
+            Arc,
+        };
 
-        let (kill_tx, kill_rx) = postage::barrier::channel();
-        let (a_tx, a_rx) = channel(kill_rx.clone(), executor.clone());
-        let (b_tx, b_rx) = channel(kill_rx, executor);
+        let killed = Arc::new(AtomicBool::new(false));
+        let (a_tx, a_rx) = channel(killed.clone(), executor.clone());
+        let (b_tx, b_rx) = channel(killed.clone(), executor);
         return (
             Self { tx: a_tx, rx: b_rx },
             Self { tx: b_tx, rx: a_rx },
-            kill_tx,
+            killed,
         );
 
         fn channel(
-            kill_rx: postage::barrier::Receiver,
-            executor: std::sync::Arc<gpui::executor::Background>,
+            killed: Arc<AtomicBool>,
+            executor: Arc<gpui::executor::Background>,
         ) -> (
             Box<dyn Send + Unpin + futures::Sink<WebSocketMessage, Error = WebSocketError>>,
             Box<
@@ -57,20 +60,17 @@ impl Connection {
             >,
         ) {
             use futures::channel::mpsc;
-            use std::{
-                io::{Error, ErrorKind},
-                sync::Arc,
-            };
+            use std::io::{Error, ErrorKind};
 
             let (tx, rx) = mpsc::unbounded::<WebSocketMessage>();
 
             let tx = tx
                 .sink_map_err(|e| WebSocketError::from(Error::new(ErrorKind::Other, e)))
                 .with({
-                    let kill_rx = kill_rx.clone();
+                    let killed = killed.clone();
                     let executor = Arc::downgrade(&executor);
                     move |msg| {
-                        let mut kill_rx = kill_rx.clone();
+                        let killed = killed.clone();
                         let executor = executor.clone();
                         Box::pin(async move {
                             if let Some(executor) = executor.upgrade() {
@@ -78,7 +78,7 @@ impl Connection {
                             }
 
                             // Writes to a half-open TCP connection will error.
-                            if kill_rx.try_recv().is_ok() {
+                            if killed.load(SeqCst) {
                                 std::io::Result::Err(
                                     Error::new(ErrorKind::Other, "connection lost").into(),
                                 )?;
@@ -90,10 +90,10 @@ impl Connection {
                 });
 
             let rx = rx.then({
-                let kill_rx = kill_rx.clone();
+                let killed = killed.clone();
                 let executor = Arc::downgrade(&executor);
                 move |msg| {
-                    let mut kill_rx = kill_rx.clone();
+                    let killed = killed.clone();
                     let executor = executor.clone();
                     Box::pin(async move {
                         if let Some(executor) = executor.upgrade() {
@@ -101,7 +101,7 @@ impl Connection {
                         }
 
                         // Reads from a half-open TCP connection will hang.
-                        if kill_rx.try_recv().is_ok() {
+                        if killed.load(SeqCst) {
                             futures::future::pending::<()>().await;
                         }
 

crates/rpc/src/peer.rs 🔗

@@ -1,16 +1,18 @@
-use super::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage};
-use super::Connection;
+use super::{
+    proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage},
+    Connection,
+};
 use anyhow::{anyhow, Context, Result};
-use futures::{channel::oneshot, stream::BoxStream, FutureExt as _, StreamExt};
-use parking_lot::{Mutex, RwLock};
-use postage::{
-    barrier, mpsc,
-    prelude::{Sink as _, Stream as _},
+use collections::HashMap;
+use futures::{
+    channel::{mpsc, oneshot},
+    stream::BoxStream,
+    FutureExt, SinkExt, StreamExt,
 };
-use smol_timeout::TimeoutExt as _;
+use parking_lot::{Mutex, RwLock};
+use smol_timeout::TimeoutExt;
 use std::sync::atomic::Ordering::SeqCst;
 use std::{
-    collections::HashMap,
     fmt,
     future::Future,
     marker::PhantomData,
@@ -88,10 +90,10 @@ pub struct Peer {
 
 #[derive(Clone)]
 pub struct ConnectionState {
-    outgoing_tx: futures::channel::mpsc::UnboundedSender<proto::Message>,
+    outgoing_tx: mpsc::UnboundedSender<proto::Message>,
     next_message_id: Arc<AtomicU32>,
     response_channels:
-        Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, barrier::Sender)>>>>>,
+        Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, oneshot::Sender<()>)>>>>>,
 }
 
 const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
@@ -124,8 +126,12 @@ impl Peer {
         // can always send messages without yielding. For incoming messages, use a
         // bounded channel so that other peers will receive backpressure if they send
         // messages faster than this peer can process them.
-        let (mut incoming_tx, incoming_rx) = mpsc::channel(64);
-        let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded();
+        #[cfg(any(test, feature = "test-support"))]
+        const INCOMING_BUFFER_SIZE: usize = 1;
+        #[cfg(not(any(test, feature = "test-support")))]
+        const INCOMING_BUFFER_SIZE: usize = 64;
+        let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE);
+        let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
 
         let connection_id = ConnectionId(self.next_connection_id.fetch_add(1, SeqCst));
         let connection_state = ConnectionState {
@@ -173,8 +179,10 @@ impl Peer {
                             let incoming = incoming.context("received invalid RPC message")?;
                             receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
                             if let proto::Message::Envelope(incoming) = incoming {
-                                if incoming_tx.send(incoming).await.is_err() {
-                                    return Ok(());
+                                match incoming_tx.send(incoming).timeout(RECEIVE_TIMEOUT).await {
+                                    Some(Ok(_)) => {},
+                                    Some(Err(_)) => return Ok(()),
+                                    None => Err(anyhow!("timed out processing incoming message"))?,
                                 }
                             }
                             break;
@@ -206,14 +214,14 @@ impl Peer {
                 if let Some(responding_to) = incoming.responding_to {
                     let channel = response_channels.lock().as_mut()?.remove(&responding_to);
                     if let Some(tx) = channel {
-                        let mut requester_resumed = barrier::channel();
+                        let requester_resumed = oneshot::channel();
                         if let Err(error) = tx.send((incoming, requester_resumed.0)) {
                             log::debug!(
                                 "received RPC but request future was dropped {:?}",
                                 error.0
                             );
                         }
-                        requester_resumed.1.recv().await;
+                        let _ = requester_resumed.1.await;
                     } else {
                         log::warn!("received RPC response to unknown request {}", responding_to);
                     }
@@ -719,26 +727,26 @@ mod tests {
             .add_test_connection(client_conn, cx.background())
             .await;
 
-        let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel();
+        let (io_ended_tx, io_ended_rx) = oneshot::channel();
         executor
             .spawn(async move {
                 io_handler.await.ok();
-                io_ended_tx.send(()).await.unwrap();
+                io_ended_tx.send(()).unwrap();
             })
             .detach();
 
-        let (mut messages_ended_tx, mut messages_ended_rx) = postage::barrier::channel();
+        let (messages_ended_tx, messages_ended_rx) = oneshot::channel();
         executor
             .spawn(async move {
                 incoming.next().await;
-                messages_ended_tx.send(()).await.unwrap();
+                messages_ended_tx.send(()).unwrap();
             })
             .detach();
 
         client.disconnect(connection_id);
 
-        io_ended_rx.recv().await;
-        messages_ended_rx.recv().await;
+        let _ = io_ended_rx.await;
+        let _ = messages_ended_rx.await;
         assert!(server_conn
             .send(WebSocketMessage::Binary(vec![]))
             .await

crates/rpc/src/rpc.rs 🔗

@@ -5,4 +5,4 @@ pub mod proto;
 pub use conn::Connection;
 pub use peer::*;
 
-pub const PROTOCOL_VERSION: u32 = 13;
+pub const PROTOCOL_VERSION: u32 = 14;

crates/search/Cargo.toml 🔗

@@ -13,12 +13,14 @@ editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 anyhow = "1.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1", features = ["derive"] }
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }

crates/search/src/buffer_search.rs 🔗

@@ -1,50 +1,47 @@
-use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch};
+use crate::{
+    active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch,
+    SelectPrevMatch,
+};
 use collections::HashMap;
 use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
 use gpui::{
-    action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity,
-    MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
+    actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, AppContext,
+    Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
 use language::OffsetRangeExt;
 use project::search::SearchQuery;
+use serde::Deserialize;
+use settings::Settings;
 use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
+
+#[derive(Clone, Deserialize)]
+pub struct Deploy {
+    pub focus: bool,
+}
 
-action!(Deploy, bool);
-action!(Dismiss);
-action!(FocusEditor);
-action!(ToggleSearchOption, SearchOption);
+#[derive(Clone)]
+pub struct ToggleSearchOption(pub SearchOption);
+
+actions!(buffer_search, [Dismiss, FocusEditor]);
+impl_actions!(buffer_search, [Deploy]);
+impl_internal_actions!(buffer_search, [ToggleSearchOption]);
 
 pub enum Event {
     UpdateLocation,
 }
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([
-        Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
-        Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
-        Binding::new("escape", Dismiss, Some("BufferSearchBar")),
-        Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
-        Binding::new(
-            "enter",
-            SelectMatch(Direction::Next),
-            Some("BufferSearchBar"),
-        ),
-        Binding::new(
-            "shift-enter",
-            SelectMatch(Direction::Prev),
-            Some("BufferSearchBar"),
-        ),
-        Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
-        Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
-    ]);
     cx.add_action(BufferSearchBar::deploy);
     cx.add_action(BufferSearchBar::dismiss);
     cx.add_action(BufferSearchBar::focus_editor);
     cx.add_action(BufferSearchBar::toggle_search_option);
-    cx.add_action(BufferSearchBar::select_match);
-    cx.add_action(BufferSearchBar::select_match_on_pane);
+    cx.add_action(BufferSearchBar::select_next_match);
+    cx.add_action(BufferSearchBar::select_prev_match);
+    cx.add_action(BufferSearchBar::select_next_match_on_pane);
+    cx.add_action(BufferSearchBar::select_prev_match_on_pane);
+    cx.add_action(BufferSearchBar::handle_editor_cancel);
 }
 
 pub struct BufferSearchBar {
@@ -320,14 +317,27 @@ impl BufferSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+        .on_click(move |cx| match direction {
+            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
+            Direction::Next => cx.dispatch_action(SelectNextMatch),
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }
 
-    fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
+    fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, cx)) {
+                return;
+            }
+        }
+        cx.propagate_action();
+    }
+
+    fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
+            if !search_bar.read(cx).dismissed {
+                search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
                 return;
             }
         }
@@ -363,7 +373,15 @@ impl BufferSearchBar {
         cx.notify();
     }
 
-    fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
+    fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
+        self.select_match(Direction::Next, cx);
+    }
+
+    fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
+        self.select_match(Direction::Prev, cx);
+    }
+
+    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
             if let Some(editor) = self.active_editor.as_ref() {
                 editor.update(cx, |editor, cx| {
@@ -384,9 +402,23 @@ impl BufferSearchBar {
         }
     }
 
-    fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
+    fn select_next_match_on_pane(
+        pane: &mut Pane,
+        action: &SelectNextMatch,
+        cx: &mut ViewContext<Pane>,
+    ) {
         if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
-            search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
+            search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
+        }
+    }
+
+    fn select_prev_match_on_pane(
+        pane: &mut Pane,
+        action: &SelectPrevMatch,
+        cx: &mut ViewContext<Pane>,
+    ) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
         }
     }
 
@@ -512,14 +544,14 @@ impl BufferSearchBar {
                                         }
                                     }
 
-                                    let theme = &cx.global::<Settings>().theme.search;
                                     editor.highlight_background::<Self>(
                                         ranges,
-                                        theme.match_background,
+                                        |theme| theme.search.match_background,
                                         cx,
                                     );
                                 });
                             }
+                            cx.notify();
                         });
                     }
                 }));
@@ -694,7 +726,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(0));
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -705,7 +737,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -716,7 +748,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -727,7 +759,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -738,7 +770,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -749,7 +781,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -760,7 +792,7 @@ mod tests {
         });
 
         search_bar.update(cx, |search_bar, cx| {
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -777,7 +809,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(1));
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -794,7 +826,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(1));
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -811,7 +843,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(2));
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -828,7 +860,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(2));
-            search_bar.select_match(&SelectMatch(Direction::Next), cx);
+            search_bar.select_next_match(&SelectNextMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -845,7 +877,7 @@ mod tests {
         });
         search_bar.update(cx, |search_bar, cx| {
             assert_eq!(search_bar.active_match_index, Some(0));
-            search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+            search_bar.select_prev_match(&SelectPrevMatch, cx);
             assert_eq!(
                 editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
                 [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]

crates/search/src/project_search.rs 🔗

@@ -1,29 +1,25 @@
 use crate::{
-    active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch,
-    ToggleSearchOption,
+    active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch,
+    SelectPrevMatch, ToggleSearchOption,
 };
 use collections::HashMap;
 use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
 use gpui::{
-    action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
-    ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
-    ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+    actions, elements::*, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext,
+    ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
+    ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use project::{search::SearchQuery, Project};
+use settings::Settings;
 use std::{
     any::{Any, TypeId},
     ops::Range,
     path::PathBuf,
 };
 use util::ResultExt as _;
-use workspace::{
-    Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
-};
+use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace};
 
-action!(Deploy);
-action!(Search);
-action!(SearchInNew);
-action!(ToggleFocus);
+actions!(project_search, [Deploy, Search, SearchInNew, ToggleFocus]);
 
 const MAX_TAB_TITLE_LEN: usize = 24;
 
@@ -32,20 +28,12 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.set_global(ActiveSearches::default());
-    cx.add_bindings([
-        Binding::new("cmd-shift-F", ToggleFocus, Some("Pane")),
-        Binding::new("cmd-f", ToggleFocus, Some("Pane")),
-        Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
-        Binding::new("enter", Search, Some("ProjectSearchBar")),
-        Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
-        Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
-        Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
-    ]);
     cx.add_action(ProjectSearchView::deploy);
     cx.add_action(ProjectSearchBar::search);
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::toggle_search_option);
-    cx.add_action(ProjectSearchBar::select_match);
+    cx.add_action(ProjectSearchBar::select_next_match);
+    cx.add_action(ProjectSearchBar::select_prev_match);
     cx.add_action(ProjectSearchBar::toggle_focus);
     cx.capture_action(ProjectSearchBar::tab);
 }
@@ -67,6 +55,7 @@ pub struct ProjectSearchView {
     regex: bool,
     query_contains_error: bool,
     active_match_index: Option<usize>,
+    results_editor_was_focused: bool,
 }
 
 pub struct ProjectSearchBar {
@@ -140,6 +129,7 @@ impl ProjectSearch {
 
 pub enum ViewEvent {
     UpdateTab,
+    EditorEvent(editor::Event),
 }
 
 impl Entity for ProjectSearchView {
@@ -181,10 +171,10 @@ impl View for ProjectSearchView {
                 .insert(self.model.read(cx).project.downgrade(), handle)
         });
 
-        if self.model.read(cx).match_ranges.is_empty() {
-            cx.focus(&self.query_editor);
-        } else {
+        if self.results_editor_was_focused && !self.model.read(cx).match_ranges.is_empty() {
             self.focus_results_editor(cx);
+        } else {
+            cx.focus(&self.query_editor);
         }
     }
 }
@@ -308,6 +298,14 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.navigate(data, cx))
     }
 
+    fn should_activate_item_on_event(event: &Self::Event) -> bool {
+        if let ViewEvent::EditorEvent(editor_event) = event {
+            Editor::should_activate_item_on_event(editor_event)
+        } else {
+            false
+        }
+    }
+
     fn should_update_tab_on_event(event: &ViewEvent) -> bool {
         matches!(event, ViewEvent::UpdateTab)
     }
@@ -342,6 +340,15 @@ impl ProjectSearchView {
             editor.set_text(query_text, cx);
             editor
         });
+        // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
+        cx.subscribe(&query_editor, |_, _, event, cx| {
+            cx.emit(ViewEvent::EditorEvent(event.clone()))
+        })
+        .detach();
+        cx.observe_focus(&query_editor, |this, _, _| {
+            this.results_editor_was_focused = false;
+        })
+        .detach();
 
         let results_editor = cx.add_view(|cx| {
             let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
@@ -350,10 +357,16 @@ impl ProjectSearchView {
         });
         cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
             .detach();
+        cx.observe_focus(&results_editor, |this, _, _| {
+            this.results_editor_was_focused = true;
+        })
+        .detach();
         cx.subscribe(&results_editor, |this, _, event, cx| {
             if matches!(event, editor::Event::SelectionsChanged { .. }) {
                 this.update_match_index(cx);
             }
+            // Reraise editor events for workspace item activation purposes
+            cx.emit(ViewEvent::EditorEvent(event.clone()));
         })
         .detach();
 
@@ -366,6 +379,7 @@ impl ProjectSearchView {
             regex,
             query_contains_error: false,
             active_match_index: None,
+            results_editor_was_focused: false,
         };
         this.model_changed(false, cx);
         this
@@ -394,6 +408,9 @@ impl ProjectSearchView {
 
         if let Some(existing) = existing {
             workspace.activate_item(&existing, cx);
+            existing.update(cx, |existing, cx| {
+                existing.focus_query_editor(cx);
+            });
         } else {
             let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
             workspace.add_item(
@@ -472,8 +489,11 @@ impl ProjectSearchView {
                 if reset_selections {
                     editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
                 }
-                let theme = &cx.global::<Settings>().theme.search;
-                editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
+                editor.highlight_background::<Self>(
+                    match_ranges,
+                    |theme| theme.search.match_background,
+                    cx,
+                );
             });
             if self.query_editor.is_focused(cx) {
                 self.focus_results_editor(cx);
@@ -549,18 +569,23 @@ impl ProjectSearchBar {
         }
     }
 
-    fn select_match(
-        pane: &mut Pane,
-        &SelectMatch(direction): &SelectMatch,
-        cx: &mut ViewContext<Pane>,
-    ) {
+    fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
         if let Some(search_view) = pane
             .active_item()
             .and_then(|item| item.downcast::<ProjectSearchView>())
         {
-            search_view.update(cx, |search_view, cx| {
-                search_view.select_match(direction, cx);
-            });
+            search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
+        } else {
+            cx.propagate_action();
+        }
+    }
+
+    fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
         } else {
             cx.propagate_action();
         }
@@ -639,7 +664,10 @@ impl ProjectSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+        .on_click(move |cx| match direction {
+            Direction::Prev => cx.dispatch_action(SelectPrevMatch),
+            Direction::Next => cx.dispatch_action(SelectNextMatch),
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }

crates/search/src/search.rs 🔗

@@ -1,6 +1,6 @@
 pub use buffer_search::BufferSearchBar;
 use editor::{Anchor, MultiBufferSnapshot};
-use gpui::{action, MutableAppContext};
+use gpui::{actions, impl_internal_actions, MutableAppContext};
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
@@ -15,8 +15,11 @@ pub fn init(cx: &mut MutableAppContext) {
     project_search::init(cx);
 }
 
-action!(ToggleSearchOption, SearchOption);
-action!(SelectMatch, Direction);
+#[derive(Clone)]
+pub struct ToggleSearchOption(pub SearchOption);
+
+actions!(search, [SelectNextMatch, SelectPrevMatch]);
+impl_internal_actions!(search, [ToggleSearchOption]);
 
 #[derive(Clone, Copy)]
 pub enum SearchOption {

crates/server/.env.toml 🔗

@@ -1,42 +0,0 @@
-# Prod database: CAREFUL!
-# DATABASE_URL = "postgres://postgres:f71db7645055488d666f3c26392113104706af1f24d2cf15@zed-db.internal:5432/zed"
-
-HTTP_PORT = 8080
-
-DATABASE_URL = "postgres://postgres@localhost/zed"
-SESSION_SECRET = "6E1GS6IQNOLIBKWMEVWF1AFO4H78KNU8"
-API_TOKEN = "secret"
-
-# Available at https://github.com/organizations/zed-industries/settings/apps/zed-local-development
-GITHUB_APP_ID = 115633
-GITHUB_CLIENT_ID = "Iv1.768076c9becc75c4"
-GITHUB_CLIENT_SECRET = "3592ffff1ecda9773a3df7b0e75375bfbe7992fc"
-GITHUB_PRIVATE_KEY = """\
------BEGIN RSA PRIVATE KEY-----
-MIIEowIBAAKCAQEAtt0O2t69ksn2zX5ucHpflNRoqdh342OOwrazLA6GS8Kp2hWM
-NwLzymm2s8k1e2F7sAVYNHJvUPZCvM/xYuVMNpx33fVr00Tni2ATNJKS2lvCEBC0
-nTUKxXQImF82IQadg41o+81gofR3zt2UM7iDRMPbmn/aZe7K8vvFEERawSfKEMv3
-RqAzqt0fBDYvwHonje0Y7/5IAO5GDMd9kDE3w034ckwtyFAJDjRGYN5kVoRlua+Q
-aIHoBkJ/jUAsS4kWqZt/r6hbrAcgok7Iv2RoapfgNTPeJKEe0pAagz1orqbrm9Qk
-WBeAToTXl4YTfQruMNVyN2/5azqLnS8Urg2jHQIDAQABAoIBAF9TVY8bVk/TIOl2
-4zOXV4RKRlVkFvtexukSPMzWtYOA8vJREUsMKvJ1sVx/o3WyF7xmzNhqX0UhWyD6
-dadMSTKe1o3Khm8YGGw7pUdesVdLRhsB2mWpZPgRyPlFiP4maK5PZU7+fUVwH5Sj
-RcLAiQ2r3CrqQ3unw/xu6wfT2kueBMJz6DBCx3y5wwEyrR7b+8ZGrjUy9BelzKti
-yIT3OLWhilwho8l03Dg72SCSskotVMcywtc7SMr5PCILL7QANdJDhEO8FP4BysHx
-6wlFwpfIPnNHN/RN1Dnnut5F64nPu//6vUs9DR9c34FzDp0SR2hJ98PLYn3uyD5b
-6oOcZrECgYEA3QXrezpLwkZN2wS6r6vmNyEKSIevjpQpuFEzGSapJRJbGiP5/C+l
-DfTmYud6Ld5YrL7xIQuf6SQWyO8WZkKA6D15VBdsFzM0pzhNGNGUgZYiTQ6rdh83
-5mL8l9IqzT5LD5RRXTj2CO7SB5iuyp8PrPyGCCVhILYJP+a4e4kHwEsCgYEA0803
-oF/QBhfKC3n/7xbRTeT4PcAHra+X84rY+KkyP1/qJXMRbCumpvTL6kZg7Jv2I3hG
-SaRK7mGhi0/omVn9aEgn4E7UKmE2ZhVbigTiqnPdYoH/hmrbQ5Z7SVaT/MNzGuKQ
-QZOmASgsZEjqSX7extXDzKOGD/AzMp3iWElUGTcCgYAOoT+vDnLJT0IEB1IcIrLA
-X22A04ppU6FXU/if55E2pPpmxo7bhIPWYqmFTnEl7BvOg20OlOhm1D612i2PY0OJ
-G9iWGl7LQlZv4ygnRmggE8H9e8UZsoNOuqqhmgW/RCpPw6+HDigq+zPn0NFxFApD
-lwuAKok9Uw9VrX30n2Nl9QKBgAG7c/ED15e1Khnd7ZHvBdc1QDKBF478GKoNQKkH
-+Tk7d5bG0iWoVbyX0/MekDxfKiwwF6MSjOpWMhQJm0VlzwTDUlArVODj2qYLFqyS
-TahHOlBL7+MRjKmI2YlIA/3VO2PE5pkitADeaz6GuiPPvdKyfN93lukaddC8KdW/
-A8kRAoGBAJdU+sTC1zfP+tbgArzf4rU5qEknserAH+GE6C/Otn134WBEyqSgd2Jb
-JpJsl2l/X/8Wfwh+SJQbhvDoY4ApMKlgLFBAIY/p2UcpEdUL2juec/F6+qGnBncQ
-4I+MKiVfixBM9p66Afybiskh3a/RvXK+/6NNOVtVYaSd7aSIrq9W
------END RSA PRIVATE KEY-----
-"""

crates/settings/Cargo.toml 🔗

@@ -0,0 +1,24 @@
+[package]
+name = "settings"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/settings.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+assets = { path = "../assets" }
+collections = { path = "../collections" }
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+anyhow = "1.0.38"
+schemars = "0.8"
+serde = { version = "1", features = ["derive", "rc"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_path_to_error = "0.1.4"
+toml = "0.5"

crates/settings/src/keymap_file.rs 🔗

@@ -0,0 +1,62 @@
+use anyhow::{Context, Result};
+use assets::Assets;
+use collections::BTreeMap;
+use gpui::{keymap::Binding, MutableAppContext};
+use serde::Deserialize;
+use serde_json::value::RawValue;
+
+#[derive(Deserialize, Default, Clone)]
+#[serde(transparent)]
+pub struct KeymapFile(BTreeMap<String, ActionsByKeystroke>);
+
+type ActionsByKeystroke = BTreeMap<String, Box<RawValue>>;
+
+#[derive(Deserialize)]
+struct ActionWithData<'a>(#[serde(borrow)] &'a str, #[serde(borrow)] &'a RawValue);
+
+impl KeymapFile {
+    pub fn load_defaults(cx: &mut MutableAppContext) {
+        for path in ["keymaps/default.json", "keymaps/vim.json"] {
+            Self::load(path, cx).unwrap();
+        }
+    }
+
+    pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> {
+        let content = Assets::get(asset_path).unwrap().data;
+        let content_str = std::str::from_utf8(content.as_ref()).unwrap();
+        Ok(serde_json::from_str::<Self>(content_str)?.add(cx)?)
+    }
+
+    pub fn add(self, cx: &mut MutableAppContext) -> Result<()> {
+        for (context, actions) in self.0 {
+            let context = if context == "*" { None } else { Some(context) };
+            cx.add_bindings(
+                actions
+                    .into_iter()
+                    .map(|(keystroke, action)| {
+                        let action = action.get();
+
+                        // This is a workaround for a limitation in serde: serde-rs/json#497
+                        // We want to deserialize the action data as a `RawValue` so that we can
+                        // deserialize the action itself dynamically directly from the JSON
+                        // string. But `RawValue` currently does not work inside of an untagged enum.
+                        let action = if action.starts_with('[') {
+                            let ActionWithData(name, data) = serde_json::from_str(action)?;
+                            cx.deserialize_action(name, Some(data.get()))
+                        } else {
+                            let name = serde_json::from_str(action)?;
+                            cx.deserialize_action(name, None)
+                        }
+                        .with_context(|| {
+                            format!(
+                            "invalid binding value for keystroke {keystroke}, context {context:?}"
+                        )
+                        })?;
+                        Binding::load(&keystroke, action, context.as_deref())
+                    })
+                    .collect::<Result<Vec<_>>>()?,
+            )
+        }
+        Ok(())
+    }
+}

crates/settings/src/settings.rs 🔗

@@ -0,0 +1,262 @@
+mod keymap_file;
+
+use anyhow::Result;
+use gpui::font_cache::{FamilyId, FontCache};
+use schemars::{
+    gen::{SchemaGenerator, SchemaSettings},
+    schema::{
+        InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
+    },
+    JsonSchema,
+};
+use serde::Deserialize;
+use serde_json::Value;
+use std::{collections::HashMap, sync::Arc};
+use theme::{Theme, ThemeRegistry};
+use util::ResultExt as _;
+
+pub use keymap_file::KeymapFile;
+
+#[derive(Clone)]
+pub struct Settings {
+    pub buffer_font_family: FamilyId,
+    pub buffer_font_size: f32,
+    pub vim_mode: bool,
+    pub tab_size: u32,
+    pub soft_wrap: SoftWrap,
+    pub preferred_line_length: u32,
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    pub theme: Arc<Theme>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct LanguageOverride {
+    pub tab_size: Option<u32>,
+    pub soft_wrap: Option<SoftWrap>,
+    pub preferred_line_length: Option<u32>,
+}
+
+#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    PreferredLineLength,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct SettingsFileContent {
+    #[serde(default)]
+    pub buffer_font_family: Option<String>,
+    #[serde(default)]
+    pub buffer_font_size: Option<f32>,
+    #[serde(default)]
+    pub vim_mode: Option<bool>,
+    #[serde(flatten)]
+    pub editor: LanguageOverride,
+    #[serde(default)]
+    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+    #[serde(default)]
+    pub theme: Option<String>,
+}
+
+impl Settings {
+    pub fn new(
+        buffer_font_family: &str,
+        font_cache: &FontCache,
+        theme: Arc<Theme>,
+    ) -> Result<Self> {
+        Ok(Self {
+            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
+            buffer_font_size: 15.,
+            vim_mode: false,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            language_overrides: Default::default(),
+            theme,
+        })
+    }
+
+    pub fn file_json_schema(
+        theme_names: Vec<String>,
+        language_names: Vec<String>,
+    ) -> serde_json::Value {
+        let settings = SchemaSettings::draft07().with(|settings| {
+            settings.option_add_null_type = false;
+        });
+        let generator = SchemaGenerator::new(settings);
+        let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
+
+        // Construct theme names reference type
+        let theme_names = theme_names
+            .into_iter()
+            .map(|name| Value::String(name))
+            .collect();
+        let theme_names_schema = Schema::Object(SchemaObject {
+            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
+            enum_values: Some(theme_names),
+            ..Default::default()
+        });
+        root_schema
+            .definitions
+            .insert("ThemeName".to_owned(), theme_names_schema);
+
+        // Construct language overrides reference type
+        let language_override_schema_reference = Schema::Object(SchemaObject {
+            reference: Some("#/definitions/LanguageOverride".to_owned()),
+            ..Default::default()
+        });
+        let language_overrides_properties = language_names
+            .into_iter()
+            .map(|name| {
+                (
+                    name,
+                    Schema::Object(SchemaObject {
+                        subschemas: Some(Box::new(SubschemaValidation {
+                            all_of: Some(vec![language_override_schema_reference.clone()]),
+                            ..Default::default()
+                        })),
+                        ..Default::default()
+                    }),
+                )
+            })
+            .collect();
+        let language_overrides_schema = Schema::Object(SchemaObject {
+            instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
+            object: Some(Box::new(ObjectValidation {
+                properties: language_overrides_properties,
+                ..Default::default()
+            })),
+            ..Default::default()
+        });
+        root_schema
+            .definitions
+            .insert("LanguageOverrides".to_owned(), language_overrides_schema);
+
+        // Modify theme property to use new theme reference type
+        let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
+        let language_overrides_schema_reference = Schema::Object(SchemaObject {
+            reference: Some("#/definitions/ThemeName".to_owned()),
+            ..Default::default()
+        });
+        settings_file_schema.properties.insert(
+            "theme".to_owned(),
+            Schema::Object(SchemaObject {
+                subschemas: Some(Box::new(SubschemaValidation {
+                    all_of: Some(vec![language_overrides_schema_reference]),
+                    ..Default::default()
+                })),
+                ..Default::default()
+            }),
+        );
+
+        // Modify language_overrides property to use LanguageOverrides reference
+        settings_file_schema.properties.insert(
+            "language_overrides".to_owned(),
+            Schema::Object(SchemaObject {
+                reference: Some("#/definitions/LanguageOverrides".to_owned()),
+                ..Default::default()
+            }),
+        );
+        serde_json::to_value(root_schema).unwrap()
+    }
+
+    pub fn with_overrides(
+        mut self,
+        language_name: impl Into<Arc<str>>,
+        overrides: LanguageOverride,
+    ) -> Self {
+        self.language_overrides
+            .insert(language_name.into(), overrides);
+        self
+    }
+
+    pub fn tab_size(&self, language: Option<&str>) -> u32 {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.tab_size)
+            .unwrap_or(self.tab_size)
+    }
+
+    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.soft_wrap)
+            .unwrap_or(self.soft_wrap)
+    }
+
+    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
+        language
+            .and_then(|language| self.language_overrides.get(language))
+            .and_then(|settings| settings.preferred_line_length)
+            .unwrap_or(self.preferred_line_length)
+    }
+
+    #[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_size: 14.,
+            vim_mode: false,
+            tab_size: 4,
+            soft_wrap: SoftWrap::None,
+            preferred_line_length: 80,
+            language_overrides: Default::default(),
+            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
+        }
+    }
+
+    pub fn merge(
+        &mut self,
+        data: &SettingsFileContent,
+        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() {
+                self.buffer_font_family = id;
+            }
+        }
+        if let Some(value) = &data.theme {
+            if let Some(theme) = theme_registry.get(&value.to_string()).log_err() {
+                self.theme = theme;
+            }
+        }
+
+        merge(&mut self.buffer_font_size, data.buffer_font_size);
+        merge(&mut self.vim_mode, data.vim_mode);
+        merge(&mut self.soft_wrap, data.editor.soft_wrap);
+        merge(&mut self.tab_size, data.editor.tab_size);
+        merge(
+            &mut self.preferred_line_length,
+            data.editor.preferred_line_length,
+        );
+
+        for (language_name, settings) in data.language_overrides.clone().into_iter() {
+            let target = self
+                .language_overrides
+                .entry(language_name.into())
+                .or_default();
+
+            merge_option(&mut target.tab_size, settings.tab_size);
+            merge_option(&mut target.soft_wrap, settings.soft_wrap);
+            merge_option(
+                &mut target.preferred_line_length,
+                settings.preferred_line_length,
+            );
+        }
+    }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}
+
+fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
+    if value.is_some() {
+        *target = value;
+    }
+}

crates/sum_tree/Cargo.toml 🔗

@@ -9,7 +9,7 @@ doctest = false
 
 [dependencies]
 arrayvec = "0.7.1"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 
 [dev-dependencies]
 ctor = "0.1"

crates/text/Cargo.toml 🔗

@@ -17,7 +17,7 @@ sum_tree = { path = "../sum_tree" }
 anyhow = "1.0.38"
 arrayvec = "0.7.1"
 lazy_static = "1.4"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 rand = { version = "0.8.3", optional = true }

crates/text/src/text.rs 🔗

@@ -826,6 +826,8 @@ impl Buffer {
                         edit.timestamp,
                     );
                     self.snapshot.version.observe(edit.timestamp.local());
+                    self.local_clock.observe(edit.timestamp.local());
+                    self.lamport_clock.observe(edit.timestamp.lamport());
                     self.resolve_edit(edit.timestamp.local());
                 }
             }
@@ -836,6 +838,7 @@ impl Buffer {
                 if !self.version.observed(undo.id) {
                     self.apply_undo(&undo)?;
                     self.snapshot.version.observe(undo.id);
+                    self.local_clock.observe(undo.id);
                     self.lamport_clock.observe(lamport_timestamp);
                 }
             }
@@ -1033,8 +1036,6 @@ impl Buffer {
         self.snapshot.visible_text = visible_text;
         self.snapshot.deleted_text = deleted_text;
         self.snapshot.insertions.edit(new_insertions, &());
-        self.local_clock.observe(timestamp.local());
-        self.lamport_clock.observe(timestamp.lamport());
         self.subscriptions.publish_mut(&edits);
     }
 

crates/theme/src/resolution.rs 🔗

@@ -1,497 +0,0 @@
-use anyhow::{anyhow, Result};
-use indexmap::IndexMap;
-use serde_json::Value;
-use std::{
-    cell::RefCell,
-    mem,
-    rc::{Rc, Weak},
-};
-
-pub fn resolve_references(value: Value) -> Result<Value> {
-    let tree = Tree::from_json(value)?;
-    tree.resolve()?;
-    tree.to_json()
-}
-
-#[derive(Clone)]
-enum Node {
-    Reference {
-        path: String,
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-    Object {
-        base: Option<String>,
-        children: IndexMap<String, Tree>,
-        resolved: bool,
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-    Array {
-        children: Vec<Tree>,
-        resolved: bool,
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-    String {
-        value: String,
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-    Number {
-        value: serde_json::Number,
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-    Bool {
-        value: bool,
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-    Null {
-        parent: Option<Weak<RefCell<Node>>>,
-    },
-}
-
-#[derive(Clone)]
-struct Tree(Rc<RefCell<Node>>);
-
-impl Tree {
-    pub fn new(node: Node) -> Self {
-        Self(Rc::new(RefCell::new(node)))
-    }
-
-    fn from_json(value: Value) -> Result<Self> {
-        match value {
-            Value::String(value) => {
-                if let Some(path) = value.strip_prefix("$") {
-                    Ok(Self::new(Node::Reference {
-                        path: path.to_string(),
-                        parent: None,
-                    }))
-                } else {
-                    Ok(Self::new(Node::String {
-                        value,
-                        parent: None,
-                    }))
-                }
-            }
-            Value::Number(value) => Ok(Self::new(Node::Number {
-                value,
-                parent: None,
-            })),
-            Value::Bool(value) => Ok(Self::new(Node::Bool {
-                value,
-                parent: None,
-            })),
-            Value::Null => Ok(Self::new(Node::Null { parent: None })),
-            Value::Object(object) => {
-                let tree = Self::new(Node::Object {
-                    base: Default::default(),
-                    children: Default::default(),
-                    resolved: false,
-                    parent: None,
-                });
-                let mut children = IndexMap::new();
-                let mut resolved = true;
-                let mut base = None;
-                for (key, value) in object.into_iter() {
-                    let value = if key == "extends" {
-                        if value.is_string() {
-                            if let Value::String(value) = value {
-                                base = value.strip_prefix("$").map(str::to_string);
-                                resolved = false;
-                                Self::new(Node::String {
-                                    value,
-                                    parent: None,
-                                })
-                            } else {
-                                unreachable!()
-                            }
-                        } else {
-                            Tree::from_json(value)?
-                        }
-                    } else {
-                        Tree::from_json(value)?
-                    };
-                    value
-                        .0
-                        .borrow_mut()
-                        .set_parent(Some(Rc::downgrade(&tree.0)));
-                    resolved &= value.is_resolved();
-                    children.insert(key.clone(), value);
-                }
-
-                *tree.0.borrow_mut() = Node::Object {
-                    base,
-                    children,
-                    resolved,
-                    parent: None,
-                };
-                Ok(tree)
-            }
-            Value::Array(elements) => {
-                let tree = Self::new(Node::Array {
-                    children: Default::default(),
-                    resolved: false,
-                    parent: None,
-                });
-
-                let mut children = Vec::new();
-                let mut resolved = true;
-                for element in elements {
-                    let child = Tree::from_json(element)?;
-                    child
-                        .0
-                        .borrow_mut()
-                        .set_parent(Some(Rc::downgrade(&tree.0)));
-                    resolved &= child.is_resolved();
-                    children.push(child);
-                }
-
-                *tree.0.borrow_mut() = Node::Array {
-                    children,
-                    resolved,
-                    parent: None,
-                };
-                Ok(tree)
-            }
-        }
-    }
-
-    fn to_json(&self) -> Result<Value> {
-        match &*self.0.borrow() {
-            Node::Reference { .. } => Err(anyhow!("unresolved tree")),
-            Node::String { value, .. } => Ok(Value::String(value.clone())),
-            Node::Number { value, .. } => Ok(Value::Number(value.clone())),
-            Node::Bool { value, .. } => Ok(Value::Bool(*value)),
-            Node::Null { .. } => Ok(Value::Null),
-            Node::Object { children, .. } => {
-                let mut json_children = serde_json::Map::new();
-                for (key, value) in children {
-                    json_children.insert(key.clone(), value.to_json()?);
-                }
-                Ok(Value::Object(json_children))
-            }
-            Node::Array { children, .. } => {
-                let mut json_children = Vec::new();
-                for child in children {
-                    json_children.push(child.to_json()?);
-                }
-                Ok(Value::Array(json_children))
-            }
-        }
-    }
-
-    fn parent(&self) -> Option<Tree> {
-        match &*self.0.borrow() {
-            Node::Reference { parent, .. }
-            | Node::Object { parent, .. }
-            | Node::Array { parent, .. }
-            | Node::String { parent, .. }
-            | Node::Number { parent, .. }
-            | Node::Bool { parent, .. }
-            | Node::Null { parent } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree),
-        }
-    }
-
-    fn get(&self, path: &str) -> Result<Option<Tree>> {
-        let mut tree = self.clone();
-        for component in path.split('.') {
-            let node = tree.0.borrow();
-            match &*node {
-                Node::Object { children, .. } => {
-                    if let Some(subtree) = children.get(component).cloned() {
-                        drop(node);
-                        tree = subtree;
-                    } else {
-                        return Err(anyhow!(
-                            "key \"{}\" does not exist in path \"{}\"",
-                            component,
-                            path
-                        ));
-                    }
-                }
-                Node::Reference { .. } => return Ok(None),
-                Node::Array { .. }
-                | Node::String { .. }
-                | Node::Number { .. }
-                | Node::Bool { .. }
-                | Node::Null { .. } => {
-                    return Err(anyhow!(
-                        "key \"{}\" in path \"{}\" is not an object",
-                        component,
-                        path
-                    ))
-                }
-            }
-        }
-
-        Ok(Some(tree))
-    }
-
-    fn is_resolved(&self) -> bool {
-        match &*self.0.borrow() {
-            Node::Reference { .. } => false,
-            Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved,
-            Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
-                true
-            }
-        }
-    }
-
-    fn update_resolved(&self) {
-        match &mut *self.0.borrow_mut() {
-            Node::Object {
-                resolved,
-                base,
-                children,
-                ..
-            } => {
-                *resolved = base.is_none() && children.values().all(|c| c.is_resolved());
-            }
-            Node::Array {
-                resolved, children, ..
-            } => {
-                *resolved = children.iter().all(|c| c.is_resolved());
-            }
-            _ => {}
-        }
-    }
-
-    pub fn resolve(&self) -> Result<()> {
-        let mut unresolved = vec![self.clone()];
-        let mut made_progress = true;
-
-        while made_progress && !unresolved.is_empty() {
-            made_progress = false;
-            for mut tree in mem::take(&mut unresolved) {
-                made_progress |= tree.resolve_subtree(self, &mut unresolved)?;
-                if tree.is_resolved() {
-                    while let Some(parent) = tree.parent() {
-                        parent.update_resolved();
-                        if !parent.is_resolved() {
-                            break;
-                        }
-                        tree = parent;
-                    }
-                }
-            }
-        }
-
-        if unresolved.is_empty() {
-            Ok(())
-        } else {
-            Err(anyhow!("tree contains cycles"))
-        }
-    }
-
-    fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec<Tree>) -> Result<bool> {
-        let node = self.0.borrow();
-        match &*node {
-            Node::Reference { path, parent } => {
-                if let Some(subtree) = root.get(&path)? {
-                    if subtree.is_resolved() {
-                        let parent = parent.clone();
-                        drop(node);
-                        let mut new_node = subtree.0.borrow().clone();
-                        new_node.set_parent(parent);
-                        *self.0.borrow_mut() = new_node;
-                        Ok(true)
-                    } else {
-                        unresolved.push(self.clone());
-                        Ok(false)
-                    }
-                } else {
-                    unresolved.push(self.clone());
-                    Ok(false)
-                }
-            }
-            Node::Object {
-                base,
-                children,
-                resolved,
-                ..
-            } => {
-                if *resolved {
-                    Ok(false)
-                } else {
-                    let mut made_progress = false;
-                    let mut children_resolved = true;
-                    for child in children.values() {
-                        made_progress |= child.resolve_subtree(root, unresolved)?;
-                        children_resolved &= child.is_resolved();
-                    }
-
-                    if children_resolved {
-                        let mut has_base = false;
-                        let mut resolved_base = None;
-                        if let Some(base) = base {
-                            has_base = true;
-                            if let Some(base) = root.get(base)? {
-                                if base.is_resolved() {
-                                    resolved_base = Some(base);
-                                }
-                            }
-                        }
-
-                        drop(node);
-
-                        if let Some(base) = resolved_base.as_ref() {
-                            self.extend_from(&base);
-                            made_progress = true;
-                        }
-
-                        if let Node::Object { resolved, base, .. } = &mut *self.0.borrow_mut() {
-                            if has_base {
-                                if resolved_base.is_some() {
-                                    base.take();
-                                    *resolved = true;
-                                } else {
-                                    unresolved.push(self.clone());
-                                }
-                            } else {
-                                *resolved = true;
-                            }
-                        }
-                    } else if base.is_some() {
-                        unresolved.push(self.clone());
-                    }
-
-                    Ok(made_progress)
-                }
-            }
-            Node::Array {
-                children, resolved, ..
-            } => {
-                if *resolved {
-                    Ok(false)
-                } else {
-                    let mut made_progress = false;
-                    let mut children_resolved = true;
-                    for child in children.iter() {
-                        made_progress |= child.resolve_subtree(root, unresolved)?;
-                        children_resolved &= child.is_resolved();
-                    }
-
-                    if children_resolved {
-                        drop(node);
-
-                        if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() {
-                            *resolved = true;
-                        }
-                    }
-
-                    Ok(made_progress)
-                }
-            }
-            Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
-                Ok(false)
-            }
-        }
-    }
-
-    fn extend_from(&self, base: &Tree) {
-        if Rc::ptr_eq(&self.0, &base.0) {
-            return;
-        }
-
-        if let (
-            Node::Object { children, .. },
-            Node::Object {
-                children: base_children,
-                ..
-            },
-        ) = (&mut *self.0.borrow_mut(), &*base.0.borrow())
-        {
-            for (key, base_value) in base_children {
-                if let Some(value) = children.get(key) {
-                    value.extend_from(base_value);
-                } else {
-                    let base_value = base_value.clone();
-                    base_value
-                        .0
-                        .borrow_mut()
-                        .set_parent(Some(Rc::downgrade(&self.0)));
-                    children.insert(key.clone(), base_value);
-                }
-            }
-        }
-    }
-}
-
-impl Node {
-    fn set_parent(&mut self, new_parent: Option<Weak<RefCell<Node>>>) {
-        match self {
-            Node::Reference { parent, .. }
-            | Node::Object { parent, .. }
-            | Node::Array { parent, .. }
-            | Node::String { parent, .. }
-            | Node::Number { parent, .. }
-            | Node::Bool { parent, .. }
-            | Node::Null { parent } => *parent = new_parent,
-        }
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    fn test_references() {
-        let json = serde_json::json!({
-            "a": {
-                "extends": "$g",
-                "x": "$b.d"
-            },
-            "b": {
-                "c": "$a",
-                "d": "$e.f"
-            },
-            "e": {
-                "extends": "$a",
-                "f": "1"
-            },
-            "g": {
-                "h": 2
-            }
-        });
-
-        assert_eq!(
-            resolve_references(json).unwrap(),
-            serde_json::json!({
-                "a": {
-                    "extends": "$g",
-                    "x": "1",
-                    "h": 2
-                },
-                "b": {
-                    "c": {
-                        "extends": "$g",
-                        "x": "1",
-                        "h": 2
-                    },
-                    "d": "1"
-                },
-                "e": {
-                    "extends": "$a",
-                    "f": "1",
-                    "x": "1",
-                    "h": 2
-                },
-                "g": {
-                    "h": 2
-                }
-            })
-        )
-    }
-
-    #[test]
-    fn test_cycles() {
-        let json = serde_json::json!({
-            "a": {
-                "b": "$c.d"
-            },
-            "c": {
-                "d": "$a.b",
-            },
-        });
-
-        assert!(resolve_references(json).is_err());
-    }
-}

crates/theme/src/theme.rs 🔗

@@ -1,4 +1,3 @@
-mod resolution;
 mod theme_registry;
 
 use gpui::{
@@ -12,7 +11,7 @@ use std::{collections::HashMap, sync::Arc};
 
 pub use theme_registry::*;
 
-pub const DEFAULT_THEME_NAME: &'static str = "black";
+pub const DEFAULT_THEME_NAME: &'static str = "dark";
 
 #[derive(Deserialize, Default)]
 pub struct Theme {
@@ -22,6 +21,7 @@ pub struct Theme {
     pub chat_panel: ChatPanel,
     pub contacts_panel: ContactsPanel,
     pub project_panel: ProjectPanel,
+    pub command_palette: CommandPalette,
     pub selector: Selector,
     pub editor: Editor,
     pub search: Search,
@@ -190,6 +190,12 @@ pub struct ProjectPanelEntry {
     pub icon_spacing: f32,
 }
 
+#[derive(Debug, Deserialize, Default)]
+pub struct CommandPalette {
+    pub key: ContainedLabel,
+    pub keystroke_spacing: f32,
+}
+
 #[derive(Deserialize, Default)]
 pub struct ContactsPanel {
     #[serde(flatten)]
@@ -262,7 +268,7 @@ pub struct ContainedText {
     pub text: TextStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default)]
 pub struct ContainedLabel {
     #[serde(flatten)]
     pub container: ContainerStyle,

crates/theme/src/theme_registry.rs 🔗

@@ -1,8 +1,8 @@
-use crate::{resolution::resolve_references, Theme};
+use crate::Theme;
 use anyhow::{Context, Result};
 use gpui::{fonts, AssetSource, FontCache};
 use parking_lot::Mutex;
-use serde_json::{Map, Value};
+use serde_json::Value;
 use std::{collections::HashMap, sync::Arc};
 
 pub struct ThemeRegistry {
@@ -25,12 +25,8 @@ impl ThemeRegistry {
     pub fn list(&self) -> impl Iterator<Item = String> {
         self.assets.list("themes/").into_iter().filter_map(|path| {
             let filename = path.strip_prefix("themes/")?;
-            let theme_name = filename.strip_suffix(".toml")?;
-            if theme_name.starts_with('_') {
-                None
-            } else {
-                Some(theme_name.to_string())
-            }
+            let theme_name = filename.strip_suffix(".json")?;
+            Some(theme_name.to_string())
         })
     }
 
@@ -44,9 +40,14 @@ impl ThemeRegistry {
             return Ok(theme.clone());
         }
 
-        let theme_data = self.load(name, true)?;
+        let asset_path = format!("themes/{}.json", name);
+        let theme_json = self
+            .assets
+            .load(&asset_path)
+            .with_context(|| format!("failed to load theme file {}", asset_path))?;
+
         let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || {
-            serde_path_to_error::deserialize(theme_data.as_ref())
+            serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json))
         })?;
 
         theme.name = name.into();
@@ -54,217 +55,4 @@ impl ThemeRegistry {
         self.themes.lock().insert(name.to_string(), theme.clone());
         Ok(theme)
     }
-
-    fn load(&self, name: &str, evaluate_references: bool) -> Result<Arc<Value>> {
-        if let Some(data) = self.theme_data.lock().get(name) {
-            return Ok(data.clone());
-        }
-
-        let asset_path = format!("themes/{}.toml", name);
-        let source_code = self
-            .assets
-            .load(&asset_path)
-            .with_context(|| format!("failed to load theme file {}", asset_path))?;
-
-        let mut theme_data: Map<String, Value> = toml::from_slice(source_code.as_ref())
-            .with_context(|| format!("failed to parse {}.toml", name))?;
-
-        // If this theme extends another base theme, deeply merge it into the base theme's data
-        if let Some(base_name) = theme_data
-            .get("extends")
-            .and_then(|name| name.as_str())
-            .map(str::to_string)
-        {
-            let base_theme_data = self
-                .load(&base_name, false)
-                .with_context(|| format!("failed to load base theme {}", base_name))?
-                .as_ref()
-                .clone();
-            if let Value::Object(mut base_theme_object) = base_theme_data {
-                deep_merge_json(&mut base_theme_object, theme_data);
-                theme_data = base_theme_object;
-            }
-        }
-
-        let mut theme_data = Value::Object(theme_data);
-
-        // Find all of the key path references in the object, and then sort them according
-        // to their dependencies.
-        if evaluate_references {
-            theme_data = resolve_references(theme_data)?;
-        }
-
-        let result = Arc::new(theme_data);
-        self.theme_data
-            .lock()
-            .insert(name.to_string(), result.clone());
-
-        Ok(result)
-    }
-}
-
-fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
-    for (key, extension_value) in extension {
-        if let Value::Object(extension_object) = extension_value {
-            if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) {
-                deep_merge_json(base_object, extension_object);
-            } else {
-                base.insert(key, Value::Object(extension_object));
-            }
-        } else {
-            base.insert(key, extension_value);
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use anyhow::anyhow;
-    use gpui::MutableAppContext;
-
-    #[gpui::test]
-    fn test_theme_extension(cx: &mut MutableAppContext) {
-        let assets = TestAssets(&[
-            (
-                "themes/_base.toml",
-                r##"
-                [ui.active_tab]
-                extends = "$ui.tab"
-                border.color = "#666666"
-                text = "$text_colors.bright"
-
-                [ui.tab]
-                extends = "$ui.element"
-                text = "$text_colors.dull"
-
-                [ui.element]
-                background = "#111111"
-                border = {width = 2.0, color = "#00000000"}
-
-                [editor]
-                background = "#222222"
-                default_text = "$text_colors.regular"
-                "##,
-            ),
-            (
-                "themes/light.toml",
-                r##"
-                extends = "_base"
-
-                [text_colors]
-                bright = "#ffffff"
-                regular = "#eeeeee"
-                dull = "#dddddd"
-
-                [editor]
-                background = "#232323"
-                "##,
-            ),
-        ]);
-
-        let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
-        let theme_data = registry.load("light", true).unwrap();
-
-        assert_eq!(
-            theme_data.as_ref(),
-            &serde_json::json!({
-              "ui": {
-                "active_tab": {
-                  "background": "#111111",
-                  "border": {
-                    "width": 2.0,
-                    "color": "#666666"
-                  },
-                  "extends": "$ui.tab",
-                  "text": "#ffffff"
-                },
-                "tab": {
-                  "background": "#111111",
-                  "border": {
-                    "width": 2.0,
-                    "color": "#00000000"
-                  },
-                  "extends": "$ui.element",
-                  "text": "#dddddd"
-                },
-                "element": {
-                  "background": "#111111",
-                  "border": {
-                    "width": 2.0,
-                    "color": "#00000000"
-                  }
-                }
-              },
-              "editor": {
-                "background": "#232323",
-                "default_text": "#eeeeee"
-              },
-              "extends": "_base",
-              "text_colors": {
-                "bright": "#ffffff",
-                "regular": "#eeeeee",
-                "dull": "#dddddd"
-              }
-            })
-        );
-    }
-
-    #[gpui::test]
-    fn test_nested_extension(cx: &mut MutableAppContext) {
-        let assets = TestAssets(&[(
-            "themes/theme.toml",
-            r##"
-                [a]
-                text = { extends = "$text.0" }
-
-                [b]
-                extends = "$a"
-                text = { extends = "$text.1" }
-
-                [text]
-                0 = { color = "red" }
-                1 = { color = "blue" }
-            "##,
-        )]);
-
-        let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
-        let theme_data = registry.load("theme", true).unwrap();
-        assert_eq!(
-            theme_data
-                .get("b")
-                .unwrap()
-                .get("text")
-                .unwrap()
-                .get("color")
-                .unwrap(),
-            "blue"
-        );
-    }
-
-    struct TestAssets(&'static [(&'static str, &'static str)]);
-
-    impl AssetSource for TestAssets {
-        fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
-            if let Some(row) = self.0.iter().find(|e| e.0 == path) {
-                Ok(row.1.as_bytes().into())
-            } else {
-                Err(anyhow!("no such path {}", path))
-            }
-        }
-
-        fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
-            self.0
-                .iter()
-                .copied()
-                .filter_map(|(path, _)| {
-                    if path.starts_with(prefix) {
-                        Some(path.into())
-                    } else {
-                        None
-                    }
-                })
-                .collect()
-        }
-    }
 }

crates/theme_selector/Cargo.toml 🔗

@@ -11,9 +11,11 @@ doctest = false
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+picker = { path = "../picker" }
 theme = { path = "../theme" }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 smol = "1.2.5"

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,44 +1,30 @@
-use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    action,
-    elements::*,
-    keymap::{self, Binding},
-    AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
-    ViewContext, ViewHandle,
+    actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext,
+    RenderContext, View, ViewContext, ViewHandle,
 };
-use std::{cmp, sync::Arc};
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
+use std::sync::Arc;
 use theme::{Theme, ThemeRegistry};
-use workspace::{
-    menu::{Confirm, SelectNext, SelectPrev},
-    Settings, Workspace,
-};
+use workspace::Workspace;
 
 pub struct ThemeSelector {
-    themes: Arc<ThemeRegistry>,
+    registry: Arc<ThemeRegistry>,
+    theme_names: Vec<String>,
     matches: Vec<StringMatch>,
-    query_editor: ViewHandle<Editor>,
-    list_state: UniformListState,
-    selected_index: usize,
     original_theme: Arc<Theme>,
+    picker: ViewHandle<Picker<Self>>,
     selection_completed: bool,
+    selected_index: usize,
 }
 
-action!(Toggle, Arc<ThemeRegistry>);
-action!(Reload, Arc<ThemeRegistry>);
+actions!(theme_selector, [Toggle, Reload]);
 
-pub fn init(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
-    cx.add_action(ThemeSelector::confirm);
-    cx.add_action(ThemeSelector::select_prev);
-    cx.add_action(ThemeSelector::select_next);
+pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ThemeSelector::toggle);
     cx.add_action(ThemeSelector::reload);
-
-    cx.add_bindings(vec![
-        Binding::new("cmd-k cmd-t", Toggle(themes.clone()), None),
-        Binding::new("cmd-k t", Reload(themes.clone()), None),
-        Binding::new("escape", Toggle(themes.clone()), Some("ThemeSelector")),
-    ]);
+    Picker::<ThemeSelector>::init(cx);
 }
 
 pub enum Event {
@@ -47,44 +33,46 @@ pub enum Event {
 
 impl ThemeSelector {
     fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
-        let query_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
-        });
-
-        cx.subscribe(&query_editor, Self::on_query_editor_event)
-            .detach();
-
+        let handle = cx.weak_handle();
+        let picker = cx.add_view(|cx| Picker::new(handle, cx));
         let original_theme = cx.global::<Settings>().theme.clone();
-
+        let theme_names = registry.list().collect::<Vec<_>>();
+        let matches = theme_names
+            .iter()
+            .map(|name| StringMatch {
+                candidate_id: 0,
+                score: 0.0,
+                positions: Default::default(),
+                string: name.clone(),
+            })
+            .collect();
         let mut this = Self {
-            themes: registry,
-            query_editor,
-            matches: Vec::new(),
-            list_state: Default::default(),
-            selected_index: 0, // Default index for now
+            registry,
+            theme_names,
+            matches,
+            picker,
             original_theme: original_theme.clone(),
+            selected_index: 0,
             selection_completed: false,
         };
-        this.update_matches(cx);
-
-        // Set selected index to current theme
         this.select_if_matching(&original_theme.name);
-
         this
     }
 
-    fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
+    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+        let themes = workspace.themes();
         workspace.toggle_modal(cx, |cx, _| {
-            let selector = cx.add_view(|cx| Self::new(action.0.clone(), cx));
-            cx.subscribe(&selector, Self::on_event).detach();
-            selector
+            let this = cx.add_view(|cx| Self::new(themes, cx));
+            cx.subscribe(&this, Self::on_event).detach();
+            this
         });
     }
 
-    fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
+    fn reload(workspace: &mut Workspace, _: &Reload, cx: &mut ViewContext<Workspace>) {
         let current_theme_name = cx.global::<Settings>().theme.name.clone();
-        action.0.clear();
-        match action.0.get(&current_theme_name) {
+        let themes = workspace.themes();
+        themes.clear();
+        match themes.get(&current_theme_name) {
             Ok(theme) => {
                 Self::set_theme(theme, cx);
                 log::info!("reloaded theme {}", current_theme_name);
@@ -95,36 +83,9 @@ impl ThemeSelector {
         }
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        self.selection_completed = true;
-        cx.emit(Event::Dismissed);
-    }
-
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if self.selected_index > 0 {
-            self.selected_index -= 1;
-        }
-        self.list_state
-            .scroll_to(ScrollTarget::Show(self.selected_index));
-
-        self.show_selected_theme(cx);
-        cx.notify();
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if self.selected_index + 1 < self.matches.len() {
-            self.selected_index += 1;
-        }
-        self.list_state
-            .scroll_to(ScrollTarget::Show(self.selected_index));
-
-        self.show_selected_theme(cx);
-        cx.notify();
-    }
-
     fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
-            match self.themes.get(&mat.string) {
+            match self.registry.get(&mat.string) {
                 Ok(theme) => Self::set_theme(theme, cx),
                 Err(error) => {
                     log::error!("error loading theme {}: {}", mat.string, error)
@@ -141,49 +102,6 @@ impl ThemeSelector {
             .unwrap_or(self.selected_index);
     }
 
-    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
-        let background = cx.background().clone();
-        let candidates = self
-            .themes
-            .list()
-            .enumerate()
-            .map(|(id, name)| StringMatchCandidate {
-                id,
-                char_bag: name.as_str().into(),
-                string: name,
-            })
-            .collect::<Vec<_>>();
-        let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
-
-        self.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 {
-            smol::block_on(match_strings(
-                &candidates,
-                &query,
-                false,
-                100,
-                &Default::default(),
-                background,
-            ))
-        };
-
-        self.selected_index = self
-            .selected_index
-            .min(self.matches.len().saturating_sub(1));
-
-        cx.notify();
-    }
-
     fn on_event(
         workspace: &mut Workspace,
         _: ViewHandle<ThemeSelector>,
@@ -197,89 +115,104 @@ impl ThemeSelector {
         }
     }
 
-    fn on_query_editor_event(
-        &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            editor::Event::BufferEdited { .. } => {
-                self.update_matches(cx);
-                self.select_if_matching(&cx.global::<Settings>().theme.name);
-                self.show_selected_theme(cx);
-            }
-            editor::Event::Blurred => cx.emit(Event::Dismissed),
-            _ => {}
-        }
+    fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
+            settings.theme = theme;
+            cx.refresh_windows();
+        });
     }
+}
 
-    fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
-        if self.matches.is_empty() {
-            let settings = cx.global::<Settings>();
-            return Container::new(
-                Label::new(
-                    "No matches".into(),
-                    settings.theme.selector.empty.label.clone(),
-                )
-                .boxed(),
-            )
-            .with_style(settings.theme.selector.empty.container)
-            .named("empty matches");
+impl PickerDelegate for ThemeSelector {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        self.selection_completed = true;
+        cx.emit(Event::Dismissed);
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        if !self.selection_completed {
+            Self::set_theme(self.original_theme.clone(), cx);
+            self.selection_completed = true;
         }
+        cx.emit(Event::Dismissed);
+    }
 
-        let handle = cx.handle();
-        let list =
-            UniformList::new(
-                self.list_state.clone(),
-                self.matches.len(),
-                move |mut range, items, cx| {
-                    let cx = cx.as_ref();
-                    let selector = handle.upgrade(cx).unwrap();
-                    let selector = selector.read(cx);
-                    let start = range.start;
-                    range.end = cmp::min(range.end, selector.matches.len());
-                    items.extend(selector.matches[range].iter().enumerate().map(
-                        move |(i, path_match)| selector.render_match(path_match, start + i, cx),
-                    ));
-                },
-            );
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
 
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        self.selected_index = ix;
+        self.show_selected_theme(cx);
     }
 
-    fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        let theme = &settings.theme;
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+        let background = cx.background().clone();
+        let candidates = self
+            .theme_names
+            .iter()
+            .enumerate()
+            .map(|(id, name)| StringMatchCandidate {
+                id,
+                char_bag: name.as_str().into(),
+                string: name.clone(),
+            })
+            .collect::<Vec<_>>();
 
-        let container = Container::new(
-            Label::new(
-                theme_match.string.clone(),
-                if index == self.selected_index {
-                    theme.selector.active_item.label.clone()
-                } else {
-                    theme.selector.item.label.clone()
-                },
-            )
-            .with_highlights(theme_match.positions.clone())
-            .boxed(),
-        )
-        .with_style(if index == self.selected_index {
-            theme.selector.active_item.container
-        } else {
-            theme.selector.item.container
-        });
+        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
+            };
 
-        container.boxed()
+            this.update(&mut cx, |this, cx| {
+                this.matches = matches;
+                this.selected_index = this
+                    .selected_index
+                    .min(this.matches.len().saturating_sub(1));
+                this.show_selected_theme(cx);
+                cx.notify();
+            });
+        })
     }
 
-    fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
-        cx.update_global::<Settings, _, _>(|settings, cx| {
-            settings.theme = theme;
-            cx.refresh_windows();
-        });
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+        let settings = cx.global::<Settings>();
+        let theme = &settings.theme;
+        let theme_match = &self.matches[ix];
+        let style = if selected {
+            &theme.selector.active_item
+        } else {
+            &theme.selector.item
+        };
+
+        Label::new(theme_match.string.clone(), style.label.clone())
+            .with_highlights(theme_match.positions.clone())
+            .contained()
+            .with_style(style.container)
+            .boxed()
     }
 }
 
@@ -298,43 +231,11 @@ impl View for ThemeSelector {
         "ThemeSelector"
     }
 
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.global::<Settings>().theme.clone();
-        Align::new(
-            ConstrainedBox::new(
-                Container::new(
-                    Flex::new(Axis::Vertical)
-                        .with_child(
-                            ChildView::new(&self.query_editor)
-                                .contained()
-                                .with_style(theme.selector.input_editor.container)
-                                .boxed(),
-                        )
-                        .with_child(
-                            FlexItem::new(self.render_matches(cx))
-                                .flex(1., false)
-                                .boxed(),
-                        )
-                        .boxed(),
-                )
-                .with_style(theme.selector.container)
-                .boxed(),
-            )
-            .with_max_width(600.0)
-            .with_max_height(400.0)
-            .boxed(),
-        )
-        .top()
-        .named("theme selector")
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.picker.clone()).boxed()
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.focus(&self.query_editor);
-    }
-
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
-        let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
-        cx
+        cx.focus(&self.picker);
     }
 }

crates/util/Cargo.toml 🔗

@@ -12,7 +12,7 @@ test-support = ["rand", "serde_json", "tempdir"]
 [dependencies]
 anyhow = "1.0.38"
 futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 rand = { version = "0.8", optional = true }
 surf = "2.2"
 tempdir = { version = "0.3.7", optional = true }

crates/vim/Cargo.toml 🔗

@@ -8,18 +8,22 @@ path = "src/vim.rs"
 doctest = false
 
 [dependencies]
+assets = { path = "../assets" }
 collections = { path = "../collections" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+serde = { version = "1", features = ["derive"] }
+settings = { path = "../settings" }
 workspace = { path = "../workspace" }
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 
 [dev-dependencies]
 indoc = "1.0.4"
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
+settings = { path = "../settings" }
 workspace = { path = "../workspace", features = ["test-support"] }

crates/vim/src/editor_events.rs 🔗

@@ -1,7 +1,7 @@
 use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
 use gpui::MutableAppContext;
 
-use crate::{mode::Mode, SwitchMode, VimState};
+use crate::{state::Mode, Vim};
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.subscribe_global(editor_created).detach();
@@ -11,9 +11,9 @@ pub fn init(cx: &mut MutableAppContext) {
 }
 
 fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
-    cx.update_default_global(|vim_state: &mut VimState, cx| {
-        vim_state.editors.insert(editor.id(), editor.downgrade());
-        vim_state.sync_editor_options(cx);
+    cx.update_default_global(|vim: &mut Vim, cx| {
+        vim.editors.insert(editor.id(), editor.downgrade());
+        vim.sync_editor_options(cx);
     })
 }
 
@@ -21,17 +21,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
     let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
         Mode::Insert
     } else {
-        Mode::normal()
+        Mode::Normal
     };
 
-    VimState::update_global(cx, |state, cx| {
+    Vim::update(cx, |state, cx| {
         state.active_editor = Some(editor.downgrade());
-        state.switch_mode(&SwitchMode(mode), cx);
+        state.switch_mode(mode, cx);
     });
 }
 
 fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
-    VimState::update_global(cx, |state, cx| {
+    Vim::update(cx, |state, cx| {
         if let Some(previous_editor) = state.active_editor.clone() {
             if previous_editor == editor.clone() {
                 state.active_editor = None;
@@ -42,11 +42,11 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont
 }
 
 fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
-    cx.update_default_global(|vim_state: &mut VimState, _| {
-        vim_state.editors.remove(&editor.id());
-        if let Some(previous_editor) = vim_state.active_editor.clone() {
+    cx.update_default_global(|vim: &mut Vim, _| {
+        vim.editors.remove(&editor.id());
+        if let Some(previous_editor) = vim.active_editor.clone() {
             if previous_editor == editor.clone() {
-                vim_state.active_editor = None;
+                vim.active_editor = None;
             }
         }
     });

crates/vim/src/insert.rs 🔗

@@ -1,47 +1,40 @@
+use crate::{state::Mode, Vim};
 use editor::Bias;
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
 
-use crate::{mode::Mode, SwitchMode, VimState};
-
-action!(NormalBefore);
+actions!(vim, [NormalBefore]);
 
 pub fn init(cx: &mut MutableAppContext) {
-    let context = Some("Editor && vim_mode == insert");
-    cx.add_bindings(vec![
-        Binding::new("escape", NormalBefore, context),
-        Binding::new("ctrl-c", NormalBefore, context),
-    ]);
-
     cx.add_action(normal_before);
 }
 
 fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
+    Vim::update(cx, |state, cx| {
         state.update_active_editor(cx, |editor, cx| {
             editor.move_cursors(cx, |map, mut cursor, _| {
                 *cursor.column_mut() = cursor.column().saturating_sub(1);
                 (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
             });
         });
-        state.switch_mode(&SwitchMode(Mode::normal()), cx);
+        state.switch_mode(Mode::Normal, cx);
     })
 }
 
 #[cfg(test)]
 mod test {
-    use crate::{mode::Mode, vim_test_context::VimTestContext};
+    use crate::{state::Mode, vim_test_context::VimTestContext};
 
     #[gpui::test]
     async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true, "").await;
         cx.simulate_keystroke("i");
         assert_eq!(cx.mode(), Mode::Insert);
-        cx.simulate_keystrokes(&["T", "e", "s", "t"]);
+        cx.simulate_keystrokes(["T", "e", "s", "t"]);
         cx.assert_editor_state("Test|");
         cx.simulate_keystroke("escape");
-        assert_eq!(cx.mode(), Mode::normal());
+        assert_eq!(cx.mode(), Mode::Normal);
         cx.assert_editor_state("Tes|t");
     }
 }

crates/vim/src/mode.rs 🔗

@@ -1,72 +0,0 @@
-use editor::CursorShape;
-use gpui::keymap::Context;
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Mode {
-    Normal(NormalState),
-    Insert,
-}
-
-impl Mode {
-    pub fn cursor_shape(&self) -> CursorShape {
-        match self {
-            Mode::Normal(_) => CursorShape::Block,
-            Mode::Insert => CursorShape::Bar,
-        }
-    }
-
-    pub fn keymap_context_layer(&self) -> Context {
-        let mut context = Context::default();
-        context.map.insert(
-            "vim_mode".to_string(),
-            match self {
-                Self::Normal(_) => "normal",
-                Self::Insert => "insert",
-            }
-            .to_string(),
-        );
-
-        match self {
-            Self::Normal(normal_state) => normal_state.set_context(&mut context),
-            _ => {}
-        }
-        context
-    }
-
-    pub fn normal() -> Mode {
-        Mode::Normal(Default::default())
-    }
-}
-
-impl Default for Mode {
-    fn default() -> Self {
-        Self::Normal(Default::default())
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum NormalState {
-    None,
-    GPrefix,
-}
-
-impl NormalState {
-    pub fn set_context(&self, context: &mut Context) {
-        let submode = match self {
-            Self::GPrefix => Some("g"),
-            _ => None,
-        };
-
-        if let Some(submode) = submode {
-            context
-                .map
-                .insert("vim_submode".to_string(), submode.to_string());
-        }
-    }
-}
-
-impl Default for NormalState {
-    fn default() -> Self {
-        NormalState::None
-    }
-}

crates/vim/src/motion.rs 🔗

@@ -0,0 +1,271 @@
+use editor::{
+    char_kind,
+    display_map::{DisplaySnapshot, ToDisplayPoint},
+    movement, Bias, DisplayPoint,
+};
+use gpui::{actions, impl_actions, MutableAppContext};
+use language::SelectionGoal;
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{
+    normal::normal_motion,
+    state::{Mode, Operator},
+    Vim,
+};
+
+#[derive(Copy, Clone)]
+pub enum Motion {
+    Left,
+    Down,
+    Up,
+    Right,
+    NextWordStart {
+        ignore_punctuation: bool,
+        stop_at_newline: bool,
+    },
+    NextWordEnd {
+        ignore_punctuation: bool,
+    },
+    PreviousWordStart {
+        ignore_punctuation: bool,
+    },
+    StartOfLine,
+    EndOfLine,
+    StartOfDocument,
+    EndOfDocument,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct NextWordStart {
+    #[serde(default)]
+    ignore_punctuation: bool,
+    #[serde(default)]
+    stop_at_newline: bool,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct NextWordEnd {
+    #[serde(default)]
+    ignore_punctuation: bool,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PreviousWordStart {
+    #[serde(default)]
+    ignore_punctuation: bool,
+}
+
+actions!(
+    vim,
+    [
+        Left,
+        Down,
+        Up,
+        Right,
+        StartOfLine,
+        EndOfLine,
+        StartOfDocument,
+        EndOfDocument
+    ]
+);
+impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
+    cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
+    cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
+    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
+    cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
+    cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
+    cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
+        motion(Motion::StartOfDocument, cx)
+    });
+    cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
+
+    cx.add_action(
+        |_: &mut Workspace,
+         &NextWordStart {
+             ignore_punctuation,
+             stop_at_newline,
+         }: &NextWordStart,
+         cx: _| {
+            motion(
+                Motion::NextWordStart {
+                    ignore_punctuation,
+                    stop_at_newline,
+                },
+                cx,
+            )
+        },
+    );
+    cx.add_action(
+        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
+            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
+        },
+    );
+    cx.add_action(
+        |_: &mut Workspace,
+         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
+         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
+    );
+}
+
+fn motion(motion: Motion, cx: &mut MutableAppContext) {
+    Vim::update(cx, |vim, cx| {
+        if let Some(Operator::Namespace(_)) = vim.active_operator() {
+            vim.pop_operator(cx);
+        }
+    });
+    match Vim::read(cx).state.mode {
+        Mode::Normal => normal_motion(motion, cx),
+        Mode::Insert => {
+            // Shouldn't execute a motion in insert mode. Ignoring
+        }
+    }
+}
+
+impl Motion {
+    pub fn move_point(
+        self,
+        map: &DisplaySnapshot,
+        point: DisplayPoint,
+        goal: SelectionGoal,
+        block_cursor_positioning: bool,
+    ) -> (DisplayPoint, SelectionGoal) {
+        use Motion::*;
+        match self {
+            Left => (left(map, point), SelectionGoal::None),
+            Down => movement::down(map, point, goal),
+            Up => movement::up(map, point, goal),
+            Right => (right(map, point), SelectionGoal::None),
+            NextWordStart {
+                ignore_punctuation,
+                stop_at_newline,
+            } => (
+                next_word_start(map, point, ignore_punctuation, stop_at_newline),
+                SelectionGoal::None,
+            ),
+            NextWordEnd { ignore_punctuation } => (
+                next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
+                SelectionGoal::None,
+            ),
+            PreviousWordStart { ignore_punctuation } => (
+                previous_word_start(map, point, ignore_punctuation),
+                SelectionGoal::None,
+            ),
+            StartOfLine => (start_of_line(map, point), SelectionGoal::None),
+            EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+            StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
+            EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
+        }
+    }
+
+    pub fn line_wise(self) -> bool {
+        use Motion::*;
+        match self {
+            Down | Up | StartOfDocument | EndOfDocument => true,
+            _ => false,
+        }
+    }
+}
+
+fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    *point.column_mut() = point.column().saturating_sub(1);
+    map.clip_point(point, Bias::Left)
+}
+
+fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+    *point.column_mut() += 1;
+    map.clip_point(point, Bias::Right)
+}
+
+fn next_word_start(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    ignore_punctuation: bool,
+    stop_at_newline: bool,
+) -> DisplayPoint {
+    let mut crossed_newline = false;
+    movement::find_boundary(map, point, |left, right| {
+        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+        let at_newline = right == '\n';
+
+        let found = (left_kind != right_kind && !right.is_whitespace())
+            || (at_newline && (crossed_newline || stop_at_newline))
+            || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
+
+        if at_newline {
+            crossed_newline = true;
+        }
+        found
+    })
+}
+
+fn next_word_end(
+    map: &DisplaySnapshot,
+    mut point: DisplayPoint,
+    ignore_punctuation: bool,
+    before_end_character: bool,
+) -> DisplayPoint {
+    *point.column_mut() += 1;
+    point = movement::find_boundary(map, point, |left, right| {
+        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+        left_kind != right_kind && !left.is_whitespace()
+    });
+    // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
+    // we have backtraced already
+    if before_end_character
+        && !map
+            .chars_at(point)
+            .skip(1)
+            .next()
+            .map(|c| c == '\n')
+            .unwrap_or(true)
+    {
+        *point.column_mut() = point.column().saturating_sub(1);
+    }
+    map.clip_point(point, Bias::Left)
+}
+
+fn previous_word_start(
+    map: &DisplaySnapshot,
+    mut point: DisplayPoint,
+    ignore_punctuation: bool,
+) -> DisplayPoint {
+    // This works even though find_preceding_boundary is called for every character in the line containing
+    // cursor because the newline is checked only once.
+    point = movement::find_preceding_boundary(map, point, |left, right| {
+        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+        (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
+    });
+    point
+}
+
+fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    map.prev_line_boundary(point.to_point(map)).1
+}
+
+fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
+}
+
+fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let mut new_point = 0usize.to_display_point(map);
+    *new_point.column_mut() = point.column();
+    map.clip_point(new_point, Bias::Left)
+}
+
+fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+    let mut new_point = map.max_point();
+    *new_point.column_mut() = point.column();
+    map.clip_point(new_point, Bias::Left)
+}

crates/vim/src/normal.rs 🔗

@@ -1,187 +1,100 @@
-mod g_prefix;
-
-use editor::{char_kind, movement, Bias};
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use crate::{
+    motion::Motion,
+    state::{Mode, Operator},
+    Vim,
+};
+use editor::Bias;
+use gpui::MutableAppContext;
 use language::SelectionGoal;
-use workspace::Workspace;
-
-use crate::{mode::NormalState, Mode, SwitchMode, VimState};
-
-action!(GPrefix);
-action!(MoveLeft);
-action!(MoveDown);
-action!(MoveUp);
-action!(MoveRight);
-action!(MoveToStartOfLine);
-action!(MoveToEndOfLine);
-action!(MoveToEnd);
-action!(MoveToNextWordStart, bool);
-action!(MoveToNextWordEnd, bool);
-action!(MoveToPreviousWordStart, bool);
-
-pub fn init(cx: &mut MutableAppContext) {
-    let context = Some("Editor && vim_mode == normal");
-    cx.add_bindings(vec![
-        Binding::new("i", SwitchMode(Mode::Insert), context),
-        Binding::new("g", SwitchMode(Mode::Normal(NormalState::GPrefix)), context),
-        Binding::new("h", MoveLeft, context),
-        Binding::new("j", MoveDown, context),
-        Binding::new("k", MoveUp, context),
-        Binding::new("l", MoveRight, context),
-        Binding::new("0", MoveToStartOfLine, context),
-        Binding::new("shift-$", MoveToEndOfLine, context),
-        Binding::new("shift-G", MoveToEnd, context),
-        Binding::new("w", MoveToNextWordStart(false), context),
-        Binding::new("shift-W", MoveToNextWordStart(true), context),
-        Binding::new("e", MoveToNextWordEnd(false), context),
-        Binding::new("shift-E", MoveToNextWordEnd(true), context),
-        Binding::new("b", MoveToPreviousWordStart(false), context),
-        Binding::new("shift-B", MoveToPreviousWordStart(true), context),
-    ]);
-    g_prefix::init(cx);
-
-    cx.add_action(move_left);
-    cx.add_action(move_down);
-    cx.add_action(move_up);
-    cx.add_action(move_right);
-    cx.add_action(move_to_start_of_line);
-    cx.add_action(move_to_end_of_line);
-    cx.add_action(move_to_end);
-    cx.add_action(move_to_next_word_start);
-    cx.add_action(move_to_next_word_end);
-    cx.add_action(move_to_previous_word_start);
-}
-
-fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, mut cursor, _| {
-                *cursor.column_mut() = cursor.column().saturating_sub(1);
-                (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
-            });
-        });
-    })
-}
-
-fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, movement::down);
-        });
-    });
-}
-
-fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, movement::up);
-        });
-    });
-}
 
-fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, mut cursor, _| {
-                *cursor.column_mut() += 1;
-                (map.clip_point(cursor, Bias::Right), SelectionGoal::None)
-            });
-        });
+pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
+    Vim::update(cx, |vim, cx| {
+        match vim.state.operator_stack.pop() {
+            None => move_cursor(vim, motion, cx),
+            Some(Operator::Change) => change_over(vim, motion, cx),
+            Some(Operator::Delete) => delete_over(vim, motion, cx),
+            Some(Operator::Namespace(_)) => {
+                // Can't do anything for a namespace operator. Ignoring
+            }
+        }
+        vim.clear_operator(cx);
     });
 }
 
-fn move_to_start_of_line(
-    _: &mut Workspace,
-    _: &MoveToStartOfLine,
-    cx: &mut ViewContext<Workspace>,
-) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, cursor, _| {
-                (
-                    movement::line_beginning(map, cursor, false),
-                    SelectionGoal::None,
-                )
-            });
-        });
+fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.move_cursors(cx, |map, cursor, goal| {
+            motion.move_point(map, cursor, goal, true)
+        })
     });
 }
 
-fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, cursor, _| {
-                (
-                    map.clip_point(movement::line_end(map, cursor, false), Bias::Left),
-                    SelectionGoal::None,
-                )
+fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.transact(cx, |editor, cx| {
+            // Don't clip at line ends during change operation
+            editor.set_clip_at_line_ends(false, cx);
+            editor.move_selections(cx, |map, selection| {
+                let (head, goal) = motion.move_point(map, selection.head(), selection.goal, false);
+                selection.set_head(head, goal);
+
+                if motion.line_wise() {
+                    selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
+                    selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
+                }
             });
+            editor.set_clip_at_line_ends(true, cx);
+            editor.insert(&"", cx);
         });
     });
+    vim.switch_mode(Mode::Insert, cx)
 }
 
-fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left));
-        });
-    });
-}
-
-fn move_to_next_word_start(
-    _: &mut Workspace,
-    &MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart,
-    cx: &mut ViewContext<Workspace>,
-) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, mut cursor, _| {
-                let mut crossed_newline = false;
-                cursor = movement::find_boundary(map, cursor, |left, right| {
-                    let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
-                    let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
-                    let at_newline = right == '\n';
-
-                    let found = (left_kind != right_kind && !right.is_whitespace())
-                        || (at_newline && crossed_newline)
-                        || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
-
-                    if at_newline {
-                        crossed_newline = true;
+fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.transact(cx, |editor, cx| {
+            // Use goal column to preserve previous position
+            editor.set_clip_at_line_ends(false, cx);
+            editor.move_selections(cx, |map, selection| {
+                let original_head = selection.head();
+                let (head, _) = motion.move_point(map, selection.head(), selection.goal, false);
+                // Set the goal column to the original position in order to fix it up
+                // after the deletion
+                selection.set_head(head, SelectionGoal::Column(original_head.column()));
+
+                if motion.line_wise() {
+                    if selection.end.row() == map.max_point().row() {
+                        // Delete previous line break since we are at the end of the document
+                        if selection.start.row() > 0 {
+                            *selection.start.row_mut() = selection.start.row().saturating_sub(1);
+                            selection.start = map.clip_point(selection.start, Bias::Left);
+                            selection.start =
+                                map.next_line_boundary(selection.start.to_point(map)).1;
+                        } else {
+                            // Selection covers the whole document. Just delete to the start of the
+                            // line.
+                            selection.start =
+                                map.prev_line_boundary(selection.start.to_point(map)).1;
+                        }
+                        selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
+                    } else {
+                        // Delete next line break so that we leave the previous line alone
+                        selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
+                        *selection.end.column_mut() = 0;
+                        *selection.end.row_mut() += 1;
+                        selection.end = map.clip_point(selection.end, Bias::Left);
                     }
-                    found
-                });
-                (cursor, SelectionGoal::None)
+                }
             });
-        });
-    });
-}
-
-fn move_to_next_word_end(
-    _: &mut Workspace,
-    &MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd,
-    cx: &mut ViewContext<Workspace>,
-) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, mut cursor, _| {
-                *cursor.column_mut() += 1;
-                cursor = movement::find_boundary(map, cursor, |left, right| {
-                    let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
-                    let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
-
-                    left_kind != right_kind && !left.is_whitespace()
-                });
-                // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
-                // we have backtraced already
-                if !map
-                    .chars_at(cursor)
-                    .skip(1)
-                    .next()
-                    .map(|c| c == '\n')
-                    .unwrap_or(true)
-                {
-                    *cursor.column_mut() = cursor.column().saturating_sub(1);
+            editor.insert(&"", cx);
+
+            // Fixup cursor position after the deletion
+            editor.set_clip_at_line_ends(true, cx);
+            editor.move_cursors(cx, |map, mut cursor, goal| {
+                if motion.line_wise() {
+                    if let SelectionGoal::Column(column) = goal {
+                        *cursor.column_mut() = column
+                    }
                 }
                 (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
             });
@@ -189,34 +102,18 @@ fn move_to_next_word_end(
     });
 }
 
-fn move_to_previous_word_start(
-    _: &mut Workspace,
-    &MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart,
-    cx: &mut ViewContext<Workspace>,
-) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_cursors(cx, |map, mut cursor, _| {
-                // This works even though find_preceding_boundary is called for every character in the line containing
-                // cursor because the newline is checked only once.
-                cursor = movement::find_preceding_boundary(map, cursor, |left, right| {
-                    let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
-                    let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
-
-                    (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
-                });
-                (cursor, SelectionGoal::None)
-            });
-        });
-    });
-}
-
 #[cfg(test)]
 mod test {
     use indoc::indoc;
     use util::test::marked_text;
 
-    use crate::vim_test_context::VimTestContext;
+    use crate::{
+        state::{
+            Mode::{self, *},
+            Namespace, Operator,
+        },
+        vim_test_context::VimTestContext,
+    };
 
     #[gpui::test]
     async fn test_hjkl(cx: &mut gpui::TestAppContext) {
@@ -322,19 +219,22 @@ mod test {
 
     #[gpui::test]
     async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
-        let initial_content = indoc! {"
-            The quick
+        let mut cx = VimTestContext::new(cx, true, "").await;
+
+        cx.set_state(
+            indoc! {"
+            The |quick
             
             brown fox jumps
-            over the lazy dog"};
-        let mut cx = VimTestContext::new(cx, true, initial_content).await;
-
+            over the lazy dog"},
+            Mode::Normal,
+        );
         cx.simulate_keystroke("shift-G");
         cx.assert_editor_state(indoc! {"
             The quick
             
             brown fox jumps
-            over the lazy do|g"});
+            over| the lazy dog"});
 
         // Repeat the action doesn't move
         cx.simulate_keystroke("shift-G");
@@ -342,7 +242,7 @@ mod test {
             The quick
             
             brown fox jumps
-            over the lazy do|g"});
+            over| the lazy dog"});
     }
 
     #[gpui::test]
@@ -361,7 +261,7 @@ mod test {
         }
 
         // Reset and test ignoring punctuation
-        cx.simulate_keystrokes(&["g", "g"]);
+        cx.simulate_keystrokes(["g", "g", "0"]);
         let (_, cursor_offsets) = marked_text(indoc! {"
             The |quick-brown
             |
@@ -391,7 +291,7 @@ mod test {
         }
 
         // Reset and test ignoring punctuation
-        cx.simulate_keystrokes(&["g", "g"]);
+        cx.simulate_keystrokes(["g", "g", "0"]);
         let (_, cursor_offsets) = marked_text(indoc! {"
             Th|e quick-brow|n
             
@@ -413,7 +313,7 @@ mod test {
             |fox_jumps |over
             |the"});
         let mut cx = VimTestContext::new(cx, true, &initial_content).await;
-        cx.simulate_keystroke("shift-G");
+        cx.simulate_keystrokes(["shift-G", "shift-$"]);
 
         for cursor_offset in cursor_offsets.into_iter().rev() {
             cx.simulate_keystroke("b");
@@ -421,7 +321,7 @@ mod test {
         }
 
         // Reset and test ignoring punctuation
-        cx.simulate_keystroke("shift-G");
+        cx.simulate_keystrokes(["shift-G", "shift-$"]);
         let (_, cursor_offsets) = marked_text(indoc! {"
             ||The |quick-brown
             |
@@ -433,4 +333,474 @@ mod test {
             cx.assert_newest_selection_head_offset(cursor_offset);
         }
     }
+
+    #[gpui::test]
+    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true, "").await;
+
+        // Can abort with escape to get back to normal mode
+        cx.simulate_keystroke("g");
+        assert_eq!(cx.mode(), Normal);
+        assert_eq!(
+            cx.active_operator(),
+            Some(Operator::Namespace(Namespace::G))
+        );
+        cx.simulate_keystroke("escape");
+        assert_eq!(cx.mode(), Normal);
+        assert_eq!(cx.active_operator(), None);
+    }
+
+    #[gpui::test]
+    async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true, "").await;
+
+        cx.set_state(
+            indoc! {"
+            The q|uick
+            
+            brown fox jumps
+            over the lazy dog"},
+            Mode::Normal,
+        );
+
+        // Jump to the end to
+        cx.simulate_keystroke("shift-G");
+        cx.assert_editor_state(indoc! {"
+            The quick
+            
+            brown fox jumps
+            over |the lazy dog"});
+
+        // Jump to the start
+        cx.simulate_keystrokes(["g", "g"]);
+        cx.assert_editor_state(indoc! {"
+            The q|uick
+            
+            brown fox jumps
+            over the lazy dog"});
+        assert_eq!(cx.mode(), Normal);
+        assert_eq!(cx.active_operator(), None);
+
+        // Repeat action doesn't change
+        cx.simulate_keystrokes(["g", "g"]);
+        cx.assert_editor_state(indoc! {"
+            The q|uick
+            
+            brown fox jumps
+            over the lazy dog"});
+        assert_eq!(cx.mode(), Normal);
+        assert_eq!(cx.active_operator(), None);
+    }
+
+    #[gpui::test]
+    async fn test_change(cx: &mut gpui::TestAppContext) {
+        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+            cx.assert_binding(
+                ["c", motion],
+                initial_state,
+                Mode::Normal,
+                state_after,
+                Mode::Insert,
+            );
+        }
+        let cx = &mut VimTestContext::new(cx, true, "").await;
+        assert("h", "Te|st", "T|st", cx);
+        assert("l", "Te|st", "Te|t", cx);
+        assert("w", "|Test", "|", cx);
+        assert("w", "Te|st", "Te|", cx);
+        assert("w", "Te|st Test", "Te| Test", cx);
+        assert("e", "Te|st Test", "Te| Test", cx);
+        assert("b", "Te|st", "|st", cx);
+        assert("b", "Test Te|st", "Test |st", cx);
+        assert(
+            "w",
+            indoc! {"
+            The quick
+            brown |fox
+            jumps over"},
+            indoc! {"
+            The quick
+            brown |
+            jumps over"},
+            cx,
+        );
+        assert(
+            "shift-W",
+            indoc! {"
+            The quick
+            brown |fox-fox
+            jumps over"},
+            indoc! {"
+            The quick
+            brown |
+            jumps over"},
+            cx,
+        );
+        assert(
+            "k",
+            indoc! {"
+            The quick
+            brown |fox"},
+            indoc! {"
+            |"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The q|uick
+            brown fox"},
+            indoc! {"
+            |"},
+            cx,
+        );
+        assert(
+            "shift-$",
+            indoc! {"
+            The q|uick
+            brown fox"},
+            indoc! {"
+            The q|
+            brown fox"},
+            cx,
+        );
+        assert(
+            "0",
+            indoc! {"
+            The q|uick
+            brown fox"},
+            indoc! {"
+            |uick
+            brown fox"},
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_delete(cx: &mut gpui::TestAppContext) {
+        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+            cx.assert_binding(
+                ["d", motion],
+                initial_state,
+                Mode::Normal,
+                state_after,
+                Mode::Normal,
+            );
+        }
+        let cx = &mut VimTestContext::new(cx, true, "").await;
+        assert("h", "Te|st", "T|st", cx);
+        assert("l", "Te|st", "Te|t", cx);
+        assert("w", "|Test", "|", cx);
+        assert("w", "Te|st", "T|e", cx);
+        assert("w", "Te|st Test", "Te|Test", cx);
+        assert("e", "Te|st Test", "Te| Test", cx);
+        assert("b", "Te|st", "|st", cx);
+        assert("b", "Test Te|st", "Test |st", cx);
+        assert(
+            "w",
+            indoc! {"
+            The quick
+            brown |fox
+            jumps over"},
+            // Trailing space after cursor
+            indoc! {"
+            The quick
+            brown| 
+            jumps over"},
+            cx,
+        );
+        assert(
+            "shift-W",
+            indoc! {"
+            The quick
+            brown |fox-fox
+            jumps over"},
+            // Trailing space after cursor
+            indoc! {"
+            The quick
+            brown| 
+            jumps over"},
+            cx,
+        );
+        assert(
+            "shift-$",
+            indoc! {"
+            The q|uick
+            brown fox"},
+            indoc! {"
+            The |q
+            brown fox"},
+            cx,
+        );
+        assert(
+            "0",
+            indoc! {"
+            The q|uick
+            brown fox"},
+            indoc! {"
+            |uick
+            brown fox"},
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_linewise_delete(cx: &mut gpui::TestAppContext) {
+        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+            cx.assert_binding(
+                ["d", motion],
+                initial_state,
+                Mode::Normal,
+                state_after,
+                Mode::Normal,
+            );
+        }
+        let cx = &mut VimTestContext::new(cx, true, "").await;
+        assert(
+            "k",
+            indoc! {"
+            The quick
+            brown |fox
+            jumps over"},
+            indoc! {"
+            jumps |over"},
+            cx,
+        );
+        assert(
+            "k",
+            indoc! {"
+            The quick
+            brown fox
+            jumps |over"},
+            indoc! {"
+            The qu|ick"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The q|uick
+            brown fox
+            jumps over"},
+            indoc! {"
+            jumps| over"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over"},
+            indoc! {"
+            The q|uick"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over"},
+            indoc! {"
+            The q|uick"},
+            cx,
+        );
+        cx.assert_binding(
+            ["d", "g", "g"],
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over
+            the lazy"},
+            Mode::Normal,
+            indoc! {"
+            jumps| over
+            the lazy"},
+            Mode::Normal,
+        );
+        cx.assert_binding(
+            ["d", "g", "g"],
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the l|azy"},
+            Mode::Normal,
+            "|",
+            Mode::Normal,
+        );
+        assert(
+            "shift-G",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over
+            the lazy"},
+            indoc! {"
+            The q|uick"},
+            cx,
+        );
+        cx.assert_binding(
+            ["d", "g", "g"],
+            indoc! {"
+            The q|uick
+            brown fox
+            jumps over
+            the lazy"},
+            Mode::Normal,
+            indoc! {"
+            brown| fox
+            jumps over
+            the lazy"},
+            Mode::Normal,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_linewise_change(cx: &mut gpui::TestAppContext) {
+        fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+            cx.assert_binding(
+                ["c", motion],
+                initial_state,
+                Mode::Normal,
+                state_after,
+                Mode::Insert,
+            );
+        }
+        let cx = &mut VimTestContext::new(cx, true, "").await;
+        assert(
+            "k",
+            indoc! {"
+            The quick
+            brown |fox
+            jumps over"},
+            indoc! {"
+            |
+            jumps over"},
+            cx,
+        );
+        assert(
+            "k",
+            indoc! {"
+            The quick
+            brown fox
+            jumps |over"},
+            indoc! {"
+            The quick
+            |"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The q|uick
+            brown fox
+            jumps over"},
+            indoc! {"
+            |
+            jumps over"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over"},
+            indoc! {"
+            The quick
+            |"},
+            cx,
+        );
+        assert(
+            "j",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over"},
+            indoc! {"
+            The quick
+            |"},
+            cx,
+        );
+        assert(
+            "shift-G",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over
+            the lazy"},
+            indoc! {"
+            The quick
+            |"},
+            cx,
+        );
+        assert(
+            "shift-G",
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over
+            the lazy"},
+            indoc! {"
+            The quick
+            |"},
+            cx,
+        );
+        assert(
+            "shift-G",
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the l|azy"},
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            |"},
+            cx,
+        );
+        cx.assert_binding(
+            ["c", "g", "g"],
+            indoc! {"
+            The quick
+            brown| fox
+            jumps over
+            the lazy"},
+            Mode::Normal,
+            indoc! {"
+            |
+            jumps over
+            the lazy"},
+            Mode::Insert,
+        );
+        cx.assert_binding(
+            ["c", "g", "g"],
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the l|azy"},
+            Mode::Normal,
+            "|",
+            Mode::Insert,
+        );
+        cx.assert_binding(
+            ["c", "g", "g"],
+            indoc! {"
+            The q|uick
+            brown fox
+            jumps over
+            the lazy"},
+            Mode::Normal,
+            indoc! {"
+            |
+            brown fox
+            jumps over
+            the lazy"},
+            Mode::Insert,
+        );
+    }
 }

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

@@ -1,82 +0,0 @@
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
-use workspace::Workspace;
-
-use crate::{mode::Mode, SwitchMode, VimState};
-
-action!(MoveToStart);
-
-pub fn init(cx: &mut MutableAppContext) {
-    let context = Some("Editor && vim_mode == normal && vim_submode == g");
-    cx.add_bindings(vec![
-        Binding::new("g", MoveToStart, context),
-        Binding::new("escape", SwitchMode(Mode::normal()), context),
-    ]);
-
-    cx.add_action(move_to_start);
-}
-
-fn move_to_start(_: &mut Workspace, _: &MoveToStart, cx: &mut ViewContext<Workspace>) {
-    VimState::update_global(cx, |state, cx| {
-        state.update_active_editor(cx, |editor, cx| {
-            editor.move_to_beginning(&editor::MoveToBeginning, cx);
-        });
-        state.switch_mode(&SwitchMode(Mode::normal()), cx);
-    })
-}
-
-#[cfg(test)]
-mod test {
-    use indoc::indoc;
-
-    use crate::{
-        mode::{Mode, NormalState},
-        vim_test_context::VimTestContext,
-    };
-
-    #[gpui::test]
-    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
-        let mut cx = VimTestContext::new(cx, true, "").await;
-
-        // Can abort with escape to get back to normal mode
-        cx.simulate_keystroke("g");
-        assert_eq!(cx.mode(), Mode::Normal(NormalState::GPrefix));
-        cx.simulate_keystroke("escape");
-        assert_eq!(cx.mode(), Mode::normal());
-    }
-
-    #[gpui::test]
-    async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
-        let initial_content = indoc! {"
-            The quick
-            
-            brown fox jumps
-            over the lazy dog"};
-        let mut cx = VimTestContext::new(cx, true, initial_content).await;
-
-        // Jump to the end to
-        cx.simulate_keystroke("shift-G");
-        cx.assert_editor_state(indoc! {"
-            The quick
-            
-            brown fox jumps
-            over the lazy do|g"});
-
-        // Jump to the start
-        cx.simulate_keystrokes(&["g", "g"]);
-        cx.assert_editor_state(indoc! {"
-            |The quick
-            
-            brown fox jumps
-            over the lazy dog"});
-        assert_eq!(cx.mode(), Mode::normal());
-
-        // Repeat action doesn't change
-        cx.simulate_keystrokes(&["g", "g"]);
-        cx.assert_editor_state(indoc! {"
-            |The quick
-            
-            brown fox jumps
-            over the lazy dog"});
-        assert_eq!(cx.mode(), Mode::normal());
-    }
-}

crates/vim/src/state.rs 🔗

@@ -0,0 +1,82 @@
+use editor::CursorShape;
+use gpui::keymap::Context;
+use serde::Deserialize;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
+pub enum Mode {
+    Normal,
+    Insert,
+}
+
+impl Default for Mode {
+    fn default() -> Self {
+        Self::Normal
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+pub enum Namespace {
+    G,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+pub enum Operator {
+    Namespace(Namespace),
+    Change,
+    Delete,
+}
+
+#[derive(Default)]
+pub struct VimState {
+    pub mode: Mode,
+    pub operator_stack: Vec<Operator>,
+}
+
+impl VimState {
+    pub fn cursor_shape(&self) -> CursorShape {
+        match self.mode {
+            Mode::Normal => CursorShape::Block,
+            Mode::Insert => CursorShape::Bar,
+        }
+    }
+
+    pub fn vim_controlled(&self) -> bool {
+        !matches!(self.mode, Mode::Insert)
+    }
+
+    pub fn keymap_context_layer(&self) -> Context {
+        let mut context = Context::default();
+        context.map.insert(
+            "vim_mode".to_string(),
+            match self.mode {
+                Mode::Normal => "normal",
+                Mode::Insert => "insert",
+            }
+            .to_string(),
+        );
+
+        if self.vim_controlled() {
+            context.set.insert("VimControl".to_string());
+        }
+
+        if let Some(operator) = &self.operator_stack.last() {
+            operator.set_context(&mut context);
+        }
+        context
+    }
+}
+
+impl Operator {
+    pub fn set_context(&self, context: &mut Context) {
+        let operator_context = match self {
+            Operator::Namespace(Namespace::G) => "g",
+            Operator::Change => "c",
+            Operator::Delete => "d",
+        }
+        .to_owned();
+
+        context
+            .map
+            .insert("vim_operator".to_string(), operator_context.to_string());
+    }
+}

crates/vim/src/vim.rs 🔗

@@ -1,45 +1,63 @@
 mod editor_events;
 mod insert;
-mod mode;
+mod motion;
 mod normal;
+mod state;
 #[cfg(test)]
 mod vim_test_context;
 
 use collections::HashMap;
 use editor::{CursorShape, Editor};
-use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
+use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
+use serde::Deserialize;
 
-use mode::Mode;
-use workspace::{self, Settings, Workspace};
+use settings::Settings;
+use state::{Mode, Operator, VimState};
+use workspace::{self, Workspace};
 
-action!(SwitchMode, Mode);
+#[derive(Clone, Deserialize)]
+pub struct SwitchMode(pub Mode);
+
+#[derive(Clone, Deserialize)]
+pub struct PushOperator(pub Operator);
+
+impl_actions!(vim, [SwitchMode, PushOperator]);
 
 pub fn init(cx: &mut MutableAppContext) {
     editor_events::init(cx);
     insert::init(cx);
-    normal::init(cx);
+    motion::init(cx);
 
-    cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| {
-        VimState::update_global(cx, |state, cx| state.switch_mode(action, cx))
+    cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
+        Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
     });
+    cx.add_action(
+        |_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
+            Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
+        },
+    );
 
     cx.observe_global::<Settings, _>(|settings, cx| {
-        VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
+        Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
     })
     .detach();
 }
 
 #[derive(Default)]
-pub struct VimState {
+pub struct Vim {
     editors: HashMap<usize, WeakViewHandle<Editor>>,
     active_editor: Option<WeakViewHandle<Editor>>,
 
     enabled: bool,
-    mode: Mode,
+    state: VimState,
 }
 
-impl VimState {
-    fn update_global<F, S>(cx: &mut MutableAppContext, update: F) -> S
+impl Vim {
+    fn read(cx: &mut MutableAppContext) -> &Self {
+        cx.default_global()
+    }
+
+    fn update<F, S>(cx: &mut MutableAppContext, update: F) -> S
     where
         F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
     {
@@ -57,33 +75,54 @@ impl VimState {
             .map(|ae| ae.update(cx, update))
     }
 
-    fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) {
-        self.mode = *mode;
+    fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
+        self.state.mode = mode;
+        self.state.operator_stack.clear();
         self.sync_editor_options(cx);
     }
 
+    fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
+        self.state.operator_stack.push(operator);
+        self.sync_editor_options(cx);
+    }
+
+    fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
+        let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
+        self.sync_editor_options(cx);
+        popped_operator
+    }
+
+    fn clear_operator(&mut self, cx: &mut MutableAppContext) {
+        self.state.operator_stack.clear();
+        self.sync_editor_options(cx);
+    }
+
+    fn active_operator(&mut self) -> Option<Operator> {
+        self.state.operator_stack.last().copied()
+    }
+
     fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
         if self.enabled != enabled {
             self.enabled = enabled;
-            self.mode = Default::default();
+            self.state = Default::default();
             if enabled {
-                self.mode = Mode::normal();
+                self.state.mode = Mode::Normal;
             }
             self.sync_editor_options(cx);
         }
     }
 
     fn sync_editor_options(&self, cx: &mut MutableAppContext) {
-        let mode = self.mode;
-        let cursor_shape = mode.cursor_shape();
+        let state = &self.state;
+        let cursor_shape = state.cursor_shape();
         for editor in self.editors.values() {
             if let Some(editor) = editor.upgrade(cx) {
                 editor.update(cx, |editor, cx| {
                     if self.enabled {
                         editor.set_cursor_shape(cursor_shape, cx);
                         editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
-                        editor.set_input_enabled(mode == Mode::Insert);
-                        let context_layer = mode.keymap_context_layer();
+                        editor.set_input_enabled(!state.vim_controlled());
+                        let context_layer = state.keymap_context_layer();
                         editor.set_keymap_context_layer::<Self>(context_layer);
                     } else {
                         editor.set_cursor_shape(CursorShape::Bar, cx);
@@ -99,12 +138,12 @@ impl VimState {
 
 #[cfg(test)]
 mod test {
-    use crate::{mode::Mode, vim_test_context::VimTestContext};
+    use crate::{state::Mode, vim_test_context::VimTestContext};
 
     #[gpui::test]
     async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, false, "").await;
-        cx.simulate_keystrokes(&["h", "j", "k", "l"]);
+        cx.simulate_keystrokes(["h", "j", "k", "l"]);
         cx.assert_editor_state("hjkl|");
     }
 
@@ -117,22 +156,22 @@ mod test {
 
         // Editor acts as though vim is disabled
         cx.disable_vim();
-        cx.simulate_keystrokes(&["h", "j", "k", "l"]);
+        cx.simulate_keystrokes(["h", "j", "k", "l"]);
         cx.assert_editor_state("hjkl|");
 
         // Enabling dynamically sets vim mode again and restores normal mode
         cx.enable_vim();
-        assert_eq!(cx.mode(), Mode::normal());
-        cx.simulate_keystrokes(&["h", "h", "h", "l"]);
+        assert_eq!(cx.mode(), Mode::Normal);
+        cx.simulate_keystrokes(["h", "h", "h", "l"]);
         assert_eq!(cx.editor_text(), "hjkl".to_owned());
         cx.assert_editor_state("hj|kl");
-        cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
+        cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
         cx.assert_editor_state("hjTest|kl");
 
         // Disabling and enabling resets to normal mode
         assert_eq!(cx.mode(), Mode::Insert);
         cx.disable_vim();
         cx.enable_vim();
-        assert_eq!(cx.mode(), Mode::normal());
+        assert_eq!(cx.mode(), Mode::Normal);
     }
 }

crates/vim/src/vim_test_context.rs 🔗

@@ -6,7 +6,7 @@ use language::{Point, Selection};
 use util::test::marked_text;
 use workspace::{WorkspaceHandle, WorkspaceParams};
 
-use crate::*;
+use crate::{state::Operator, *};
 
 pub struct VimTestContext<'a> {
     cx: &'a mut gpui::TestAppContext,
@@ -23,7 +23,10 @@ impl<'a> VimTestContext<'a> {
         cx.update(|cx| {
             editor::init(cx);
             crate::init(cx);
+
+            settings::KeymapFile::load("keymaps/vim.json", cx).unwrap();
         });
+
         let params = cx.update(WorkspaceParams::test);
 
         cx.update(|cx| {
@@ -97,7 +100,12 @@ impl<'a> VimTestContext<'a> {
     }
 
     pub fn mode(&mut self) -> Mode {
-        self.cx.update(|cx| cx.global::<VimState>().mode)
+        self.cx.read(|cx| cx.global::<Vim>().state.mode)
+    }
+
+    pub fn active_operator(&mut self) -> Option<Operator> {
+        self.cx
+            .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
     }
 
     pub fn editor_text(&mut self) -> String {
@@ -116,12 +124,23 @@ impl<'a> VimTestContext<'a> {
             .dispatch_keystroke(self.window_id, keystroke, input, false);
     }
 
-    pub fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
+    pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
         for keystroke_text in keystroke_texts.into_iter() {
             self.simulate_keystroke(keystroke_text);
         }
     }
 
+    pub fn set_state(&mut self, text: &str, mode: Mode) {
+        self.cx
+            .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
+        self.editor.update(self.cx, |editor, cx| {
+            let (unmarked_text, markers) = marked_text(&text);
+            editor.set_text(unmarked_text, cx);
+            let cursor_offset = markers[0];
+            editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
+        })
+    }
+
     pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
         let actual_head = self.newest_selection().head();
         let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
@@ -168,6 +187,21 @@ impl<'a> VimTestContext<'a> {
             actual_position_text, expected_position_text
         )
     }
+
+    pub fn assert_binding<const COUNT: usize>(
+        &mut self,
+        keystrokes: [&str; COUNT],
+        initial_state: &str,
+        initial_mode: Mode,
+        state_after: &str,
+        mode_after: Mode,
+    ) {
+        self.set_state(initial_state, initial_mode);
+        self.simulate_keystrokes(keystrokes);
+        self.assert_editor_state(state_after);
+        assert_eq!(self.mode(), mode_after);
+        assert_eq!(self.active_operator(), None);
+    }
 }
 
 impl<'a> Deref for VimTestContext<'a> {

crates/workspace/Cargo.toml 🔗

@@ -8,7 +8,7 @@ path = "src/workspace.rs"
 doctest = false
 
 [features]
-test-support = ["client/test-support", "project/test-support"]
+test-support = ["client/test-support", "project/test-support", "settings/test-support"]
 
 [dependencies]
 client = { path = "../client" }
@@ -17,14 +17,14 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
 futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
-schemars = "0.8"
 serde = { version = "1", features = ["derive", "rc"] }
 serde_json = { version = "1", features = ["preserve_order"] }
 smallvec = { version = "1.6", features = ["union"] }
@@ -33,3 +33,4 @@ smallvec = { version = "1.6", features = ["union"] }
 client = { path = "../client", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }

crates/workspace/src/lsp_status.rs 🔗

@@ -1,18 +1,19 @@
-use crate::{ItemHandle, Settings, StatusItemView};
+use crate::{ItemHandle, StatusItemView};
 use futures::StreamExt;
-use gpui::AppContext;
+use gpui::{actions, AppContext};
 use gpui::{
-    action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext,
-    RenderContext, View, ViewContext,
+    elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext,
+    View, ViewContext,
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use project::{LanguageServerProgress, Project};
+use settings::Settings;
 use smallvec::SmallVec;
 use std::cmp::Reverse;
 use std::fmt::Write;
 use std::sync::Arc;
 
-action!(DismissErrorMessage);
+actions!(lsp_status, [DismissErrorMessage]);
 
 pub struct LspStatus {
     checking_for_update: Vec<String>,

crates/workspace/src/menu.rs 🔗

@@ -1,19 +1,16 @@
-use gpui::{action, keymap::Binding, MutableAppContext};
+#[derive(Clone)]
+pub struct SelectIndex(pub usize);
 
-action!(Confirm);
-action!(SelectPrev);
-action!(SelectNext);
-action!(SelectFirst);
-action!(SelectLast);
+gpui::actions!(
+    menu,
+    [
+        Cancel,
+        Confirm,
+        SelectPrev,
+        SelectNext,
+        SelectFirst,
+        SelectLast
+    ]
+);
 
-pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([
-        Binding::new("up", SelectPrev, Some("menu")),
-        Binding::new("ctrl-p", SelectPrev, Some("menu")),
-        Binding::new("down", SelectNext, Some("menu")),
-        Binding::new("ctrl-n", SelectNext, Some("menu")),
-        Binding::new("cmd-up", SelectFirst, Some("menu")),
-        Binding::new("cmd-down", SelectLast, Some("menu")),
-        Binding::new("enter", Confirm, Some("menu")),
-    ]);
-}
+gpui::impl_internal_actions!(menu, [SelectIndex]);

crates/workspace/src/pane.rs 🔗

@@ -1,29 +1,59 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
+use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
+use anyhow::Result;
 use collections::{HashMap, VecDeque};
 use futures::StreamExt;
 use gpui::{
-    action,
+    actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    keymap::Binding,
+    impl_actions, impl_internal_actions,
     platform::{CursorStyle, NavigationDirection},
-    AppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, RenderContext, Task,
-    View, ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
-use project::{Project, ProjectEntryId, ProjectPath};
+use project::{ProjectEntryId, ProjectPath};
+use serde::Deserialize;
+use settings::Settings;
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use util::ResultExt;
 
-action!(Split, SplitDirection);
-action!(ActivateItem, usize);
-action!(ActivatePrevItem);
-action!(ActivateNextItem);
-action!(CloseActiveItem);
-action!(CloseInactiveItems);
-action!(CloseItem, usize);
-action!(GoBack, Option<WeakViewHandle<Pane>>);
-action!(GoForward, Option<WeakViewHandle<Pane>>);
+actions!(
+    pane,
+    [
+        ActivatePrevItem,
+        ActivateNextItem,
+        CloseActiveItem,
+        CloseInactiveItems,
+    ]
+);
+
+#[derive(Clone, Deserialize)]
+pub struct Split(pub SplitDirection);
+
+#[derive(Clone)]
+pub struct CloseItem {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct ActivateItem(pub usize);
+
+#[derive(Clone, Deserialize)]
+pub struct GoBack {
+    #[serde(skip_deserializing)]
+    pub pane: Option<WeakViewHandle<Pane>>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct GoForward {
+    #[serde(skip_deserializing)]
+    pub pane: Option<WeakViewHandle<Pane>>,
+}
+
+impl_actions!(pane, [Split, GoBack, GoForward]);
+impl_internal_actions!(pane, [CloseItem, ActivateItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -37,14 +67,11 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
         pane.activate_next_item(cx);
     });
-    cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
-        pane.close_active_item(cx).detach();
-    });
-    cx.add_action(|pane: &mut Pane, _: &CloseInactiveItems, cx| {
-        pane.close_inactive_items(cx).detach();
-    });
-    cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
-        pane.close_item(action.0, cx).detach();
+    cx.add_async_action(Pane::close_active_item);
+    cx.add_async_action(Pane::close_inactive_items);
+    cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
+        let pane = action.pane.upgrade(cx)?;
+        Some(Pane::close_item(workspace, pane, action.item_id, cx))
     });
     cx.add_action(|pane: &mut Pane, action: &Split, cx| {
         pane.split(action.0, cx);
@@ -53,7 +80,7 @@ pub fn init(cx: &mut MutableAppContext) {
         Pane::go_back(
             workspace,
             action
-                .0
+                .pane
                 .as_ref()
                 .and_then(|weak_handle| weak_handle.upgrade(cx)),
             cx,
@@ -64,26 +91,13 @@ pub fn init(cx: &mut MutableAppContext) {
         Pane::go_forward(
             workspace,
             action
-                .0
+                .pane
                 .as_ref()
                 .and_then(|weak_handle| weak_handle.upgrade(cx)),
             cx,
         )
         .detach();
     });
-
-    cx.add_bindings(vec![
-        Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
-        Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
-        Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
-        Binding::new("alt-cmd-w", CloseInactiveItems, Some("Pane")),
-        Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
-        Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
-        Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
-        Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
-        Binding::new("ctrl--", GoBack(None), Some("Pane")),
-        Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")),
-    ]);
 }
 
 pub enum Event {
@@ -96,9 +110,9 @@ pub enum Event {
 pub struct Pane {
     items: Vec<Box<dyn ItemHandle>>,
     active_item_index: usize,
+    autoscroll: bool,
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
-    project: ModelHandle<Project>,
 }
 
 pub struct ItemNavHistory {
@@ -134,13 +148,13 @@ pub struct NavigationEntry {
 }
 
 impl Pane {
-    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
         Self {
             items: Vec::new(),
             active_item_index: 0,
+            autoscroll: false,
             nav_history: Default::default(),
             toolbar: cx.add_view(|_| Toolbar::new()),
-            project,
         }
     }
 
@@ -197,21 +211,14 @@ impl Pane {
                     .upgrade(cx)
                     .and_then(|v| pane.index_for_item(v.as_ref()))
                 {
-                    if let Some(item) = pane.active_item() {
-                        pane.nav_history.borrow_mut().set_mode(mode);
-                        item.deactivated(cx);
-                        pane.nav_history
-                            .borrow_mut()
-                            .set_mode(NavigationMode::Normal);
-                    }
-
-                    let prev_active_index = mem::replace(&mut pane.active_item_index, index);
-                    pane.focus_active_item(cx);
-                    pane.update_toolbar(cx);
-                    cx.emit(Event::ActivateItem { local: true });
-                    cx.notify();
+                    let prev_active_item_index = pane.active_item_index;
+                    pane.nav_history.borrow_mut().set_mode(mode);
+                    pane.activate_item(index, true, cx);
+                    pane.nav_history
+                        .borrow_mut()
+                        .set_mode(NavigationMode::Normal);
 
-                    let mut navigated = prev_active_index != pane.active_item_index;
+                    let mut navigated = prev_active_item_index != pane.active_item_index;
                     if let Some(data) = entry.data {
                         navigated |= pane.active_item()?.navigate(data, cx);
                     }
@@ -372,10 +379,12 @@ impl Pane {
     }
 
     pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
+        use NavigationMode::{GoingBack, GoingForward};
         if index < self.items.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
-            if prev_active_item_ix != self.active_item_index
-                && prev_active_item_ix < self.items.len()
+            if matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
+                || (prev_active_item_ix != self.active_item_index
+                    && prev_active_item_ix < self.items.len())
             {
                 self.items[prev_active_item_ix].deactivated(cx);
                 cx.emit(Event::ActivateItem { local });
@@ -385,6 +394,7 @@ impl Pane {
                 self.focus_active_item(cx);
                 self.activate(cx);
             }
+            self.autoscroll = true;
             cx.notify();
         }
     }
@@ -409,162 +419,183 @@ impl Pane {
         self.activate_item(index, true, cx);
     }
 
-    pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
-        if self.items.is_empty() {
-            Task::ready(())
+    fn close_active_item(
+        workspace: &mut Workspace,
+        _: &CloseActiveItem,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        let pane_handle = workspace.active_pane().clone();
+        let pane = pane_handle.read(cx);
+        if pane.items.is_empty() {
+            None
         } else {
-            self.close_item(self.items[self.active_item_index].id(), cx)
+            let item_id_to_close = pane.items[pane.active_item_index].id();
+            Some(Self::close_items(
+                workspace,
+                pane_handle,
+                cx,
+                move |item_id| item_id == item_id_to_close,
+            ))
         }
     }
 
-    pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
-        if self.items.is_empty() {
-            Task::ready(())
+    pub fn close_inactive_items(
+        workspace: &mut Workspace,
+        _: &CloseInactiveItems,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Task<Result<()>>> {
+        let pane_handle = workspace.active_pane().clone();
+        let pane = pane_handle.read(cx);
+        if pane.items.is_empty() {
+            None
         } else {
-            let active_item_id = self.items[self.active_item_index].id();
-            self.close_items(cx, move |id| id != active_item_id)
+            let active_item_id = pane.items[pane.active_item_index].id();
+            Some(Self::close_items(workspace, pane_handle, cx, move |id| {
+                id != active_item_id
+            }))
         }
     }
 
-    pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) -> Task<()> {
-        self.close_items(cx, move |view_id| view_id == view_id_to_close)
+    pub fn close_item(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item_id_to_close: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<()>> {
+        Self::close_items(workspace, pane, cx, move |view_id| {
+            view_id == item_id_to_close
+        })
     }
 
     pub fn close_items(
-        &mut self,
-        cx: &mut ViewContext<Self>,
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
         should_close: impl 'static + Fn(usize) -> bool,
-    ) -> Task<()> {
+    ) -> Task<Result<()>> {
         const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
         const DIRTY_MESSAGE: &'static str =
             "This file contains unsaved edits. Do you want to save it?";
 
-        let project = self.project.clone();
-        cx.spawn(|this, mut cx| async move {
-            while let Some(item_to_close_ix) = this.read_with(&cx, |this, _| {
-                this.items.iter().position(|item| should_close(item.id()))
+        let project = workspace.project().clone();
+        cx.spawn(|workspace, mut cx| async move {
+            while let Some(item_to_close_ix) = pane.read_with(&cx, |pane, _| {
+                pane.items.iter().position(|item| should_close(item.id()))
             }) {
                 let item =
-                    this.read_with(&cx, |this, _| this.items[item_to_close_ix].boxed_clone());
-                if cx.read(|cx| item.is_dirty(cx)) {
-                    if cx.read(|cx| item.can_save(cx)) {
-                        let mut answer = this.update(&mut cx, |this, cx| {
-                            this.activate_item(item_to_close_ix, true, cx);
+                    pane.read_with(&cx, |pane, _| pane.items[item_to_close_ix].boxed_clone());
+
+                let is_last_item_for_entry = workspace.read_with(&cx, |workspace, cx| {
+                    let project_entry_id = item.project_entry_id(cx);
+                    project_entry_id.is_none()
+                        || workspace
+                            .items(cx)
+                            .filter(|item| item.project_entry_id(cx) == project_entry_id)
+                            .count()
+                            == 1
+                });
+
+                if is_last_item_for_entry {
+                    if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
+                        let mut answer = pane.update(&mut cx, |pane, cx| {
+                            pane.activate_item(item_to_close_ix, true, cx);
                             cx.prompt(
                                 PromptLevel::Warning,
-                                DIRTY_MESSAGE,
-                                &["Save", "Don't Save", "Cancel"],
+                                CONFLICT_MESSAGE,
+                                &["Overwrite", "Discard", "Cancel"],
                             )
                         });
 
                         match answer.next().await {
                             Some(0) => {
-                                if cx
-                                    .update(|cx| item.save(project.clone(), cx))
-                                    .await
-                                    .log_err()
-                                    .is_none()
-                                {
-                                    break;
-                                }
+                                cx.update(|cx| item.save(project.clone(), cx)).await?;
+                            }
+                            Some(1) => {
+                                cx.update(|cx| item.reload(project.clone(), cx)).await?;
                             }
-                            Some(1) => {}
                             _ => break,
                         }
-                    } else if cx.read(|cx| item.can_save_as(cx)) {
-                        let mut answer = this.update(&mut cx, |this, cx| {
-                            this.activate_item(item_to_close_ix, true, cx);
-                            cx.prompt(
-                                PromptLevel::Warning,
-                                DIRTY_MESSAGE,
-                                &["Save", "Don't Save", "Cancel"],
-                            )
-                        });
+                    } else if cx.read(|cx| item.is_dirty(cx)) {
+                        if cx.read(|cx| item.can_save(cx)) {
+                            let mut answer = pane.update(&mut cx, |pane, cx| {
+                                pane.activate_item(item_to_close_ix, true, cx);
+                                cx.prompt(
+                                    PromptLevel::Warning,
+                                    DIRTY_MESSAGE,
+                                    &["Save", "Don't Save", "Cancel"],
+                                )
+                            });
 
-                        match answer.next().await {
-                            Some(0) => {
-                                let start_abs_path = project
-                                    .read_with(&cx, |project, cx| {
-                                        let worktree = project.visible_worktrees(cx).next()?;
-                                        Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
-                                    })
-                                    .unwrap_or(Path::new("").into());
-
-                                let mut abs_path =
-                                    cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
-                                if let Some(abs_path) = abs_path.next().await.flatten() {
-                                    if cx
-                                        .update(|cx| item.save_as(project.clone(), abs_path, cx))
-                                        .await
-                                        .log_err()
-                                        .is_none()
-                                    {
+                            match answer.next().await {
+                                Some(0) => {
+                                    cx.update(|cx| item.save(project.clone(), cx)).await?;
+                                }
+                                Some(1) => {}
+                                _ => break,
+                            }
+                        } else if cx.read(|cx| item.can_save_as(cx)) {
+                            let mut answer = pane.update(&mut cx, |pane, cx| {
+                                pane.activate_item(item_to_close_ix, true, cx);
+                                cx.prompt(
+                                    PromptLevel::Warning,
+                                    DIRTY_MESSAGE,
+                                    &["Save", "Don't Save", "Cancel"],
+                                )
+                            });
+
+                            match answer.next().await {
+                                Some(0) => {
+                                    let start_abs_path = project
+                                        .read_with(&cx, |project, cx| {
+                                            let worktree = project.visible_worktrees(cx).next()?;
+                                            Some(
+                                                worktree
+                                                    .read(cx)
+                                                    .as_local()?
+                                                    .abs_path()
+                                                    .to_path_buf(),
+                                            )
+                                        })
+                                        .unwrap_or(Path::new("").into());
+
+                                    let mut abs_path =
+                                        cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
+                                    if let Some(abs_path) = abs_path.next().await.flatten() {
+                                        cx.update(|cx| item.save_as(project.clone(), abs_path, cx))
+                                            .await?;
+                                    } else {
                                         break;
                                     }
-                                } else {
-                                    break;
                                 }
-                            }
-                            Some(1) => {}
-                            _ => break,
-                        }
-                    }
-                } else if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
-                    let mut answer = this.update(&mut cx, |this, cx| {
-                        this.activate_item(item_to_close_ix, true, cx);
-                        cx.prompt(
-                            PromptLevel::Warning,
-                            CONFLICT_MESSAGE,
-                            &["Overwrite", "Discard", "Cancel"],
-                        )
-                    });
-
-                    match answer.next().await {
-                        Some(0) => {
-                            if cx
-                                .update(|cx| item.save(project.clone(), cx))
-                                .await
-                                .log_err()
-                                .is_none()
-                            {
-                                break;
+                                Some(1) => {}
+                                _ => break,
                             }
                         }
-                        Some(1) => {
-                            if cx
-                                .update(|cx| item.reload(project.clone(), cx))
-                                .await
-                                .log_err()
-                                .is_none()
-                            {
-                                break;
-                            }
-                        }
-                        _ => break,
                     }
                 }
 
-                this.update(&mut cx, |this, cx| {
-                    if let Some(item_ix) = this.items.iter().position(|i| i.id() == item.id()) {
-                        if item_ix == this.active_item_index {
-                            if item_ix + 1 < this.items.len() {
-                                this.activate_next_item(cx);
+                pane.update(&mut cx, |pane, cx| {
+                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
+                        if item_ix == pane.active_item_index {
+                            if item_ix + 1 < pane.items.len() {
+                                pane.activate_next_item(cx);
                             } else if item_ix > 0 {
-                                this.activate_prev_item(cx);
+                                pane.activate_prev_item(cx);
                             }
                         }
 
-                        let item = this.items.remove(item_ix);
-                        if this.items.is_empty() {
+                        let item = pane.items.remove(item_ix);
+                        if pane.items.is_empty() {
                             item.deactivated(cx);
+                            pane.update_toolbar(cx);
                             cx.emit(Event::Remove);
                         }
 
-                        if item_ix < this.active_item_index {
-                            this.active_item_index -= 1;
+                        if item_ix < pane.active_item_index {
+                            pane.active_item_index -= 1;
                         }
 
-                        let mut nav_history = this.nav_history.borrow_mut();
+                        let mut nav_history = pane.nav_history.borrow_mut();
                         if let Some(path) = item.project_path(cx) {
                             nav_history.paths_by_item.insert(item.id(), path);
                         } else {
@@ -574,7 +605,8 @@ impl Pane {
                 });
             }
 
-            this.update(&mut cx, |_, cx| cx.notify());
+            pane.update(&mut cx, |_, cx| cx.notify());
+            Ok(())
         })
     }
 
@@ -602,12 +634,18 @@ impl Pane {
         });
     }
 
-    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = cx.global::<Settings>().theme.clone();
 
         enum Tabs {}
+        let pane = cx.handle();
         let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
-            let mut row = Flex::row();
+            let autoscroll = if mem::take(&mut self.autoscroll) {
+                Some(self.active_item_index)
+            } else {
+                None
+            };
+            let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
             for (ix, item) in self.items.iter().enumerate() {
                 let is_active = ix == self.active_item_index;
 
@@ -697,8 +735,14 @@ impl Pane {
                                             )
                                             .with_padding(Padding::uniform(4.))
                                             .with_cursor_style(CursorStyle::PointingHand)
-                                            .on_click(move |cx| {
-                                                cx.dispatch_action(CloseItem(item_id))
+                                            .on_click({
+                                                let pane = pane.clone();
+                                                move |cx| {
+                                                    cx.dispatch_action(CloseItem {
+                                                        item_id,
+                                                        pane: pane.clone(),
+                                                    })
+                                                }
                                             })
                                             .named("close-tab-icon")
                                         } else {
@@ -763,8 +807,8 @@ impl View for Pane {
         .on_navigate_mouse_down(move |direction, cx| {
             let this = this.clone();
             match direction {
-                NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))),
-                NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))),
+                NavigationDirection::Back => cx.dispatch_action(GoBack { pane: Some(this) }),
+                NavigationDirection::Forward => cx.dispatch_action(GoForward { pane: Some(this) }),
             }
 
             true
@@ -860,10 +904,11 @@ impl NavHistory {
 
 #[cfg(test)]
 mod tests {
-    use crate::WorkspaceParams;
-
     use super::*;
-    use gpui::TestAppContext;
+    use crate::WorkspaceParams;
+    use gpui::{ModelHandle, TestAppContext, ViewContext};
+    use project::Project;
+    use std::sync::atomic::AtomicUsize;
 
     #[gpui::test]
     async fn test_close_items(cx: &mut TestAppContext) {
@@ -873,7 +918,7 @@ mod tests {
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
         let item1 = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
-            item.has_conflict = true;
+            item.is_dirty = true;
             item
         });
         let item2 = cx.add_view(window_id, |_| {
@@ -884,15 +929,11 @@ mod tests {
         });
         let item3 = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
+            item.is_dirty = true;
             item.has_conflict = true;
             item
         });
         let item4 = cx.add_view(window_id, |_| {
-            let mut item = TestItem::new();
-            item.is_dirty = true;
-            item
-        });
-        let item5 = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
             item.is_dirty = true;
             item.can_save = false;
@@ -903,26 +944,26 @@ mod tests {
             workspace.add_item(Box::new(item2.clone()), cx);
             workspace.add_item(Box::new(item3.clone()), cx);
             workspace.add_item(Box::new(item4.clone()), cx);
-            workspace.add_item(Box::new(item5.clone()), cx);
             workspace.active_pane().clone()
         });
 
-        let close_items = pane.update(cx, |pane, cx| {
-            pane.activate_item(1, true, cx);
-            assert_eq!(pane.active_item().unwrap().id(), item2.id());
+        let close_items = workspace.update(cx, |workspace, cx| {
+            pane.update(cx, |pane, cx| {
+                pane.activate_item(1, true, cx);
+                assert_eq!(pane.active_item().unwrap().id(), item2.id());
+            });
 
             let item1_id = item1.id();
             let item3_id = item3.id();
             let item4_id = item4.id();
-            let item5_id = item5.id();
-            pane.close_items(cx, move |id| {
-                [item1_id, item3_id, item4_id, item5_id].contains(&id)
+            Pane::close_items(workspace, pane.clone(), cx, move |id| {
+                [item1_id, item3_id, item4_id].contains(&id)
             })
         });
 
         cx.foreground().run_until_parked();
         pane.read_with(cx, |pane, _| {
-            assert_eq!(pane.items.len(), 5);
+            assert_eq!(pane.items.len(), 4);
             assert_eq!(pane.active_item().unwrap().id(), item1.id());
         });
 
@@ -932,7 +973,7 @@ mod tests {
             assert_eq!(item1.read(cx).save_count, 1);
             assert_eq!(item1.read(cx).save_as_count, 0);
             assert_eq!(item1.read(cx).reload_count, 0);
-            assert_eq!(pane.items.len(), 4);
+            assert_eq!(pane.items.len(), 3);
             assert_eq!(pane.active_item().unwrap().id(), item3.id());
         });
 
@@ -942,33 +983,67 @@ mod tests {
             assert_eq!(item3.read(cx).save_count, 0);
             assert_eq!(item3.read(cx).save_as_count, 0);
             assert_eq!(item3.read(cx).reload_count, 1);
-            assert_eq!(pane.items.len(), 3);
+            assert_eq!(pane.items.len(), 2);
             assert_eq!(pane.active_item().unwrap().id(), item4.id());
         });
 
         cx.simulate_prompt_answer(window_id, 0);
         cx.foreground().run_until_parked();
+        cx.simulate_new_path_selection(|_| Some(Default::default()));
+        close_items.await.unwrap();
         pane.read_with(cx, |pane, cx| {
-            assert_eq!(item4.read(cx).save_count, 1);
-            assert_eq!(item4.read(cx).save_as_count, 0);
+            assert_eq!(item4.read(cx).save_count, 0);
+            assert_eq!(item4.read(cx).save_as_count, 1);
             assert_eq!(item4.read(cx).reload_count, 0);
-            assert_eq!(pane.items.len(), 2);
-            assert_eq!(pane.active_item().unwrap().id(), item5.id());
+            assert_eq!(pane.items.len(), 1);
+            assert_eq!(pane.active_item().unwrap().id(), item2.id());
         });
+    }
 
-        cx.simulate_prompt_answer(window_id, 0);
+    #[gpui::test]
+    async fn test_prompting_only_on_last_item_for_entry(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+
+        let params = cx.update(WorkspaceParams::test);
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let item = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.is_dirty = true;
+            item.project_entry_id = Some(ProjectEntryId::new(&AtomicUsize::new(1)));
+            item
+        });
+
+        let (left_pane, right_pane) = workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item.clone()), cx);
+            let left_pane = workspace.active_pane().clone();
+            let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
+            (left_pane, right_pane)
+        });
+
+        workspace
+            .update(cx, |workspace, cx| {
+                let item = right_pane.read(cx).active_item().unwrap();
+                Pane::close_item(workspace, right_pane.clone(), item.id(), cx)
+            })
+            .await
+            .unwrap();
+        workspace.read_with(cx, |workspace, _| {
+            assert_eq!(workspace.panes(), [left_pane.clone()]);
+        });
+
+        let close_item = workspace.update(cx, |workspace, cx| {
+            let item = left_pane.read(cx).active_item().unwrap();
+            Pane::close_item(workspace, left_pane.clone(), item.id(), cx)
+        });
         cx.foreground().run_until_parked();
-        cx.simulate_new_path_selection(|_| Some(Default::default()));
-        close_items.await;
-        pane.read_with(cx, |pane, cx| {
-            assert_eq!(item5.read(cx).save_count, 0);
-            assert_eq!(item5.read(cx).save_as_count, 1);
-            assert_eq!(item5.read(cx).reload_count, 0);
-            assert_eq!(pane.items.len(), 1);
-            assert_eq!(pane.active_item().unwrap().id(), item2.id());
+        cx.simulate_prompt_answer(window_id, 0);
+        close_item.await.unwrap();
+        left_pane.read_with(cx, |pane, _| {
+            assert_eq!(pane.items.len(), 0);
         });
     }
 
+    #[derive(Clone)]
     struct TestItem {
         save_count: usize,
         save_as_count: usize,
@@ -976,6 +1051,7 @@ mod tests {
         is_dirty: bool,
         has_conflict: bool,
         can_save: bool,
+        project_entry_id: Option<ProjectEntryId>,
     }
 
     impl TestItem {
@@ -987,6 +1063,7 @@ mod tests {
                 is_dirty: false,
                 has_conflict: false,
                 can_save: true,
+                project_entry_id: None,
             }
         }
     }
@@ -1015,11 +1092,18 @@ mod tests {
         }
 
         fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
-            None
+            self.project_entry_id
         }
 
         fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
 
+        fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
+        where
+            Self: Sized,
+        {
+            Some(self.clone())
+        }
+
         fn is_dirty(&self, _: &AppContext) -> bool {
             self.is_dirty
         }

crates/workspace/src/pane_group.rs 🔗

@@ -4,6 +4,7 @@ use client::PeerId;
 use collections::HashMap;
 use gpui::{elements::*, Axis, Border, ViewHandle};
 use project::Collaborator;
+use serde::Deserialize;
 use theme::Theme;
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -254,7 +255,7 @@ impl PaneAxis {
     }
 }
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Deserialize)]
 pub enum SplitDirection {
     Up,
     Down,

crates/workspace/src/settings.rs 🔗

@@ -1,325 +0,0 @@
-use anyhow::Result;
-use futures::{stream, SinkExt, StreamExt as _};
-use gpui::{
-    executor,
-    font_cache::{FamilyId, FontCache},
-};
-use language::Language;
-use postage::{prelude::Stream, watch};
-use project::Fs;
-use schemars::{schema_for, JsonSchema};
-use serde::Deserialize;
-use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
-use theme::{Theme, ThemeRegistry};
-use util::ResultExt;
-
-#[derive(Clone)]
-pub struct Settings {
-    pub buffer_font_family: FamilyId,
-    pub buffer_font_size: f32,
-    pub vim_mode: bool,
-    pub tab_size: usize,
-    pub soft_wrap: SoftWrap,
-    pub preferred_line_length: u32,
-    pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
-    pub theme: Arc<Theme>,
-}
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-pub struct LanguageOverride {
-    pub tab_size: Option<usize>,
-    pub soft_wrap: Option<SoftWrap>,
-    pub preferred_line_length: Option<u32>,
-}
-
-#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
-    None,
-    EditorWidth,
-    PreferredLineLength,
-}
-
-#[derive(Clone)]
-pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-struct SettingsFileContent {
-    #[serde(default)]
-    buffer_font_family: Option<String>,
-    #[serde(default)]
-    buffer_font_size: Option<f32>,
-    #[serde(default)]
-    vim_mode: Option<bool>,
-    #[serde(flatten)]
-    editor: LanguageOverride,
-    #[serde(default)]
-    language_overrides: HashMap<Arc<str>, LanguageOverride>,
-    #[serde(default)]
-    theme: Option<String>,
-}
-
-impl SettingsFile {
-    pub async fn new(
-        fs: Arc<dyn Fs>,
-        executor: &executor::Background,
-        path: impl Into<Arc<Path>>,
-    ) -> Self {
-        let path = path.into();
-        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
-        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
-        let (mut tx, rx) = watch::channel_with(settings);
-        executor
-            .spawn(async move {
-                while events.next().await.is_some() {
-                    if let Some(settings) = Self::load(fs.clone(), &path).await {
-                        if tx.send(settings).await.is_err() {
-                            break;
-                        }
-                    }
-                }
-            })
-            .detach();
-        Self(rx)
-    }
-
-    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
-        if fs.is_file(&path).await {
-            fs.load(&path)
-                .await
-                .log_err()
-                .and_then(|data| serde_json::from_str(&data).log_err())
-        } else {
-            Some(SettingsFileContent::default())
-        }
-    }
-}
-
-impl Settings {
-    pub fn file_json_schema() -> serde_json::Value {
-        serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
-    }
-
-    pub fn from_files(
-        defaults: Self,
-        sources: Vec<SettingsFile>,
-        theme_registry: Arc<ThemeRegistry>,
-        font_cache: Arc<FontCache>,
-    ) -> impl futures::stream::Stream<Item = Self> {
-        stream::select_all(sources.iter().enumerate().map(|(i, source)| {
-            let mut rx = source.0.clone();
-            // Consume the initial item from all of the constituent file watches but one.
-            // This way, the stream will yield exactly one item for the files' initial
-            // state, and won't return any more items until the files change.
-            if i > 0 {
-                rx.try_recv().ok();
-            }
-            rx
-        }))
-        .map(move |_| {
-            let mut settings = defaults.clone();
-            for source in &sources {
-                settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
-            }
-            settings
-        })
-    }
-
-    pub fn new(
-        buffer_font_family: &str,
-        font_cache: &FontCache,
-        theme: Arc<Theme>,
-    ) -> Result<Self> {
-        Ok(Self {
-            buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
-            buffer_font_size: 15.,
-            vim_mode: false,
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            preferred_line_length: 80,
-            language_overrides: Default::default(),
-            theme,
-        })
-    }
-
-    pub fn with_overrides(
-        mut self,
-        language_name: impl Into<Arc<str>>,
-        overrides: LanguageOverride,
-    ) -> Self {
-        self.language_overrides
-            .insert(language_name.into(), overrides);
-        self
-    }
-
-    pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.tab_size)
-            .unwrap_or(self.tab_size)
-    }
-
-    pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.soft_wrap)
-            .unwrap_or(self.soft_wrap)
-    }
-
-    pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
-        language
-            .and_then(|language| self.language_overrides.get(language.name().as_ref()))
-            .and_then(|settings| settings.preferred_line_length)
-            .unwrap_or(self.preferred_line_length)
-    }
-
-    #[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_size: 14.,
-            vim_mode: false,
-            tab_size: 4,
-            soft_wrap: SoftWrap::None,
-            preferred_line_length: 80,
-            language_overrides: Default::default(),
-            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
-        }
-    }
-
-    fn merge(
-        &mut self,
-        data: &SettingsFileContent,
-        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() {
-                self.buffer_font_family = id;
-            }
-        }
-        if let Some(value) = &data.theme {
-            if let Some(theme) = theme_registry.get(value).log_err() {
-                self.theme = theme;
-            }
-        }
-
-        merge(&mut self.buffer_font_size, data.buffer_font_size);
-        merge(&mut self.vim_mode, data.vim_mode);
-        merge(&mut self.soft_wrap, data.editor.soft_wrap);
-        merge(&mut self.tab_size, data.editor.tab_size);
-        merge(
-            &mut self.preferred_line_length,
-            data.editor.preferred_line_length,
-        );
-
-        for (language_name, settings) in &data.language_overrides {
-            let target = self
-                .language_overrides
-                .entry(language_name.clone())
-                .or_default();
-
-            merge_option(&mut target.tab_size, settings.tab_size);
-            merge_option(&mut target.soft_wrap, settings.soft_wrap);
-            merge_option(
-                &mut target.preferred_line_length,
-                settings.preferred_line_length,
-            );
-        }
-    }
-}
-
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}
-
-fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
-    if value.is_some() {
-        *target = value;
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use project::FakeFs;
-
-    #[gpui::test]
-    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
-        let executor = cx.background();
-        let fs = FakeFs::new(executor.clone());
-
-        fs.save(
-            "/settings1.json".as_ref(),
-            &r#"
-            {
-                "buffer_font_size": 24,
-                "soft_wrap": "editor_width",
-                "language_overrides": {
-                    "Markdown": {
-                        "preferred_line_length": 100,
-                        "soft_wrap": "preferred_line_length"
-                    }
-                }
-            }
-            "#
-            .into(),
-        )
-        .await
-        .unwrap();
-
-        let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
-        let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
-        let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
-
-        let mut settings_rx = Settings::from_files(
-            cx.read(Settings::test),
-            vec![source1, source2, source3],
-            ThemeRegistry::new((), cx.font_cache()),
-            cx.font_cache(),
-        );
-
-        let settings = settings_rx.next().await.unwrap();
-        let md_settings = settings.language_overrides.get("Markdown").unwrap();
-        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
-        assert_eq!(settings.buffer_font_size, 24.0);
-        assert_eq!(settings.tab_size, 4);
-        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
-        assert_eq!(md_settings.preferred_line_length, Some(100));
-
-        fs.save(
-            "/settings2.json".as_ref(),
-            &r#"
-            {
-                "tab_size": 2,
-                "soft_wrap": "none",
-                "language_overrides": {
-                    "Markdown": {
-                        "preferred_line_length": 120
-                    }
-                }
-            }
-            "#
-            .into(),
-        )
-        .await
-        .unwrap();
-
-        let settings = settings_rx.next().await.unwrap();
-        let md_settings = settings.language_overrides.get("Markdown").unwrap();
-        assert_eq!(settings.soft_wrap, SoftWrap::None);
-        assert_eq!(settings.buffer_font_size, 24.0);
-        assert_eq!(settings.tab_size, 2);
-        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
-        assert_eq!(md_settings.preferred_line_length, Some(120));
-
-        fs.remove_file("/settings2.json".as_ref(), Default::default())
-            .await
-            .unwrap();
-
-        let settings = settings_rx.next().await.unwrap();
-        assert_eq!(settings.tab_size, 4);
-    }
-}

crates/workspace/src/sidebar.rs 🔗

@@ -1,5 +1,6 @@
 use super::Workspace;
-use gpui::{action, elements::*, platform::CursorStyle, AnyViewHandle, RenderContext};
+use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
+use serde::Deserialize;
 use std::{cell::RefCell, rc::Rc};
 use theme::Theme;
 
@@ -10,7 +11,7 @@ pub struct Sidebar {
     width: Rc<RefCell<f32>>,
 }
 
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Deserialize)]
 pub enum Side {
     Left,
     Right,
@@ -21,10 +22,15 @@ struct Item {
     view: AnyViewHandle,
 }
 
-action!(ToggleSidebarItem, SidebarItemId);
-action!(ToggleSidebarItemFocus, SidebarItemId);
+#[derive(Clone, Deserialize)]
+pub struct ToggleSidebarItem(pub SidebarItemId);
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
+pub struct ToggleSidebarItemFocus(pub SidebarItemId);
+
+impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+
+#[derive(Clone, Deserialize)]
 pub struct SidebarItemId {
     pub side: Side,
     pub item_index: usize,

crates/workspace/src/status_bar.rs 🔗

@@ -1,4 +1,5 @@
-use crate::{ItemHandle, Pane, Settings};
+use crate::{ItemHandle, Pane};
+use settings::Settings;
 use gpui::{
     elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
     View, ViewContext, ViewHandle,

crates/workspace/src/toolbar.rs 🔗

@@ -1,8 +1,9 @@
-use crate::{ItemHandle, Settings};
+use crate::ItemHandle;
 use gpui::{
     elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
     View, ViewContext, ViewHandle,
 };
+use settings::Settings;
 
 pub trait ToolbarItemView: View {
     fn set_active_pane_item(

crates/workspace/src/workspace.rs 🔗

@@ -2,7 +2,6 @@ pub mod lsp_status;
 pub mod menu;
 pub mod pane;
 pub mod pane_group;
-pub mod settings;
 pub mod sidebar;
 mod status_bar;
 mod toolbar;
@@ -14,16 +13,16 @@ use client::{
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
 use gpui::{
-    action,
+    actions,
     color::Color,
     elements::*,
-    geometry::{vector::vec2f, PathBuilder},
-    json::{self, to_string_pretty, ToJson},
-    keymap::Binding,
+    geometry::{rect::RectF, vector::vec2f, PathBuilder},
+    impl_internal_actions,
+    json::{self, ToJson},
     platform::{CursorStyle, WindowOptions},
-    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity,
-    ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
-    View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
+    ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
@@ -31,8 +30,8 @@ pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
-pub use settings::Settings;
-use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
+use settings::Settings;
+use sidebar::{Side, Sidebar, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -70,32 +69,56 @@ type FollowableItemBuilders = HashMap<
     ),
 >;
 
-action!(Open, Arc<AppState>);
-action!(OpenNew, Arc<AppState>);
-action!(OpenPaths, OpenParams);
-action!(ToggleShare);
-action!(ToggleFollow, PeerId);
-action!(FollowNextCollaborator);
-action!(Unfollow);
-action!(JoinProject, JoinProjectParams);
-action!(Save);
-action!(DebugElements);
-action!(ActivatePreviousPane);
-action!(ActivateNextPane);
+actions!(
+    workspace,
+    [
+        ToggleShare,
+        Unfollow,
+        Save,
+        ActivatePreviousPane,
+        ActivateNextPane,
+        FollowNextCollaborator,
+    ]
+);
+
+#[derive(Clone)]
+pub struct Open(pub Arc<AppState>);
+
+#[derive(Clone)]
+pub struct OpenNew(pub Arc<AppState>);
+
+#[derive(Clone)]
+pub struct OpenPaths {
+    pub paths: Vec<PathBuf>,
+    pub app_state: Arc<AppState>,
+}
+
+#[derive(Clone)]
+pub struct ToggleFollow(pub PeerId);
+
+#[derive(Clone)]
+pub struct JoinProject {
+    pub project_id: u64,
+    pub app_state: Arc<AppState>,
+}
+
+impl_internal_actions!(
+    workspace,
+    [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
+);
 
 pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
     pane::init(cx);
-    menu::init(cx);
 
     cx.add_global_action(open);
     cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
-        open_paths(&action.0.paths, &action.0.app_state, cx).detach();
+        open_paths(&action.paths, &action.app_state, cx).detach();
     });
     cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
         open_new(&action.0, cx)
     });
     cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
-        join_project(action.0.project_id, &action.0.app_state, cx).detach();
+        join_project(action.project_id, &action.app_state, cx).detach();
     });
 
     cx.add_action(Workspace::toggle_share);
@@ -112,7 +135,6 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
             workspace.save_active_item(cx).detach_and_log_err(cx);
         },
     );
-    cx.add_action(Workspace::debug_elements);
     cx.add_action(Workspace::toggle_sidebar_item);
     cx.add_action(Workspace::toggle_sidebar_item_focus);
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
@@ -121,29 +143,6 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
         workspace.activate_next_pane(cx)
     });
-    cx.add_bindings(vec![
-        Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None),
-        Binding::new("cmd-s", Save, None),
-        Binding::new("cmd-alt-i", DebugElements, None),
-        Binding::new("cmd-k cmd-left", ActivatePreviousPane, None),
-        Binding::new("cmd-k cmd-right", ActivateNextPane, None),
-        Binding::new(
-            "cmd-shift-!",
-            ToggleSidebarItem(SidebarItemId {
-                side: Side::Left,
-                item_index: 0,
-            }),
-            None,
-        ),
-        Binding::new(
-            "cmd-1",
-            ToggleSidebarItemFocus(SidebarItemId {
-                side: Side::Left,
-                item_index: 0,
-            }),
-            None,
-        ),
-    ]);
 
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
@@ -188,18 +187,6 @@ pub struct AppState {
         fn(ModelHandle<Project>, &Arc<AppState>, &mut ViewContext<Workspace>) -> Workspace,
 }
 
-#[derive(Clone)]
-pub struct OpenParams {
-    pub paths: Vec<PathBuf>,
-    pub app_state: Arc<AppState>,
-}
-
-#[derive(Clone)]
-pub struct JoinProjectParams {
-    pub project_id: u64,
-    pub app_state: Arc<AppState>,
-}
-
 pub trait Item: View {
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@@ -386,6 +373,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
         -> Task<Result<()>>;
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
     fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
+    fn on_release(
+        &self,
+        cx: &mut MutableAppContext,
+        callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+    ) -> gpui::Subscription;
 }
 
 pub trait WeakItemHandle {
@@ -421,17 +413,17 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
-    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
+    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
         self.update(cx, |item, cx| {
-            cx.add_option_view(|cx| item.clone_on_split(cx))
+            item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
         })
-        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
     }
 
-    fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
+    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
-            item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
+            cx.add_option_view(|cx| item.clone_on_split(cx))
         })
+        .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
     }
 
     fn added_to_pane(
@@ -494,8 +486,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             }
 
             if T::should_close_item_on_event(event) {
-                pane.update(cx, |pane, cx| pane.close_item(item.id(), cx))
-                    .detach();
+                Pane::close_item(workspace, pane, item.id(), cx).detach_and_log_err(cx);
                 return;
             }
 
@@ -523,6 +514,30 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.update(cx, |this, cx| this.navigate(data, cx))
     }
 
+    fn id(&self) -> usize {
+        self.id()
+    }
+
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
+    }
+
+    fn is_dirty(&self, cx: &AppContext) -> bool {
+        self.read(cx).is_dirty(cx)
+    }
+
+    fn has_conflict(&self, cx: &AppContext) -> bool {
+        self.read(cx).has_conflict(cx)
+    }
+
+    fn can_save(&self, cx: &AppContext) -> bool {
+        self.read(cx).can_save(cx)
+    }
+
+    fn can_save_as(&self, cx: &AppContext) -> bool {
+        self.read(cx).can_save_as(cx)
+    }
+
     fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
         self.update(cx, |item, cx| item.save(project, cx))
     }
@@ -544,30 +559,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         self.update(cx, |item, cx| item.reload(project, cx))
     }
 
-    fn is_dirty(&self, cx: &AppContext) -> bool {
-        self.read(cx).is_dirty(cx)
-    }
-
-    fn has_conflict(&self, cx: &AppContext) -> bool {
-        self.read(cx).has_conflict(cx)
-    }
-
-    fn id(&self) -> usize {
-        self.id()
-    }
-
-    fn to_any(&self) -> AnyViewHandle {
-        self.into()
-    }
-
-    fn can_save(&self, cx: &AppContext) -> bool {
-        self.read(cx).can_save(cx)
-    }
-
-    fn can_save_as(&self, cx: &AppContext) -> bool {
-        self.read(cx).can_save_as(cx)
-    }
-
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
         self.read(cx).act_as_type(type_id, self, cx)
     }
@@ -581,6 +572,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             None
         }
     }
+
+    fn on_release(
+        &self,
+        cx: &mut MutableAppContext,
+        callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+    ) -> gpui::Subscription {
+        cx.observe_release(self, move |_, cx| callback(cx))
+    }
 }
 
 impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
@@ -611,6 +610,7 @@ pub struct WorkspaceParams {
     pub client: Arc<Client>,
     pub fs: Arc<dyn Fs>,
     pub languages: Arc<LanguageRegistry>,
+    pub themes: Arc<ThemeRegistry>,
     pub user_store: ModelHandle<UserStore>,
     pub channel_list: ModelHandle<ChannelList>,
 }
@@ -640,6 +640,7 @@ impl WorkspaceParams {
             channel_list: cx
                 .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
             client,
+            themes: ThemeRegistry::new((), cx.font_cache().clone()),
             fs,
             languages,
             user_store,
@@ -658,6 +659,7 @@ impl WorkspaceParams {
             ),
             client: app_state.client.clone(),
             fs: app_state.fs.clone(),
+            themes: app_state.themes.clone(),
             languages: app_state.languages.clone(),
             user_store: app_state.user_store.clone(),
             channel_list: app_state.channel_list.clone(),
@@ -675,6 +677,7 @@ pub struct Workspace {
     user_store: ModelHandle<client::UserStore>,
     remote_entity_subscription: Option<Subscription>,
     fs: Arc<dyn Fs>,
+    themes: Arc<ThemeRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_sidebar: Sidebar,
@@ -735,7 +738,7 @@ impl Workspace {
         })
         .detach();
 
-        let pane = cx.add_view(|cx| Pane::new(params.project.clone(), cx));
+        let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
             let active_entry = me.active_project_path(cx);
@@ -783,6 +786,7 @@ impl Workspace {
             remote_entity_subscription: None,
             user_store: params.user_store.clone(),
             fs: params.fs.clone(),
+            themes: params.themes.clone(),
             left_sidebar: Sidebar::new(Side::Left),
             right_sidebar: Sidebar::new(Side::Right),
             project: params.project.clone(),
@@ -815,6 +819,10 @@ impl Workspace {
         &self.project
     }
 
+    pub fn themes(&self) -> Arc<ThemeRegistry> {
+        self.themes.clone()
+    }
+
     pub fn worktrees<'a>(
         &self,
         cx: &'a AppContext,
@@ -1050,24 +1058,8 @@ impl Workspace {
         cx.notify();
     }
 
-    pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
-        match to_string_pretty(&cx.debug_elements()) {
-            Ok(json) => {
-                let kib = json.len() as f32 / 1024.;
-                cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
-                log::info!(
-                    "copied {:.1} KiB of element debug JSON to the clipboard",
-                    kib
-                );
-            }
-            Err(error) => {
-                log::error!("error debugging elements: {}", error);
-            }
-        };
-    }
-
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(|cx| Pane::new(self.project.clone(), cx));
+        let pane = cx.add_view(|cx| Pane::new(cx));
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
             let active_entry = me.active_project_path(cx);
@@ -2067,7 +2059,8 @@ impl Element for AvatarRibbon {
     fn dispatch_event(
         &mut self,
         _: &gpui::Event,
-        _: gpui::geometry::rect::RectF,
+        _: RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut gpui::EventContext,
@@ -2090,9 +2083,9 @@ impl Element for AvatarRibbon {
     }
 }
 
-impl std::fmt::Debug for OpenParams {
+impl std::fmt::Debug for OpenPaths {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("OpenParams")
+        f.debug_struct("OpenPaths")
             .field("paths", &self.paths)
             .finish()
     }
@@ -2107,7 +2100,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
     });
     cx.spawn(|mut cx| async move {
         if let Some(paths) = paths.recv().await.flatten() {
-            cx.update(|cx| cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })));
+            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
         }
     })
     .detach();
@@ -2119,7 +2112,10 @@ pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
     cx: &mut MutableAppContext,
-) -> Task<ViewHandle<Workspace>> {
+) -> Task<(
+    ViewHandle<Workspace>,
+    Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
+)> {
     log::info!("open paths {:?}", abs_paths);
 
     // Open paths in existing workspace if possible
@@ -2156,8 +2152,8 @@ pub fn open_paths(
 
     let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
     cx.spawn(|_| async move {
-        task.await;
-        workspace
+        let items = task.await;
+        (workspace, items)
     })
 }
 

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.24.0"
+version = "0.29.0"
 
 [lib]
 name = "zed"
@@ -15,10 +15,13 @@ name = "Zed"
 path = "src/main.rs"
 
 [dependencies]
+assets = { path = "../assets" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
 chat_panel = { path = "../chat_panel" }
+cli = { path = "../cli" }
 collections = { path = "../collections" }
+command_palette = { path = "../command_palette" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 contacts_panel = { path = "../contacts_panel" }
@@ -38,6 +41,7 @@ project = { path = "../project" }
 project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
 rpc = { path = "../rpc" }
+settings = { path = "../settings" }
 sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
 theme = { path = "../theme" }
@@ -49,7 +53,6 @@ anyhow = "1.0.38"
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-recursion = "0.3"
 async-trait = "0.1"
-crossbeam-channel = "0.5.0"
 ctor = "0.1.20"
 dirs = "3.0"
 easy-parallel = "3.1.0"
@@ -61,7 +64,7 @@ image = "0.23"
 indexmap = "1.6.2"
 lazy_static = "1.4.0"
 libc = "0.2"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 log-panics = { version = "2.0", features = ["with-backtrace"] }
 num_cpus = "1.13.0"
 parking_lot = "0.11.1"
@@ -98,6 +101,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 env_logger = "0.8"

crates/zed/assets/themes/_base.toml 🔗

@@ -1,413 +0,0 @@
-[text]
-base = { family = "Zed Sans", size = 14 }
-
-[workspace]
-background = "$surface.0"
-pane_divider = { width = 1, color = "$border.0" }
-leader_border_opacity = 0.7
-leader_border_width = 2.0
-
-[workspace.titlebar]
-height = 32
-border = { width = 1, bottom = true, color = "$border.0" }
-title = "$text.0"
-avatar_width = 18
-avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
-avatar_ribbon = { background = "#ff0000", height = 3, width = 12 }
-outdated_warning = { extends = "$text.2", size = 13 }
-share_icon_color = "$text.2.color"
-share_icon_active_color = "$text.0.color"
-
-[workspace.titlebar.sign_in_prompt]
-extends = "$text.2"
-size = 13
-underline = true
-padding = { right = 8 }
-
-[workspace.titlebar.hovered_sign_in_prompt]
-extends = "$workspace.titlebar.sign_in_prompt"
-color = "$text.1.color"
-
-[workspace.titlebar.offline_icon]
-padding = { right = 4 }
-width = 16
-color = "$text.2.color"
-
-[workspace.tab]
-height = 34
-text = "$text.2"
-padding = { left = 12, right = 12 }
-icon_width = 8
-spacing = 10
-icon_close = "$text.2.color"
-icon_close_active = "$text.0.color"
-icon_dirty = "$status.info"
-icon_conflict = "$status.warn"
-border = { left = true, bottom = true, width = 1, color = "$border.0", overlay = true }
-
-[workspace.active_tab]
-extends = "$workspace.tab"
-border.bottom = false
-background = "$surface.1"
-text = "$text.0"
-
-[workspace.sidebar]
-width = 30
-border = { right = true, width = 1, color = "$border.0" }
-
-[workspace.sidebar.resize_handle]
-padding = { left = 1 }
-background = "$border.0"
-
-[workspace.sidebar.item]
-icon_color = "$text.2.color"
-icon_size = 18
-height = "$workspace.tab.height"
-
-[workspace.sidebar.active_item]
-extends = "$workspace.sidebar.item"
-icon_color = "$text.0.color"
-
-[workspace.left_sidebar]
-extends = "$workspace.sidebar"
-border = { width = 1, color = "$border.0", right = true }
-
-[workspace.right_sidebar]
-extends = "$workspace.sidebar"
-border = { width = 1, color = "$border.0", left = true }
-
-[workspace.status_bar]
-padding = { left = 6, right = 6 }
-height = 24
-item_spacing = 8
-cursor_position = "$text.2"
-diagnostic_message = "$text.2"
-lsp_message = "$text.2"
-auto_update_progress_message = "$text.2"
-auto_update_done_message = "$text.2"
-
-[workspace.toolbar]
-background = "$surface.1"
-border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false }
-height = 34
-item_spacing = 8
-padding = { left = 16, right = 8, top = 4, bottom = 4 }
-
-[breadcrumbs]
-extends = "$text.1"
-padding = { left = 6 }
-
-[panel]
-padding = { top = 12, left = 12, bottom = 12, right = 12 }
-
-[chat_panel]
-extends = "$panel"
-channel_name = { extends = "$text.0", weight = "bold" }
-channel_name_hash = { text = "$text.2", padding.right = 8 }
-
-[chat_panel.message]
-body = "$text.1"
-sender = { extends = "$text.0", weight = "bold", margin.right = 8 }
-timestamp = "$text.2"
-padding.bottom = 6
-
-[chat_panel.pending_message]
-extends = "$chat_panel.message"
-body = { color = "$text.3.color" }
-sender = { color = "$text.3.color" }
-timestamp = { color = "$text.3.color" }
-
-[chat_panel.channel_select.item]
-padding = 4
-name = "$text.1"
-hash = { extends = "$text.2", margin.right = 8 }
-
-[chat_panel.channel_select.hovered_item]
-extends = "$chat_panel.channel_select.item"
-background = "$state.hover"
-corner_radius = 6
-
-[chat_panel.channel_select.active_item]
-extends = "$chat_panel.channel_select.item"
-name = "$text.0"
-
-[chat_panel.channel_select.hovered_active_item]
-extends = "$chat_panel.channel_select.hovered_item"
-name = "$text.0"
-
-[chat_panel.channel_select.header]
-extends = "$chat_panel.channel_select.active_item"
-padding.bottom = 4
-padding.left = 0
-
-[chat_panel.channel_select.menu]
-padding = 4
-corner_radius = 6
-border = { color = "$border.0", width = 1 }
-background = "$surface.0"
-shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
-
-[chat_panel.input_editor]
-background = "$surface.1"
-corner_radius = 6
-padding = { left = 8, right = 8, top = 7, bottom = 7 }
-text = "$text.0"
-placeholder_text = "$text.2"
-selection = "$selection.host"
-border = { width = 1, color = "$border.0" }
-
-[chat_panel.sign_in_prompt]
-extends = "$text.0"
-underline = true
-
-[chat_panel.hovered_sign_in_prompt]
-extends = "$chat_panel.sign_in_prompt"
-color = "$text.1.color"
-
-[contacts_panel]
-extends = "$panel"
-host_row_height = 28
-host_avatar = { corner_radius = 10, width = 18 }
-host_username = { extends = "$text.0", padding.left = 8 }
-tree_branch_width = 1
-tree_branch_color = "$surface.2"
-
-[contacts_panel.project]
-height = 24
-padding = { left = 8 }
-guest_avatar = { corner_radius = 8, width = 14 }
-guest_avatar_spacing = 4
-
-[contacts_panel.project.name]
-extends = "$text.1"
-margin = { right = 6 }
-
-[contacts_panel.unshared_project]
-extends = "$contacts_panel.project"
-
-[contacts_panel.hovered_unshared_project]
-extends = "$contacts_panel.unshared_project"
-background = "$state.hover"
-corner_radius = 6
-
-[contacts_panel.shared_project]
-extends = "$contacts_panel.project"
-name.color = "$text.0.color"
-
-[contacts_panel.hovered_shared_project]
-extends = "$contacts_panel.shared_project"
-background = "$state.hover"
-corner_radius = 6
-
-[project_panel]
-extends = "$panel"
-padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
-
-[project_panel.entry]
-text = "$text.1"
-height = 22
-icon_color = "$text.3.color"
-icon_size = 8
-icon_spacing = 8
-
-[project_panel.hovered_entry]
-extends = "$project_panel.entry"
-background = "$state.hover"
-
-[project_panel.selected_entry]
-extends = "$project_panel.entry"
-text = { extends = "$text.0" }
-
-[project_panel.hovered_selected_entry]
-extends = "$project_panel.hovered_entry"
-text = { extends = "$text.0" }
-
-[selector]
-background = "$surface.0"
-padding = 8
-margin = { top = 52, bottom = 52 }
-corner_radius = 6
-shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
-border = { width = 1, color = "$border.0" }
-
-[selector.input_editor]
-background = "$surface.1"
-corner_radius = 6
-padding = { left = 16, right = 16, top = 7, bottom = 7 }
-text = "$text.0"
-placeholder_text = "$text.2"
-selection = "$selection.host"
-border = { width = 1, color = "$border.0" }
-
-[selector.empty]
-text = "$text.2"
-padding = { left = 16, right = 16, top = 8, bottom = 4 }
-
-[selector.item]
-text = "$text.1"
-highlight_text = { extends = "$text.base", color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
-padding = { left = 16, right = 16, top = 4, bottom = 4 }
-corner_radius = 6
-
-[selector.active_item]
-extends = "$selector.item"
-background = "$state.hover"
-text = "$text.0"
-
-[editor]
-text_color = "$text.1.color"
-background = "$surface.1"
-gutter_background = "$surface.1"
-gutter_padding_factor = 2.5
-active_line_background = "$state.active_line"
-highlighted_line_background = "$state.highlighted_line"
-rename_fade = 0.6
-unnecessary_code_fade = 0.5
-document_highlight_read_background = "#99999920"
-document_highlight_write_background = "#99999916"
-diff_background_deleted = "$state.deleted_line"
-diff_background_inserted = "$state.inserted_line"
-line_number = "$text.2.color"
-line_number_active = "$text.0.color"
-selection = "$selection.host"
-guest_selections = "$selection.guests"
-error_color = "$status.bad"
-code_actions_indicator = "$text.3.color"
-
-[editor.diagnostic_path_header]
-background = "$state.active_line"
-filename = { extends = "$text.0", size = 14 }
-path = { extends = "$text.2", size = 14, margin.left = 12 }
-text_scale_factor = 0.857
-
-[editor.diagnostic_header]
-background = "$editor.background"
-border = { width = 1, top = true, bottom = true, color = "$border.1" }
-code = { extends = "$text.2", size = 14, margin.left = 10 }
-icon_width_factor = 1.5
-text_scale_factor = 0.857
-
-[editor.diagnostic_header.message]
-text = { extends = "$text.1", size = 14 }
-highlight_text = { extends = "$text.0", size = 14, weight = "bold" }
-
-[editor.error_diagnostic]
-header.border = { width = 1, top = true, color = "$border.0" }
-text_scale_factor = 0.857
-
-[editor.error_diagnostic.message]
-text = { extends = "$text.1", size = 14, color = "$status.bad" }
-highlight_text = { extends = "$text.1", size = 14, color = "$status.bad", weight = "bold" }
-
-[editor.warning_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$status.warn"
-message.highlight_text.color = "$status.warn"
-
-[editor.information_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$status.info"
-message.highlight_text.color = "$status.info"
-
-[editor.hint_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$status.info"
-message.highlight_text.color = "$status.info"
-
-[editor.invalid_error_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.invalid_warning_diagnostic]
-extends = "$editor.warning_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.invalid_information_diagnostic]
-extends = "$editor.information_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.invalid_hint_diagnostic]
-extends = "$editor.hint_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.autocomplete]
-background = "$surface.2"
-border = { width = 2, color = "$border.1" }
-corner_radius = 6
-padding = 6
-match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
-margin.left = -14
-
-[editor.autocomplete.item]
-padding = { left = 6, right = 6, top = 2, bottom = 2 }
-corner_radius = 6
-
-[editor.autocomplete.selected_item]
-extends = "$editor.autocomplete.item"
-background = "$state.selected"
-
-[editor.autocomplete.hovered_item]
-extends = "$editor.autocomplete.item"
-background = "$state.hover"
-
-[project_diagnostics]
-background = "$surface.1"
-empty_message = { extends = "$text.0", size = 18 }
-status_bar_item = { extends = "$text.2", margin.right = 10 }
-tab_icon_width = 13
-tab_icon_spacing = 4
-tab_summary_spacing = 10
-
-[search]
-match_background = "$state.highlighted_line"
-results_status = { extends = "$text.0", size = 18 }
-tab_icon_width = 14
-tab_icon_spacing = 4
-
-[search.option_button]
-extends = "$text.1"
-padding = { left = 6, right = 6, top = 1, bottom = 1 }
-corner_radius = 6
-background = "$surface.1"
-border = { width = 1, color = "$border.0" }
-margin.left = 1
-margin.right = 1
-
-[search.option_button_group]
-padding = { left = 2, right = 2 }
-
-[search.active_option_button]
-extends = "$search.option_button"
-background = "$surface.2"
-
-[search.hovered_option_button]
-extends = "$search.option_button"
-background = "$surface.2"
-
-[search.active_hovered_option_button]
-extends = "$search.option_button"
-background = "$surface.2"
-
-[search.match_index]
-extends = "$text.2"
-padding = 6
-
-[search.editor]
-min_width = 200
-max_width = 500
-background = "$surface.0"
-corner_radius = 6
-padding = { left = 14, right = 14, top = 3, bottom = 3 }
-margin = { right = 5 }
-text = "$text.0"
-placeholder_text = "$text.2"
-selection = "$selection.host"
-border = { width = 1, color = "$border.0" }
-
-[search.invalid_editor]
-extends = "$search.editor"
-border = { width = 1, color = "$status.bad" }

crates/zed/assets/themes/black.toml 🔗

@@ -1,67 +0,0 @@
-extends = "_base"
-
-[surface]
-0 = "#222222"
-1 = "#0f0b0c"
-2 = "#131415"
-
-[border]
-0 = "#000000B2"
-1 = "#FFFFFF20"
-
-[text]
-0 = { extends = "$text.base", color = "#ffffff" }
-1 = { extends = "$text.base", color = "#b3b3b3" }
-2 = { extends = "$text.base", color = "#7b7d80" }
-3 = { extends = "$text.base", color = "#66686A" }
-
-[shadow]
-0 = "#00000052"
-
-[selection]
-host = { selection = "#3B57BC55", cursor = "$text.0.color" }
-guests = [
-  { selection = "#FDF35133", cursor = "#FDF351" },
-  { selection = "#4EACAD33", cursor = "#4EACAD" },
-  { selection = "#D0453B33", cursor = "#D0453B" },
-  { selection = "#3B874B33", cursor = "#3B874B" },
-  { selection = "#BD7CB433", cursor = "#BD7CB4" },
-  { selection = "#EE823133", cursor = "#EE8231" },
-  { selection = "#5A2B9233", cursor = "#5A2B92" },
-]
-
-[status]
-good = "#4fac63"
-info = "#3c5dd4"
-warn = "#faca50"
-bad = "#b7372e"
-
-[state]
-active_line = "#161313"
-highlighted_line = "#faca5033"
-deleted_line = "#dd000036"
-inserted_line = "#00dd0036"
-hover = "#00000033"
-selected = "#00000088"
-
-[editor.syntax]
-keyword = { color = "#0086c0", weight = "bold" }
-function = "#dcdcaa"
-string = "#cb8f77"
-type = "#4ec9b0"
-number = "#b5cea8"
-comment = "#6a9955"
-property = "#4e94ce"
-variant = "#4fc1ff"
-constant = "#9cdcfe"
-title = { color = "#9cdcfe", weight = "bold" }
-emphasis = "#4ec9b0"
-"emphasis.strong" = { color = "#4ec9b0", weight = "bold" }
-link_uri = { color = "#6a9955", underline = true }
-link_text = { color = "#cb8f77", italic = true }
-list_marker = "#4e94ce"
-
-[workspace.disconnected_overlay]
-extends = "$text.base"
-color = "#ffffff"
-background = "#000000aa"

crates/zed/assets/themes/dark.toml 🔗

@@ -1,67 +0,0 @@
-extends = "_base"
-
-[surface]
-0 = "#283340"
-1 = "#1C2733"
-2 = "#1C2733"
-
-[border]
-0 = "#1B222B"
-1 = "#FFFFFF20"
-
-[text]
-0 = { extends = "$text.base", color = "#FFFFFF" }
-1 = { extends = "$text.base", color = "#CDD1E2" }
-2 = { extends = "$text.base", color = "#9BA8BE" }
-3 = { extends = "$text.base", color = "#6E7483" }
-
-[shadow]
-0 = "#00000052"
-
-[selection]
-host = { selection = "#3B57BC55", cursor = "$text.0.color" }
-guests = [
-  { selection = "#FDF35133", cursor = "#FDF351" },
-  { selection = "#4EACAD33", cursor = "#4EACAD" },
-  { selection = "#D0453B33", cursor = "#D0453B" },
-  { selection = "#3B874B33", cursor = "#3B874B" },
-  { selection = "#BD7CB433", cursor = "#BD7CB4" },
-  { selection = "#EE823133", cursor = "#EE8231" },
-  { selection = "#5A2B9233", cursor = "#5A2B92" },
-]
-
-[status]
-good = "#4fac63"
-info = "#3c5dd4"
-warn = "#faca50"
-bad = "#b7372e"
-
-[state]
-active_line = "#00000022"
-highlighted_line = "#faca5033"
-deleted_line = "#dd000036"
-inserted_line = "#00dd0036"
-hover = "#00000033"
-selected = "#00000088"
-
-[editor.syntax]
-keyword = { color = "#0086c0", weight = "bold" }
-function = "#dcdcaa"
-string = "#cb8f77"
-type = "#4ec9b0"
-number = "#b5cea8"
-comment = "#6a9955"
-property = "#4e94ce"
-variant = "#4fc1ff"
-constant = "#9cdcfe"
-title = { color = "#9cdcfe", weight = "bold" }
-emphasis = "#4ec9b0"
-"emphasis.strong" = { color = "#4ec9b0", weight = "bold" }
-link_uri = { color = "#6a9955", underline = true }
-link_text = { color = "#cb8f77", italic = true }
-list_marker = "#4e94ce"
-
-[workspace.disconnected_overlay]
-extends = "$text.base"
-color = "#ffffff"
-background = "#000000aa"

crates/zed/assets/themes/light.toml 🔗

@@ -1,67 +0,0 @@
-extends = "_base"
-
-[surface]
-0 = "#EAEAEB"
-1 = "#FAFAFA"
-2 = "#FFFFFF"
-
-[border]
-0 = "#DDDDDC"
-1 = "#0000000F"
-
-[text]
-0 = { extends = "$text.base", color = "#000000" }
-1 = { extends = "$text.base", color = "#29292B" }
-2 = { extends = "$text.base", color = "#7E7E83" }
-3 = { extends = "$text.base", color = "#939393" }
-
-[shadow]
-0 = "#0000000D"
-
-[selection]
-host = { selection = "#3B57BC55", cursor = "$text.0.color" }
-guests = [
-  { selection = "#D0453B33", cursor = "#D0453B" },
-  { selection = "#3B874B33", cursor = "#3B874B" },
-  { selection = "#BD7CB433", cursor = "#BD7CB4" },
-  { selection = "#EE823133", cursor = "#EE8231" },
-  { selection = "#5A2B9233", cursor = "#5A2B92" },
-  { selection = "#FDF35133", cursor = "#FDF351" },
-  { selection = "#4EACAD33", cursor = "#4EACAD" },
-]
-
-[status]
-good = "#4fac63"
-info = "#3c5dd4"
-warn = "#faca50"
-bad = "#b7372e"
-
-[state]
-active_line = "#00000008"
-highlighted_line = "#faca5033"
-deleted_line = "#dd000036"
-inserted_line = "#00dd0036"
-hover = "#0000000D"
-selected = "#0000001c"
-
-[editor.syntax]
-keyword = { color = "#0000fa", weight = "bold" }
-function = "#795e26"
-string = "#a82121"
-type = "#267f29"
-number = "#b5cea8"
-comment = "#6a9955"
-property = "#4e94ce"
-variant = "#4fc1ff"
-constant = "#5a9ccc"
-title = { color = "#5a9ccc", weight = "bold" }
-emphasis = "#267f29"
-"emphasis.strong" = { color = "#267f29", weight = "bold" }
-link_uri = { color = "#6a9955", underline = true }
-link_text = { color = "#a82121", italic = true }
-list_marker = "#4e94ce"
-
-[workspace.disconnected_overlay]
-extends = "$text.base"
-color = "#ffffff"
-background = "#000000cc"

crates/zed/src/languages.rs 🔗

@@ -47,6 +47,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
             tree_sitter_typescript::language_typescript(),
             Some(Arc::new(typescript::TypeScriptLspAdapter)),
         ),
+        (
+            "javascript",
+            tree_sitter_typescript::language_tsx(),
+            Some(Arc::new(typescript::TypeScriptLspAdapter)),
+        ),
     ] {
         languages.add(Arc::new(language(name, grammar, lsp_adapter)));
     }

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

@@ -0,0 +1,12 @@
+name = "JavaScript"
+path_suffixes = ["js", "jsx"]
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = false, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
+]

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

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

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

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

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

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

crates/zed/src/main.rs 🔗

@@ -2,30 +2,38 @@
 #![allow(non_snake_case)]
 
 use anyhow::{anyhow, Context, Result};
+use assets::Assets;
+use cli::{
+    ipc::{self, IpcSender},
+    CliRequest, CliResponse, IpcHandshake,
+};
 use client::{self, http, ChannelList, UserStore};
 use fs::OpenOptions;
-use futures::{channel::oneshot, StreamExt};
-use gpui::{App, AssetSource, Task};
+use futures::{
+    channel::{mpsc, oneshot},
+    FutureExt, SinkExt, StreamExt,
+};
+use gpui::{App, AssetSource, AsyncAppContext, Task};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
+use settings::{self, KeymapFile, Settings, SettingsFileContent};
 use smol::process::Command;
-use std::{env, fs, path::PathBuf, sync::Arc};
+use std::{env, fs, path::PathBuf, sync::Arc, thread, time::Duration};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::ResultExt;
-use workspace::{
-    self,
-    settings::{self, SettingsFile},
-    AppState, OpenNew, OpenParams, OpenPaths, Settings,
-};
+use workspace::{self, AppState, OpenNew, OpenPaths};
 use zed::{
-    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
+    self, build_window_options, build_workspace,
+    fs::RealFs,
+    languages, menus,
+    settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
 };
 
 fn main() {
     init_logger();
 
-    let app = gpui::App::new(Assets).unwrap();
+    let mut app = gpui::App::new(Assets).unwrap();
     load_embedded_fonts(&app);
 
     let fs = Arc::new(RealFs);
@@ -46,8 +54,37 @@ fn main() {
                 soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
                 ..Default::default()
             },
+        )
+        .with_overrides(
+            "Rust",
+            settings::LanguageOverride {
+                tab_size: Some(4),
+                ..Default::default()
+            },
+        )
+        .with_overrides(
+            "JavaScript",
+            settings::LanguageOverride {
+                tab_size: Some(2),
+                ..Default::default()
+            },
+        )
+        .with_overrides(
+            "TypeScript",
+            settings::LanguageOverride {
+                tab_size: Some(2),
+                ..Default::default()
+            },
+        )
+        .with_overrides(
+            "TSX",
+            settings::LanguageOverride {
+                tab_size: Some(2),
+                ..Default::default()
+            },
         );
-    let settings_file = load_settings_file(&app, fs.clone());
+
+    let config_files = load_config_files(&app, fs.clone());
 
     let login_shell_env_loaded = if stdout_is_a_pty() {
         Task::ready(())
@@ -57,6 +94,18 @@ fn main() {
         })
     };
 
+    let (cli_connections_tx, mut cli_connections_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() {
+                cli_connections_tx
+                    .unbounded_send(cli_connection)
+                    .map_err(|_| anyhow!("no listener for cli connections"))
+                    .log_err();
+            };
+        }
+    });
+
     app.run(move |cx| {
         let http = http::client();
         let client = client::Client::new(http.clone());
@@ -69,6 +118,7 @@ fn main() {
         project::Project::init(&client);
         client::Channel::init(&client);
         client::init(client.clone(), cx);
+        command_palette::init(cx);
         workspace::init(&client, cx);
         editor::init(cx);
         go_to_line::init(cx);
@@ -97,13 +147,16 @@ fn main() {
         })
         .detach_and_log_err(cx);
 
-        let settings_file = cx.background().block(settings_file).unwrap();
-        let mut settings_rx = Settings::from_files(
+        let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
+        let mut settings_rx = settings_from_files(
             default_settings,
             vec![settings_file],
             themes.clone(),
             cx.font_cache().clone(),
         );
+
+        cx.spawn(|cx| watch_keymap_file(keymap_file, cx)).detach();
+
         let settings = cx.background().block(settings_rx.next()).unwrap();
         cx.spawn(|mut cx| async move {
             while let Some(settings) = settings_rx.next().await {
@@ -130,20 +183,32 @@ fn main() {
             build_workspace,
         });
         journal::init(app_state.clone(), cx);
+        theme_selector::init(cx);
         zed::init(&app_state, cx);
-        theme_selector::init(app_state.themes.clone(), cx);
 
         cx.set_menus(menus::menus(&app_state.clone()));
 
         if stdout_is_a_pty() {
             cx.platform().activate(true);
-        }
-
-        let paths = collect_path_args();
-        if paths.is_empty() {
-            cx.dispatch_global_action(OpenNew(app_state.clone()));
+            let paths = collect_path_args();
+            if paths.is_empty() {
+                cx.dispatch_global_action(OpenNew(app_state.clone()));
+            } else {
+                cx.dispatch_global_action(OpenPaths { paths, app_state });
+            }
         } else {
-            cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
+            if let Ok(Some(connection)) = cli_connections_rx.try_next() {
+                cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
+                    .detach();
+            } else {
+                cx.dispatch_global_action(OpenNew(app_state.clone()));
+            }
+            cx.spawn(|cx| async move {
+                while let Some(connection) = cli_connections_rx.next().await {
+                    handle_cli_connection(connection, app_state.clone(), cx.clone()).await;
+                }
+            })
+            .detach();
         }
     });
 }
@@ -239,15 +304,137 @@ fn load_embedded_fonts(app: &App) {
         .unwrap();
 }
 
-fn load_settings_file(app: &App, fs: Arc<dyn Fs>) -> oneshot::Receiver<SettingsFile> {
+fn load_config_files(
+    app: &App,
+    fs: Arc<dyn Fs>,
+) -> oneshot::Receiver<(
+    WatchedJsonFile<SettingsFileContent>,
+    WatchedJsonFile<KeymapFile>,
+)> {
     let executor = app.background();
     let (tx, rx) = oneshot::channel();
     executor
         .clone()
         .spawn(async move {
-            let file = SettingsFile::new(fs, &executor, zed::SETTINGS_PATH.clone()).await;
-            tx.send(file).ok()
+            let settings_file =
+                WatchedJsonFile::new(fs.clone(), &executor, zed::SETTINGS_PATH.clone()).await;
+            let keymap_file = WatchedJsonFile::new(fs, &executor, zed::KEYMAP_PATH.clone()).await;
+            tx.send((settings_file, keymap_file)).ok()
         })
         .detach();
     rx
 }
+
+fn connect_to_cli(
+    server_name: &str,
+) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
+    let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
+        .context("error connecting to cli")?;
+    let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
+    let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
+
+    handshake_tx
+        .send(IpcHandshake {
+            requests: request_tx,
+            responses: response_rx,
+        })
+        .context("error sending ipc handshake")?;
+
+    let (mut async_request_tx, async_request_rx) =
+        futures::channel::mpsc::channel::<CliRequest>(16);
+    thread::spawn(move || {
+        while let Ok(cli_request) = request_rx.recv() {
+            if smol::block_on(async_request_tx.send(cli_request)).is_err() {
+                break;
+            }
+        }
+        Ok::<_, anyhow::Error>(())
+    });
+
+    Ok((async_request_rx, response_tx))
+}
+
+async fn handle_cli_connection(
+    (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+    app_state: Arc<AppState>,
+    mut cx: AsyncAppContext,
+) {
+    if let Some(request) = requests.next().await {
+        match request {
+            CliRequest::Open { paths, wait } => {
+                let (workspace, items) = cx
+                    .update(|cx| workspace::open_paths(&paths, &app_state, cx))
+                    .await;
+
+                let mut errored = false;
+                let mut futures = Vec::new();
+                cx.update(|cx| {
+                    for (item, path) in items.into_iter().zip(&paths) {
+                        match item {
+                            Some(Ok(item)) => {
+                                let released = oneshot::channel();
+                                item.on_release(
+                                    cx,
+                                    Box::new(move |_| {
+                                        let _ = released.0.send(());
+                                    }),
+                                )
+                                .detach();
+                                futures.push(released.1);
+                            }
+                            Some(Err(err)) => {
+                                responses
+                                    .send(CliResponse::Stderr {
+                                        message: format!("error opening {:?}: {}", path, err),
+                                    })
+                                    .log_err();
+                                errored = true;
+                            }
+                            None => {}
+                        }
+                    }
+                });
+
+                if wait {
+                    let background = cx.background();
+                    let wait = async move {
+                        if paths.is_empty() {
+                            let (done_tx, done_rx) = oneshot::channel();
+                            let _subscription = cx.update(|cx| {
+                                cx.observe_release(&workspace, move |_, _| {
+                                    let _ = done_tx.send(());
+                                })
+                            });
+                            drop(workspace);
+                            let _ = done_rx.await;
+                        } else {
+                            let _ = futures::future::try_join_all(futures).await;
+                        };
+                    }
+                    .fuse();
+                    futures::pin_mut!(wait);
+
+                    loop {
+                        // Repeatedly check if CLI is still open to avoid wasting resources
+                        // waiting for files or workspaces to close.
+                        let mut timer = background.timer(Duration::from_secs(1)).fuse();
+                        futures::select_biased! {
+                            _ = wait => break,
+                            _ = timer => {
+                                if responses.send(CliResponse::Ping).is_err() {
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                responses
+                    .send(CliResponse::Exit {
+                        status: if errored { 1 } else { 0 },
+                    })
+                    .log_err();
+            }
+        }
+    }
+}

crates/zed/src/menus.rs 🔗

@@ -16,7 +16,13 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
                 MenuItem::Action {
                     name: "Check for Updates",
                     keystroke: None,
-                    action: Box::new(super::CheckForUpdates),
+                    action: Box::new(auto_update::Check),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Install CLI",
+                    keystroke: None,
+                    action: Box::new(super::InstallCommandLineInterface),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {

crates/zed/src/settings_file.rs 🔗

@@ -0,0 +1,171 @@
+use futures::{stream, StreamExt};
+use gpui::{executor, AsyncAppContext, FontCache};
+use postage::sink::Sink as _;
+use postage::{prelude::Stream, watch};
+use project::Fs;
+use serde::Deserialize;
+use settings::{KeymapFile, Settings, SettingsFileContent};
+use std::{path::Path, sync::Arc, time::Duration};
+use theme::ThemeRegistry;
+use util::ResultExt;
+
+#[derive(Clone)]
+pub struct WatchedJsonFile<T>(watch::Receiver<T>);
+
+impl<T> WatchedJsonFile<T>
+where
+    T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
+{
+    pub async fn new(
+        fs: Arc<dyn Fs>,
+        executor: &executor::Background,
+        path: impl Into<Arc<Path>>,
+    ) -> Self {
+        let path = path.into();
+        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
+        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
+        let (mut tx, rx) = watch::channel_with(settings);
+        executor
+            .spawn(async move {
+                while events.next().await.is_some() {
+                    if let Some(settings) = Self::load(fs.clone(), &path).await {
+                        if tx.send(settings).await.is_err() {
+                            break;
+                        }
+                    }
+                }
+            })
+            .detach();
+        Self(rx)
+    }
+
+    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
+        if fs.is_file(&path).await {
+            fs.load(&path)
+                .await
+                .log_err()
+                .and_then(|data| serde_json::from_str(&data).log_err())
+        } else {
+            Some(T::default())
+        }
+    }
+}
+
+pub fn settings_from_files(
+    defaults: Settings,
+    sources: Vec<WatchedJsonFile<SettingsFileContent>>,
+    theme_registry: Arc<ThemeRegistry>,
+    font_cache: Arc<FontCache>,
+) -> impl futures::stream::Stream<Item = Settings> {
+    stream::select_all(sources.iter().enumerate().map(|(i, source)| {
+        let mut rx = source.0.clone();
+        // Consume the initial item from all of the constituent file watches but one.
+        // This way, the stream will yield exactly one item for the files' initial
+        // state, and won't return any more items until the files change.
+        if i > 0 {
+            rx.try_recv().ok();
+        }
+        rx
+    }))
+    .map(move |_| {
+        let mut settings = defaults.clone();
+        for source in &sources {
+            settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
+        }
+        settings
+    })
+}
+
+pub async fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFile>, mut cx: AsyncAppContext) {
+    while let Some(content) = file.0.recv().await {
+        cx.update(|cx| {
+            cx.clear_bindings();
+            settings::KeymapFile::load_defaults(cx);
+            content.add(cx).log_err();
+        });
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use project::FakeFs;
+    use settings::SoftWrap;
+
+    #[gpui::test]
+    async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        fs.save(
+            "/settings1.json".as_ref(),
+            &r#"
+            {
+                "buffer_font_size": 24,
+                "soft_wrap": "editor_width",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 100,
+                        "soft_wrap": "preferred_line_length"
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
+        let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
+        let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
+
+        let mut settings_rx = settings_from_files(
+            cx.read(Settings::test),
+            vec![source1, source2, source3],
+            ThemeRegistry::new((), cx.font_cache()),
+            cx.font_cache(),
+        );
+
+        let settings = settings_rx.next().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 4);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(100));
+
+        fs.save(
+            "/settings2.json".as_ref(),
+            &r#"
+            {
+                "tab_size": 2,
+                "soft_wrap": "none",
+                "language_overrides": {
+                    "Markdown": {
+                        "preferred_line_length": 120
+                    }
+                }
+            }
+            "#
+            .into(),
+        )
+        .await
+        .unwrap();
+
+        let settings = settings_rx.next().await.unwrap();
+        let md_settings = settings.language_overrides.get("Markdown").unwrap();
+        assert_eq!(settings.soft_wrap, SoftWrap::None);
+        assert_eq!(settings.buffer_font_size, 24.0);
+        assert_eq!(settings.tab_size, 2);
+        assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+        assert_eq!(md_settings.preferred_line_length, Some(120));
+
+        fs.remove_file("/settings2.json".as_ref(), Default::default())
+            .await
+            .unwrap();
+
+        let settings = settings_rx.next().await.unwrap();
+        assert_eq!(settings.tab_size, 4);
+    }
+}

crates/zed/src/test.rs 🔗

@@ -1,11 +1,12 @@
-use crate::{assets::Assets, build_window_options, build_workspace, AppState};
+use crate::{build_window_options, build_workspace, AppState};
+use assets::Assets;
 use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
 use gpui::MutableAppContext;
 use language::LanguageRegistry;
 use project::fs::FakeFs;
+use settings::Settings;
 use std::sync::Arc;
 use theme::ThemeRegistry;
-use workspace::Settings;
 
 #[cfg(test)]
 #[ctor::ctor]

crates/zed/src/zed.rs 🔗

@@ -1,21 +1,22 @@
-pub mod assets;
 pub mod languages;
 pub mod menus;
+pub mod settings_file;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
+use anyhow::{anyhow, Context, Result};
 use breadcrumbs::Breadcrumbs;
 use chat_panel::ChatPanel;
 pub use client;
 pub use contacts_panel;
 use contacts_panel::ContactsPanel;
 pub use editor;
+use editor::Editor;
 use gpui::{
-    action,
+    actions,
     geometry::vector::vec2f,
-    keymap::Binding,
     platform::{WindowBounds, WindowOptions},
-    ModelHandle, ViewContext,
+    AsyncAppContext, ModelHandle, ViewContext,
 };
 use lazy_static::lazy_static;
 pub use lsp;
@@ -23,15 +24,28 @@ use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
-use std::{path::PathBuf, sync::Arc};
+use serde_json::to_string_pretty;
+use settings::Settings;
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::ResultExt;
 pub use workspace;
-use workspace::{AppState, Settings, Workspace, WorkspaceParams};
-
-action!(About);
-action!(Quit);
-action!(OpenSettings);
-action!(AdjustBufferFontSize, f32);
-action!(CheckForUpdates);
+use workspace::{AppState, Workspace, WorkspaceParams};
+
+actions!(
+    zed,
+    [
+        About,
+        Quit,
+        DebugElements,
+        OpenSettings,
+        IncreaseBufferFontSize,
+        DecreaseBufferFontSize,
+        InstallCommandLineInterface,
+    ]
+);
 
 const MIN_FONT_SIZE: f32 = 6.0;
 
@@ -40,21 +54,27 @@ lazy_static! {
         .expect("failed to determine home directory")
         .join(".zed");
     pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
+    pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json");
 }
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     cx.add_global_action(quit);
-    cx.add_global_action(|_: &CheckForUpdates, cx| auto_update::check(cx));
-    cx.add_global_action({
-        move |action: &AdjustBufferFontSize, cx| {
-            cx.update_global::<Settings, _, _>(|settings, cx| {
-                settings.buffer_font_size =
-                    (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
-                cx.refresh_windows();
-            });
-        }
+    cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
+            settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
+            cx.refresh_windows();
+        });
+    });
+    cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
+        cx.update_global::<Settings, _, _>(|settings, cx| {
+            settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
+            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_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
@@ -93,14 +113,32 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             .detach_and_log_err(cx);
         }
     });
+    cx.add_action(
+        |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+            let content = to_string_pretty(&cx.debug_elements()).unwrap();
+            let project = workspace.project().clone();
+            let json_language = project.read(cx).languages().get_language("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,
+                );
+            }
+        },
+    );
 
     workspace::lsp_status::init(cx);
 
-    cx.add_bindings(vec![
-        Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
-        Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
-        Binding::new("cmd-,", OpenSettings, None),
-    ])
+    settings::KeymapFile::load_defaults(cx);
 }
 
 pub fn build_workspace(
@@ -131,19 +169,23 @@ pub fn build_workspace(
         client: app_state.client.clone(),
         fs: app_state.fs.clone(),
         languages: app_state.languages.clone(),
+        themes: app_state.themes.clone(),
         user_store: app_state.user_store.clone(),
         channel_list: app_state.channel_list.clone(),
     };
     let mut workspace = Workspace::new(&workspace_params, cx);
     let project = workspace.project().clone();
 
+    let theme_names = app_state.themes.list().collect();
+    let language_names = app_state.languages.language_names();
+
     project.update(cx, |project, _| {
         project.set_language_server_settings(serde_json::json!({
             "json": {
                 "schemas": [
                     {
                         "fileMatch": "**/.zed/settings.json",
-                        "schema": Settings::file_json_schema(),
+                        "schema": Settings::file_json_schema(theme_names, language_names),
                     }
                 ]
             }
@@ -199,11 +241,58 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
     cx.platform().quit();
 }
 
+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"))
+    }
+}
+
 #[cfg(test)]
 mod tests {
-    use crate::assets::Assets;
-
     use super::*;
+    use assets::Assets;
     use editor::{DisplayPoint, Editor};
     use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
     use project::{Fs, ProjectPath};
@@ -567,7 +656,7 @@ mod tests {
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -580,7 +669,7 @@ mod tests {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "untitled");
             assert!(Arc::ptr_eq(
-                editor.language(cx).unwrap(),
+                editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
@@ -604,7 +693,7 @@ mod tests {
         editor.read_with(cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "the-new-name.rs");
-            assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust");
+            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
         });
 
         // Edit the file and save it again. This time, there is no filename prompt.
@@ -622,7 +711,7 @@ mod tests {
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
-        cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
         workspace
             .update(cx, |workspace, cx| {
                 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
@@ -659,7 +748,7 @@ mod tests {
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -670,7 +759,7 @@ mod tests {
 
         editor.update(cx, |editor, cx| {
             assert!(Arc::ptr_eq(
-                editor.language(cx).unwrap(),
+                editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
@@ -684,7 +773,7 @@ mod tests {
         // The buffer is not dirty anymore and the language is assigned based on the path.
         editor.read_with(cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
-            assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust")
+            assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
         });
     }
 
@@ -729,32 +818,47 @@ mod tests {
             .update(cx, |w, cx| w.open_path(file1.clone(), cx))
             .await
             .unwrap();
-        cx.read(|cx| {
-            assert_eq!(
-                pane_1.read(cx).active_item().unwrap().project_path(cx),
-                Some(file1.clone())
-            );
+
+        let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
+            let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+            assert_eq!(editor.project_path(cx), Some(file1.clone()));
+            let buffer = editor.update(cx, |editor, cx| {
+                editor.insert("dirt", cx);
+                editor.buffer().downgrade()
+            });
+            (editor.downgrade(), buffer)
         });
 
-        cx.dispatch_action(
-            window_id,
-            vec![pane_1.id()],
-            pane::Split(SplitDirection::Right),
-        );
-        cx.update(|cx| {
+        cx.dispatch_action(window_id, pane::Split(SplitDirection::Right));
+        let editor_2 = cx.update(|cx| {
             let pane_2 = workspace.read(cx).active_pane().clone();
             assert_ne!(pane_1, pane_2);
 
             let pane2_item = pane_2.read(cx).active_item().unwrap();
             assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
 
-            cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem);
+            pane2_item.downcast::<Editor>().unwrap().downgrade()
         });
+        cx.dispatch_action(window_id, workspace::CloseActiveItem);
+
         cx.foreground().run_until_parked();
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.panes().len(), 1);
             assert_eq!(workspace.active_pane(), &pane_1);
         });
+
+        cx.dispatch_action(window_id, workspace::CloseActiveItem);
+        cx.foreground().run_until_parked();
+        cx.simulate_prompt_answer(window_id, 1);
+        cx.foreground().run_until_parked();
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.active_item(cx).is_none());
+        });
+
+        cx.assert_dropped(editor_1);
+        cx.assert_dropped(editor_2);
+        cx.assert_dropped(buffer);
     }
 
     #[gpui::test]
@@ -882,11 +986,10 @@ mod tests {
             .update(cx, |workspace, cx| {
                 let editor3_id = editor3.id();
                 drop(editor3);
-                workspace
-                    .active_pane()
-                    .update(cx, |pane, cx| pane.close_item(editor3_id, cx))
+                Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
             })
-            .await;
+            .await
+            .unwrap();
         workspace
             .update(cx, |w, cx| Pane::go_forward(w, None, cx))
             .await;
@@ -900,11 +1003,10 @@ mod tests {
             .update(cx, |workspace, cx| {
                 let editor2_id = editor2.id();
                 drop(editor2);
-                workspace
-                    .active_pane()
-                    .update(cx, |pane, cx| pane.close_item(editor2_id, cx))
+                Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
             })
-            .await;
+            .await
+            .unwrap();
         app_state
             .fs
             .as_fake()
@@ -992,7 +1094,8 @@ mod tests {
         lazy_static::lazy_static! {
             static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
             static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
-                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into()
+                Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into(),
+                Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap().to_vec().into(),
             ];
         }
 

script/build-css 🔗

@@ -7,4 +7,4 @@ cd ./script
 if [[ $1 == --release ]]; then
     export NODE_ENV=production # Purge unused styles in --release mode
 fi
-npx tailwindcss build ../crates/server/styles.css --output ../crates/server/static/styles.css
+npx tailwindcss build ../crates/collab/styles.css --output ../crates/collab/static/styles.css

script/bundle 🔗

@@ -4,24 +4,33 @@ set -e
 
 export ZED_BUNDLE=true
 
-# Install cargo-bundle 0.5.0 if it's not already installed
+echo "Installing cargo bundle"
 cargo install cargo-bundle --version 0.5.0
 
 # Deal with versions of macOS that don't include libstdc++ headers
 export CXXFLAGS="-stdlib=libc++"
 
-# Build the app bundle for x86_64
-pushd crates/zed > /dev/null
-cargo bundle --release --target x86_64-apple-darwin
-popd > /dev/null
+echo "Compiling binaries"
+cargo build --release --package zed --target aarch64-apple-darwin
+cargo build --release --package zed --target x86_64-apple-darwin
+cargo build --release --package cli --target aarch64-apple-darwin
+cargo build --release --package cli --target x86_64-apple-darwin
 
-# Build the binary for aarch64 (Apple M1)
-cargo build --release --target aarch64-apple-darwin
+echo "Creating application bundle"
+(cd crates/zed && cargo bundle --release --target x86_64-apple-darwin)
 
-# Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries
-lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
+echo "Creating fat binaries"
+lipo \
+    -create \
+    target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/Zed \
+    -output \
+    target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
+lipo \
+    -create \
+    target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/cli \
+    -output \
+    target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/cli
 
-# Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now.
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Signing bundle with Apple-issued certificate"
     security create-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain || echo ""
@@ -39,7 +48,6 @@ else
     codesign --force --deep --sign - target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v
 fi
 
-# Create a DMG
 echo "Creating DMG"
 mkdir -p target/release
 hdiutil create -volname Zed -srcfolder target/x86_64-apple-darwin/release/bundle/osx -ov -format UDZO target/release/Zed.dmg

script/deploy 🔗

@@ -16,7 +16,7 @@ if [[ $# < 1 ]]; then
 fi
 
 export ZED_KUBE_NAMESPACE=$1
-ENV_FILE="crates/server/k8s/environments/${ZED_KUBE_NAMESPACE}.sh"
+ENV_FILE="crates/collab/k8s/environments/${ZED_KUBE_NAMESPACE}.sh"
 if [[ ! -f $ENV_FILE ]]; then
   echo "Invalid environment name '${ZED_KUBE_NAMESPACE}'"
   exit 1
@@ -28,10 +28,10 @@ if [[ $ZED_KUBE_NAMESPACE == "production" && -n $(git status --short) ]]; then
 fi
 
 git_sha=$(git rev-parse HEAD)
-export ZED_IMAGE_ID="registry.digitalocean.com/zed/zed-server:${ZED_KUBE_NAMESPACE}-${git_sha}"
+export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${ZED_KUBE_NAMESPACE}-${git_sha}"
 export $(cat $ENV_FILE)
 
 docker build . --tag "$ZED_IMAGE_ID"
 docker push "$ZED_IMAGE_ID"
 
-envsubst < crates/server/k8s/manifest.template.yml | kubectl apply -f -
+envsubst < crates/collab/k8s/manifest.template.yml | kubectl apply -f -

script/seed-db 🔗

@@ -1,9 +1,9 @@
 #!/bin/bash
 set -e
 
-cd crates/server
+cd crates/collab
 
 # Export contents of .env.toml
 eval "$(cargo run --bin dotenv)"
 
-cargo run --package=zed-server --features seed-support --bin seed
+cargo run --package=collab --features seed-support --bin seed

script/sqlx 🔗

@@ -5,7 +5,7 @@ set -e
 # Install sqlx-cli if needed
 [[ "$(sqlx --version)" == "sqlx-cli 0.5.7" ]] || cargo install sqlx-cli --version 0.5.7
 
-cd crates/server
+cd crates/collab
 
 # Export contents of .env.toml
 eval "$(cargo run --bin dotenv)"

script/tailwind.config.js 🔗

@@ -40,7 +40,7 @@ module.exports = {
     },
     darkMode: false,
     purge: [
-        "../crates/server/templates/**/*.hbs",
-        "../crates/server/templates/*.hbs"
+        "../crates/collab/templates/**/*.hbs",
+        "../crates/collab/templates/*.hbs"
     ]
 }

styles/dist/core.json 🔗

@@ -0,0 +1,1155 @@
+{
+  "color": {
+    "neutral": {
+      "0": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "25": {
+        "value": "#f8f8f8",
+        "step": 25,
+        "type": "color"
+      },
+      "50": {
+        "value": "#f1f1f1",
+        "step": 50,
+        "type": "color"
+      },
+      "75": {
+        "value": "#eaeaea",
+        "step": 75,
+        "type": "color"
+      },
+      "100": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      },
+      "125": {
+        "value": "#dcdcdc",
+        "step": 125,
+        "type": "color"
+      },
+      "150": {
+        "value": "#d5d5d5",
+        "step": 150,
+        "type": "color"
+      },
+      "175": {
+        "value": "#cdcdcd",
+        "step": 175,
+        "type": "color"
+      },
+      "200": {
+        "value": "#c6c6c6",
+        "step": 200,
+        "type": "color"
+      },
+      "225": {
+        "value": "#bfbfbf",
+        "step": 225,
+        "type": "color"
+      },
+      "250": {
+        "value": "#b8b8b8",
+        "step": 250,
+        "type": "color"
+      },
+      "275": {
+        "value": "#b1b1b1",
+        "step": 275,
+        "type": "color"
+      },
+      "300": {
+        "value": "#aaaaaa",
+        "step": 300,
+        "type": "color"
+      },
+      "325": {
+        "value": "#a3a3a3",
+        "step": 325,
+        "type": "color"
+      },
+      "350": {
+        "value": "#9c9c9c",
+        "step": 350,
+        "type": "color"
+      },
+      "375": {
+        "value": "#959595",
+        "step": 375,
+        "type": "color"
+      },
+      "400": {
+        "value": "#8e8e8e",
+        "step": 400,
+        "type": "color"
+      },
+      "425": {
+        "value": "#878787",
+        "step": 425,
+        "type": "color"
+      },
+      "450": {
+        "value": "#808080",
+        "step": 450,
+        "type": "color"
+      },
+      "475": {
+        "value": "#787878",
+        "step": 475,
+        "type": "color"
+      },
+      "500": {
+        "value": "#717171",
+        "step": 500,
+        "type": "color"
+      },
+      "525": {
+        "value": "#6a6a6a",
+        "step": 525,
+        "type": "color"
+      },
+      "550": {
+        "value": "#636363",
+        "step": 550,
+        "type": "color"
+      },
+      "575": {
+        "value": "#5c5c5c",
+        "step": 575,
+        "type": "color"
+      },
+      "600": {
+        "value": "#555555",
+        "step": 600,
+        "type": "color"
+      },
+      "625": {
+        "value": "#4e4e4e",
+        "step": 625,
+        "type": "color"
+      },
+      "650": {
+        "value": "#474747",
+        "step": 650,
+        "type": "color"
+      },
+      "675": {
+        "value": "#404040",
+        "step": 675,
+        "type": "color"
+      },
+      "700": {
+        "value": "#393939",
+        "step": 700,
+        "type": "color"
+      },
+      "725": {
+        "value": "#323232",
+        "step": 725,
+        "type": "color"
+      },
+      "750": {
+        "value": "#2b2b2b",
+        "step": 750,
+        "type": "color"
+      },
+      "775": {
+        "value": "#232323",
+        "step": 775,
+        "type": "color"
+      },
+      "800": {
+        "value": "#1c1c1c",
+        "step": 800,
+        "type": "color"
+      },
+      "825": {
+        "value": "#151515",
+        "step": 825,
+        "type": "color"
+      },
+      "850": {
+        "value": "#0e0e0e",
+        "step": 850,
+        "type": "color"
+      },
+      "875": {
+        "value": "#070707",
+        "step": 875,
+        "type": "color"
+      },
+      "900": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "rose": {
+      "0": {
+        "value": "#feecef",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#fcc5cf",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#fa9fae",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#f8788e",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#f5526e",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#f0284a",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#cd1434",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#97142a",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#64101e",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#330a11",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "red": {
+      "0": {
+        "value": "#feecec",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#fcc6c6",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#f9a0a0",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#f57b7b",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#eb2d2d",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#c91818",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#951515",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#631111",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#330a0a",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "orange": {
+      "0": {
+        "value": "#fef3ec",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#fcd6bd",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#fab98e",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#f99d5f",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#f9812e",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#ee670a",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#bb550e",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#8b4210",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#5d2f0e",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#331b0a",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "amber": {
+      "0": {
+        "value": "#fef7ec",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#fce2ba",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#f9ce89",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#f7bb57",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#f6a724",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#de900c",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#b0740f",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#845910",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#5a3e0e",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#33240a",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "yellow": {
+      "0": {
+        "value": "#fef9ec",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#fce9b7",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#f9da82",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#f8cc4d",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#f7bf17",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#d3a20b",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#a8820e",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#7e630f",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#58460e",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#33290a",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "lime": {
+      "0": {
+        "value": "#f7feec",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#dffab5",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#c7f57f",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#aeef4b",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#96e818",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#79ba16",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#639714",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#4e7412",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#38530f",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#23330a",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "green": {
+      "0": {
+        "value": "#ecfef2",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#b7f9ce",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#84f2ab",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#54e989",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#27dd69",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#20b456",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#157338",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#105328",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a3319",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "emerald": {
+      "0": {
+        "value": "#ecfef8",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#b0fae1",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#74f6cb",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#39f0b3",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#12d796",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#10a977",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#118a62",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#106c4e",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#0d4f3a",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a3326",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "teal": {
+      "0": {
+        "value": "#ecfefc",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#b1faf2",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#76f5e7",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#3eeeda",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#16d6c1",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#14a898",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#138a7d",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#116c62",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#0e4f48",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a332f",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "cyan": {
+      "0": {
+        "value": "#ecfcfe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#b2f3fb",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#78eaf9",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#3de2f8",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#07d5f1",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#09aac0",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#0c8a9a",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#0e6a75",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#0d4c53",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a2f33",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "sky": {
+      "0": {
+        "value": "#ecf8fe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#b9e5fb",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#86d3f8",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#53c1f5",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#20b0f2",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#1096d3",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#1179a8",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#115c7f",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#0e4158",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a2633",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "blue": {
+      "0": {
+        "value": "#ecf3fe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#c5dafc",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#9ec1fa",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#76a8f8",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#4f8ff7",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#135acd",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#134697",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#103063",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a1a33",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "indigo": {
+      "0": {
+        "value": "#ececfe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#cdcdfc",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#aeaff9",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#9091f6",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#7274f3",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#484bed",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#1b1edc",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#1819a1",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#121269",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#0a0a33",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "violet": {
+      "0": {
+        "value": "#f1ecfe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#daccfc",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#c3acfb",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#ac8cf9",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#966cf7",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#7741f2",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#5316e0",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#3f15a3",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#2b116a",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#160a33",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "purple": {
+      "0": {
+        "value": "#f5ecfe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#e4cbfc",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#d2a9fb",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#c188f9",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#b066f8",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#993bf3",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#7b14dd",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#5c14a1",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#3e1169",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#1f0a33",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "fuschia": {
+      "0": {
+        "value": "#fdecfe",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#f8c5fb",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#f19ff6",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#e87af0",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#de57e8",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#d430e0",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#b31fbc",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#87198e",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#5c1260",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#310a33",
+        "step": 900,
+        "type": "color"
+      }
+    },
+    "pink": {
+      "0": {
+        "value": "#feecf5",
+        "step": 0,
+        "type": "color"
+      },
+      "100": {
+        "value": "#fbc6e1",
+        "step": 100,
+        "type": "color"
+      },
+      "200": {
+        "value": "#f8a1cc",
+        "step": 200,
+        "type": "color"
+      },
+      "300": {
+        "value": "#f47db8",
+        "step": 300,
+        "type": "color"
+      },
+      "400": {
+        "value": "#ef59a3",
+        "step": 400,
+        "type": "color"
+      },
+      "500": {
+        "value": "#e8318c",
+        "step": 500,
+        "type": "color"
+      },
+      "600": {
+        "value": "#c71a71",
+        "step": 600,
+        "type": "color"
+      },
+      "700": {
+        "value": "#941756",
+        "step": 700,
+        "type": "color"
+      },
+      "800": {
+        "value": "#63113b",
+        "step": 800,
+        "type": "color"
+      },
+      "900": {
+        "value": "#330a1f",
+        "step": 900,
+        "type": "color"
+      }
+    }
+  },
+  "text": {
+    "family": {
+      "sans": {
+        "value": "Zed Sans",
+        "type": "fontFamily"
+      },
+      "mono": {
+        "value": "Zed Mono",
+        "type": "fontFamily"
+      }
+    },
+    "weight": {
+      "thin": {
+        "value": "thin",
+        "type": "fontWeight"
+      },
+      "extra_light": {
+        "value": "extra_light",
+        "type": "fontWeight"
+      },
+      "light": {
+        "value": "light",
+        "type": "fontWeight"
+      },
+      "normal": {
+        "value": "normal",
+        "type": "fontWeight"
+      },
+      "medium": {
+        "value": "medium",
+        "type": "fontWeight"
+      },
+      "semibold": {
+        "value": "semibold",
+        "type": "fontWeight"
+      },
+      "bold": {
+        "value": "bold",
+        "type": "fontWeight"
+      },
+      "extra_bold": {
+        "value": "extra_bold",
+        "type": "fontWeight"
+      },
+      "black": {
+        "value": "black",
+        "type": "fontWeight"
+      }
+    }
+  },
+  "size": {
+    "3xs": {
+      "value": 8,
+      "type": "fontSize"
+    },
+    "2xs": {
+      "value": 10,
+      "type": "fontSize"
+    },
+    "xs": {
+      "value": 12,
+      "type": "fontSize"
+    },
+    "sm": {
+      "value": 14,
+      "type": "fontSize"
+    },
+    "md": {
+      "value": 16,
+      "type": "fontSize"
+    },
+    "lg": {
+      "value": 18,
+      "type": "fontSize"
+    },
+    "xl": {
+      "value": 20,
+      "type": "fontSize"
+    }
+  }
+}

styles/dist/dark.json 🔗

@@ -0,0 +1,681 @@
+{
+  "meta": {
+    "themeName": "dark"
+  },
+  "text": {
+    "primary": {
+      "value": "#f1f1f1",
+      "step": 50,
+      "type": "color"
+    },
+    "secondary": {
+      "value": "#9c9c9c",
+      "step": 350,
+      "type": "color"
+    },
+    "muted": {
+      "value": "#808080",
+      "step": 450,
+      "type": "color"
+    },
+    "placeholder": {
+      "value": "#474747",
+      "step": 650,
+      "type": "color"
+    },
+    "active": {
+      "value": "#ffffff",
+      "step": 0,
+      "type": "color"
+    },
+    "feature": {
+      "value": "#4f8ff7",
+      "step": 400,
+      "type": "color"
+    },
+    "ok": {
+      "value": "#1b9447",
+      "step": 600,
+      "type": "color"
+    },
+    "error": {
+      "value": "#f15656",
+      "step": 400,
+      "type": "color"
+    },
+    "warning": {
+      "value": "#f7bb57",
+      "step": 300,
+      "type": "color"
+    },
+    "info": {
+      "value": "#2472f2",
+      "step": 500,
+      "type": "color"
+    }
+  },
+  "icon": {
+    "primary": {
+      "value": "#c6c6c6",
+      "step": 200,
+      "type": "color"
+    },
+    "secondary": {
+      "value": "#9c9c9c",
+      "step": 350,
+      "type": "color"
+    },
+    "muted": {
+      "value": "#555555",
+      "step": 600,
+      "type": "color"
+    },
+    "placeholder": {
+      "value": "#393939",
+      "step": 700,
+      "type": "color"
+    },
+    "active": {
+      "value": "#ffffff",
+      "step": 0,
+      "type": "color"
+    },
+    "feature": {
+      "value": "#2472f2",
+      "step": 500,
+      "type": "color"
+    },
+    "ok": {
+      "value": "#1b9447",
+      "step": 600,
+      "type": "color"
+    },
+    "error": {
+      "value": "#eb2d2d",
+      "step": 500,
+      "type": "color"
+    },
+    "warning": {
+      "value": "#f6a724",
+      "step": 400,
+      "type": "color"
+    },
+    "info": {
+      "value": "#135acd",
+      "step": 600,
+      "type": "color"
+    }
+  },
+  "background": {
+    "100": {
+      "base": {
+        "value": "#2b2b2b",
+        "step": 750,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#323232",
+        "step": 725,
+        "type": "color"
+      },
+      "active": {
+        "value": "#1c1c1c",
+        "step": 800,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#404040",
+        "step": 675,
+        "type": "color"
+      }
+    },
+    "300": {
+      "base": {
+        "value": "#1c1c1c",
+        "step": 800,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#232323",
+        "step": 775,
+        "type": "color"
+      },
+      "active": {
+        "value": "#2b2b2b",
+        "step": 750,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#232323",
+        "step": 775,
+        "type": "color"
+      }
+    },
+    "500": {
+      "base": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#ffffff14",
+        "step": 0,
+        "type": "color"
+      },
+      "active": {
+        "value": "#ffffff1f",
+        "step": 0,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#151515",
+        "step": 825,
+        "type": "color"
+      }
+    },
+    "on300": {
+      "base": {
+        "value": "#0e0e0e80",
+        "step": 850,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#070707",
+        "step": 875,
+        "type": "color"
+      },
+      "active": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#070707",
+        "step": 875,
+        "type": "color"
+      }
+    },
+    "on500": {
+      "base": {
+        "value": "#0e0e0e",
+        "step": 850,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#1c1c1c",
+        "step": 800,
+        "type": "color"
+      },
+      "active": {
+        "value": "#232323",
+        "step": 775,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#1c1c1c",
+        "step": 800,
+        "type": "color"
+      }
+    },
+    "ok": {
+      "base": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "active": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      }
+    },
+    "error": {
+      "base": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      },
+      "active": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "warning": {
+      "base": {
+        "value": "#f7bb57",
+        "step": 300,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#f7bb57",
+        "step": 300,
+        "type": "color"
+      },
+      "active": {
+        "value": "#f7bb57",
+        "step": 300,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#f7bb57",
+        "step": 300,
+        "type": "color"
+      }
+    },
+    "info": {
+      "base": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "active": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      }
+    }
+  },
+  "border": {
+    "primary": {
+      "value": "#070707",
+      "step": 875,
+      "type": "color"
+    },
+    "secondary": {
+      "value": "#232323",
+      "step": 775,
+      "type": "color"
+    },
+    "muted": {
+      "value": "#404040",
+      "step": 675,
+      "type": "color"
+    },
+    "focused": {
+      "value": "#484bed",
+      "step": 500,
+      "type": "color"
+    },
+    "active": {
+      "value": "#000000",
+      "step": 900,
+      "type": "color"
+    },
+    "ok": {
+      "value": "#20b456",
+      "step": 500,
+      "type": "color"
+    },
+    "error": {
+      "value": "#eb2d2d",
+      "step": 500,
+      "type": "color"
+    },
+    "warning": {
+      "value": "#de900c",
+      "step": 500,
+      "type": "color"
+    },
+    "info": {
+      "value": "#2472f2",
+      "step": 500,
+      "type": "color"
+    }
+  },
+  "editor": {
+    "background": {
+      "value": "#000000",
+      "step": 900,
+      "type": "color"
+    },
+    "indent_guide": {
+      "value": "#404040",
+      "step": 675,
+      "type": "color"
+    },
+    "indent_guide_active": {
+      "value": "#232323",
+      "step": 775,
+      "type": "color"
+    },
+    "line": {
+      "active": {
+        "value": "#ffffff12",
+        "step": 0,
+        "type": "color"
+      },
+      "highlighted": {
+        "value": "#ffffff1f",
+        "step": 0,
+        "type": "color"
+      },
+      "inserted": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "deleted": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      },
+      "modified": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "highlight": {
+      "selection": {
+        "value": "#2472f23d",
+        "step": 500,
+        "type": "color"
+      },
+      "occurrence": {
+        "value": "#ffffff1f",
+        "step": 0,
+        "type": "color"
+      },
+      "activeOccurrence": {
+        "value": "#ffffff29",
+        "step": 0,
+        "type": "color"
+      },
+      "matchingBracket": {
+        "value": "#ffffff1f",
+        "step": 0,
+        "type": "color"
+      },
+      "match": {
+        "value": "#3f15a380",
+        "step": 700,
+        "type": "color"
+      },
+      "activeMatch": {
+        "value": "#5316e0b3",
+        "step": 600,
+        "type": "color"
+      },
+      "related": {
+        "value": "#151515",
+        "step": 825,
+        "type": "color"
+      }
+    },
+    "gutter": {
+      "primary": {
+        "value": "#474747",
+        "step": 650,
+        "type": "color"
+      },
+      "active": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      }
+    }
+  },
+  "syntax": {
+    "primary": {
+      "value": "#d5d5d5",
+      "type": "color"
+    },
+    "comment": {
+      "value": "#aaaaaa",
+      "type": "color"
+    },
+    "keyword": {
+      "value": "#4f8ff7",
+      "type": "color"
+    },
+    "function": {
+      "value": "#f9da82",
+      "type": "color"
+    },
+    "type": {
+      "value": "#3eeeda",
+      "type": "color"
+    },
+    "variant": {
+      "value": "#53c1f5",
+      "type": "color"
+    },
+    "property": {
+      "value": "#4f8ff7",
+      "type": "color"
+    },
+    "enum": {
+      "value": "#ee670a",
+      "type": "color"
+    },
+    "operator": {
+      "value": "#ee670a",
+      "type": "color"
+    },
+    "string": {
+      "value": "#f99d5f",
+      "type": "color"
+    },
+    "number": {
+      "value": "#aeef4b",
+      "type": "color"
+    },
+    "boolean": {
+      "value": "#aeef4b",
+      "type": "color"
+    }
+  },
+  "player": {
+    "1": {
+      "baseColor": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#2472f23d",
+        "step": 500,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#2472f2cc",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "2": {
+      "baseColor": {
+        "value": "#79ba16",
+        "step": 500,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#79ba16",
+        "step": 500,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#79ba163d",
+        "step": 500,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#79ba16cc",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "3": {
+      "baseColor": {
+        "value": "#d430e0",
+        "step": 500,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#d430e0",
+        "step": 500,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#d430e03d",
+        "step": 500,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#d430e0cc",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "4": {
+      "baseColor": {
+        "value": "#ee670a",
+        "step": 500,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#ee670a",
+        "step": 500,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#ee670a3d",
+        "step": 500,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#ee670acc",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "5": {
+      "baseColor": {
+        "value": "#993bf3",
+        "step": 500,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#993bf3",
+        "step": 500,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#993bf33d",
+        "step": 500,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#993bf3cc",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "6": {
+      "baseColor": {
+        "value": "#16d6c1",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#16d6c1",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#16d6c13d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#16d6c1cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "7": {
+      "baseColor": {
+        "value": "#ef59a3",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#ef59a3",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#ef59a33d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#ef59a3cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "8": {
+      "baseColor": {
+        "value": "#f7bf17",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#f7bf17",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#f7bf173d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#f7bf17cc",
+        "step": 400,
+        "type": "color"
+      }
+    }
+  },
+  "shadowAlpha": {
+    "value": 0.32,
+    "type": "number"
+  }
+}

styles/dist/light.json 🔗

@@ -0,0 +1,681 @@
+{
+  "meta": {
+    "themeName": "light"
+  },
+  "text": {
+    "primary": {
+      "value": "#2b2b2b",
+      "step": 750,
+      "type": "color"
+    },
+    "secondary": {
+      "value": "#474747",
+      "step": 650,
+      "type": "color"
+    },
+    "muted": {
+      "value": "#636363",
+      "step": 550,
+      "type": "color"
+    },
+    "placeholder": {
+      "value": "#808080",
+      "step": 450,
+      "type": "color"
+    },
+    "active": {
+      "value": "#000000",
+      "step": 900,
+      "type": "color"
+    },
+    "feature": {
+      "value": "#484bed",
+      "step": 500,
+      "type": "color"
+    },
+    "ok": {
+      "value": "#20b456",
+      "step": 500,
+      "type": "color"
+    },
+    "error": {
+      "value": "#eb2d2d",
+      "step": 500,
+      "type": "color"
+    },
+    "warning": {
+      "value": "#d3a20b",
+      "step": 500,
+      "type": "color"
+    },
+    "info": {
+      "value": "#2472f2",
+      "step": 500,
+      "type": "color"
+    }
+  },
+  "icon": {
+    "primary": {
+      "value": "#393939",
+      "step": 700,
+      "type": "color"
+    },
+    "secondary": {
+      "value": "#717171",
+      "step": 500,
+      "type": "color"
+    },
+    "muted": {
+      "value": "#9c9c9c",
+      "step": 350,
+      "type": "color"
+    },
+    "placeholder": {
+      "value": "#aaaaaa",
+      "step": 300,
+      "type": "color"
+    },
+    "active": {
+      "value": "#000000",
+      "step": 900,
+      "type": "color"
+    },
+    "feature": {
+      "value": "#484bed",
+      "step": 500,
+      "type": "color"
+    },
+    "ok": {
+      "value": "#1b9447",
+      "step": 600,
+      "type": "color"
+    },
+    "error": {
+      "value": "#c91818",
+      "step": 600,
+      "type": "color"
+    },
+    "warning": {
+      "value": "#f7bf17",
+      "step": 400,
+      "type": "color"
+    },
+    "info": {
+      "value": "#135acd",
+      "step": 600,
+      "type": "color"
+    }
+  },
+  "background": {
+    "100": {
+      "base": {
+        "value": "#eaeaea",
+        "step": 75,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      },
+      "active": {
+        "value": "#d5d5d5",
+        "step": 150,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      }
+    },
+    "300": {
+      "base": {
+        "value": "#f8f8f8",
+        "step": 25,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#eaeaea",
+        "step": 75,
+        "type": "color"
+      },
+      "active": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#eaeaea",
+        "step": 75,
+        "type": "color"
+      }
+    },
+    "500": {
+      "base": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#00000008",
+        "step": 900,
+        "type": "color"
+      },
+      "active": {
+        "value": "#0000000f",
+        "step": 900,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#f1f1f1",
+        "step": 50,
+        "type": "color"
+      }
+    },
+    "on300": {
+      "base": {
+        "value": "#f1f1f1",
+        "step": 50,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      },
+      "active": {
+        "value": "#d5d5d5",
+        "step": 150,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      }
+    },
+    "on500": {
+      "base": {
+        "value": "#f1f1f1",
+        "step": 50,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#f8f8f8",
+        "step": 25,
+        "type": "color"
+      },
+      "active": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#f8f8f8",
+        "step": 25,
+        "type": "color"
+      }
+    },
+    "ok": {
+      "base": {
+        "value": "#b7f9ce",
+        "step": 100,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#b7f9ce",
+        "step": 100,
+        "type": "color"
+      },
+      "active": {
+        "value": "#b7f9ce",
+        "step": 100,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#b7f9ce",
+        "step": 100,
+        "type": "color"
+      }
+    },
+    "error": {
+      "base": {
+        "value": "#fcc6c6",
+        "step": 100,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#fcc6c6",
+        "step": 100,
+        "type": "color"
+      },
+      "active": {
+        "value": "#fcc6c6",
+        "step": 100,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#fcc6c6",
+        "step": 100,
+        "type": "color"
+      }
+    },
+    "warning": {
+      "base": {
+        "value": "#fce9b7",
+        "step": 100,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#fce9b7",
+        "step": 100,
+        "type": "color"
+      },
+      "active": {
+        "value": "#fce9b7",
+        "step": 100,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#fce9b7",
+        "step": 100,
+        "type": "color"
+      }
+    },
+    "info": {
+      "base": {
+        "value": "#c5dafc",
+        "step": 100,
+        "type": "color"
+      },
+      "hovered": {
+        "value": "#c5dafc",
+        "step": 100,
+        "type": "color"
+      },
+      "active": {
+        "value": "#c5dafc",
+        "step": 100,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#c5dafc",
+        "step": 100,
+        "type": "color"
+      }
+    }
+  },
+  "border": {
+    "primary": {
+      "value": "#d5d5d5",
+      "step": 150,
+      "type": "color"
+    },
+    "secondary": {
+      "value": "#d5d5d5",
+      "step": 150,
+      "type": "color"
+    },
+    "muted": {
+      "value": "#e3e3e3",
+      "step": 100,
+      "type": "color"
+    },
+    "focused": {
+      "value": "#484bed",
+      "step": 500,
+      "type": "color"
+    },
+    "active": {
+      "value": "#b8b8b8",
+      "step": 250,
+      "type": "color"
+    },
+    "ok": {
+      "value": "#84f2ab",
+      "step": 200,
+      "type": "color"
+    },
+    "error": {
+      "value": "#f9a0a0",
+      "step": 200,
+      "type": "color"
+    },
+    "warning": {
+      "value": "#f9da82",
+      "step": 200,
+      "type": "color"
+    },
+    "info": {
+      "value": "#9ec1fa",
+      "step": 200,
+      "type": "color"
+    }
+  },
+  "editor": {
+    "background": {
+      "value": "#ffffff",
+      "step": 0,
+      "type": "color"
+    },
+    "indent_guide": {
+      "value": "#e3e3e3",
+      "step": 100,
+      "type": "color"
+    },
+    "indent_guide_active": {
+      "value": "#d5d5d5",
+      "step": 150,
+      "type": "color"
+    },
+    "line": {
+      "active": {
+        "value": "#0000000f",
+        "step": 900,
+        "type": "color"
+      },
+      "highlighted": {
+        "value": "#0000001f",
+        "step": 900,
+        "type": "color"
+      },
+      "inserted": {
+        "value": "#b7f9ce",
+        "step": 100,
+        "type": "color"
+      },
+      "deleted": {
+        "value": "#fcc6c6",
+        "step": 100,
+        "type": "color"
+      },
+      "modified": {
+        "value": "#c5dafc",
+        "step": 100,
+        "type": "color"
+      }
+    },
+    "highlight": {
+      "selection": {
+        "value": "#2472f23d",
+        "step": 500,
+        "type": "color"
+      },
+      "occurrence": {
+        "value": "#0000000f",
+        "step": 900,
+        "type": "color"
+      },
+      "activeOccurrence": {
+        "value": "#00000029",
+        "step": 900,
+        "type": "color"
+      },
+      "matchingBracket": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "match": {
+        "value": "#fce9b7",
+        "step": 100,
+        "type": "color"
+      },
+      "activeMatch": {
+        "value": "#f9da82",
+        "step": 200,
+        "type": "color"
+      },
+      "related": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      }
+    },
+    "gutter": {
+      "primary": {
+        "value": "#aaaaaa",
+        "step": 300,
+        "type": "color"
+      },
+      "active": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      }
+    }
+  },
+  "syntax": {
+    "primary": {
+      "value": "#1c1c1c",
+      "type": "color"
+    },
+    "comment": {
+      "value": "#717171",
+      "type": "color"
+    },
+    "keyword": {
+      "value": "#1819a1",
+      "type": "color"
+    },
+    "function": {
+      "value": "#bb550e",
+      "type": "color"
+    },
+    "type": {
+      "value": "#a8820e",
+      "type": "color"
+    },
+    "variant": {
+      "value": "#97142a",
+      "type": "color"
+    },
+    "property": {
+      "value": "#106c4e",
+      "type": "color"
+    },
+    "enum": {
+      "value": "#eb2d2d",
+      "type": "color"
+    },
+    "operator": {
+      "value": "#eb2d2d",
+      "type": "color"
+    },
+    "string": {
+      "value": "#eb2d2d",
+      "type": "color"
+    },
+    "number": {
+      "value": "#484bed",
+      "type": "color"
+    },
+    "boolean": {
+      "value": "#eb2d2d",
+      "type": "color"
+    }
+  },
+  "player": {
+    "1": {
+      "baseColor": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#2472f23d",
+        "step": 500,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#2472f2cc",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "2": {
+      "baseColor": {
+        "value": "#12d796",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#12d796",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#12d7963d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#12d796cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "3": {
+      "baseColor": {
+        "value": "#de57e8",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#de57e8",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#de57e83d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#de57e8cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "4": {
+      "baseColor": {
+        "value": "#f9812e",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#f9812e",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#f9812e3d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#f9812ecc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "5": {
+      "baseColor": {
+        "value": "#b066f8",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#b066f8",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#b066f83d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#b066f8cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "6": {
+      "baseColor": {
+        "value": "#16d6c1",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#16d6c1",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#16d6c13d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#16d6c1cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "7": {
+      "baseColor": {
+        "value": "#ef59a3",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#ef59a3",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#ef59a33d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#ef59a3cc",
+        "step": 400,
+        "type": "color"
+      }
+    },
+    "8": {
+      "baseColor": {
+        "value": "#f7bf17",
+        "step": 400,
+        "type": "color"
+      },
+      "cursorColor": {
+        "value": "#f7bf17",
+        "step": 400,
+        "type": "color"
+      },
+      "selectionColor": {
+        "value": "#f7bf173d",
+        "step": 400,
+        "type": "color"
+      },
+      "borderColor": {
+        "value": "#f7bf17cc",
+        "step": 400,
+        "type": "color"
+      }
+    }
+  },
+  "shadowAlpha": {
+    "value": 0.12,
+    "type": "number"
+  }
+}

styles/dist/tokens.json 🔗

@@ -0,0 +1,2519 @@
+{
+  "core": {
+    "color": {
+      "neutral": {
+        "0": {
+          "value": "#ffffff",
+          "step": 0,
+          "type": "color"
+        },
+        "25": {
+          "value": "#f8f8f8",
+          "step": 25,
+          "type": "color"
+        },
+        "50": {
+          "value": "#f1f1f1",
+          "step": 50,
+          "type": "color"
+        },
+        "75": {
+          "value": "#eaeaea",
+          "step": 75,
+          "type": "color"
+        },
+        "100": {
+          "value": "#e3e3e3",
+          "step": 100,
+          "type": "color"
+        },
+        "125": {
+          "value": "#dcdcdc",
+          "step": 125,
+          "type": "color"
+        },
+        "150": {
+          "value": "#d5d5d5",
+          "step": 150,
+          "type": "color"
+        },
+        "175": {
+          "value": "#cdcdcd",
+          "step": 175,
+          "type": "color"
+        },
+        "200": {
+          "value": "#c6c6c6",
+          "step": 200,
+          "type": "color"
+        },
+        "225": {
+          "value": "#bfbfbf",
+          "step": 225,
+          "type": "color"
+        },
+        "250": {
+          "value": "#b8b8b8",
+          "step": 250,
+          "type": "color"
+        },
+        "275": {
+          "value": "#b1b1b1",
+          "step": 275,
+          "type": "color"
+        },
+        "300": {
+          "value": "#aaaaaa",
+          "step": 300,
+          "type": "color"
+        },
+        "325": {
+          "value": "#a3a3a3",
+          "step": 325,
+          "type": "color"
+        },
+        "350": {
+          "value": "#9c9c9c",
+          "step": 350,
+          "type": "color"
+        },
+        "375": {
+          "value": "#959595",
+          "step": 375,
+          "type": "color"
+        },
+        "400": {
+          "value": "#8e8e8e",
+          "step": 400,
+          "type": "color"
+        },
+        "425": {
+          "value": "#878787",
+          "step": 425,
+          "type": "color"
+        },
+        "450": {
+          "value": "#808080",
+          "step": 450,
+          "type": "color"
+        },
+        "475": {
+          "value": "#787878",
+          "step": 475,
+          "type": "color"
+        },
+        "500": {
+          "value": "#717171",
+          "step": 500,
+          "type": "color"
+        },
+        "525": {
+          "value": "#6a6a6a",
+          "step": 525,
+          "type": "color"
+        },
+        "550": {
+          "value": "#636363",
+          "step": 550,
+          "type": "color"
+        },
+        "575": {
+          "value": "#5c5c5c",
+          "step": 575,
+          "type": "color"
+        },
+        "600": {
+          "value": "#555555",
+          "step": 600,
+          "type": "color"
+        },
+        "625": {
+          "value": "#4e4e4e",
+          "step": 625,
+          "type": "color"
+        },
+        "650": {
+          "value": "#474747",
+          "step": 650,
+          "type": "color"
+        },
+        "675": {
+          "value": "#404040",
+          "step": 675,
+          "type": "color"
+        },
+        "700": {
+          "value": "#393939",
+          "step": 700,
+          "type": "color"
+        },
+        "725": {
+          "value": "#323232",
+          "step": 725,
+          "type": "color"
+        },
+        "750": {
+          "value": "#2b2b2b",
+          "step": 750,
+          "type": "color"
+        },
+        "775": {
+          "value": "#232323",
+          "step": 775,
+          "type": "color"
+        },
+        "800": {
+          "value": "#1c1c1c",
+          "step": 800,
+          "type": "color"
+        },
+        "825": {
+          "value": "#151515",
+          "step": 825,
+          "type": "color"
+        },
+        "850": {
+          "value": "#0e0e0e",
+          "step": 850,
+          "type": "color"
+        },
+        "875": {
+          "value": "#070707",
+          "step": 875,
+          "type": "color"
+        },
+        "900": {
+          "value": "#000000",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "rose": {
+        "0": {
+          "value": "#feecef",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#fcc5cf",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#fa9fae",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#f8788e",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#f5526e",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#f0284a",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#cd1434",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#97142a",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#64101e",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#330a11",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "red": {
+        "0": {
+          "value": "#feecec",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#fcc6c6",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#f9a0a0",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#f57b7b",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#f15656",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#eb2d2d",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#c91818",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#951515",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#631111",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#330a0a",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "orange": {
+        "0": {
+          "value": "#fef3ec",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#fcd6bd",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#fab98e",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#f99d5f",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#f9812e",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#ee670a",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#bb550e",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#8b4210",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#5d2f0e",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#331b0a",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "amber": {
+        "0": {
+          "value": "#fef7ec",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#fce2ba",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#f9ce89",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#f7bb57",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#f6a724",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#de900c",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#b0740f",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#845910",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#5a3e0e",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#33240a",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "yellow": {
+        "0": {
+          "value": "#fef9ec",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#fce9b7",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#f9da82",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#f8cc4d",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#f7bf17",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#d3a20b",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#a8820e",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#7e630f",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#58460e",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#33290a",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "lime": {
+        "0": {
+          "value": "#f7feec",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#dffab5",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#c7f57f",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#aeef4b",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#96e818",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#79ba16",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#639714",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#4e7412",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#38530f",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#23330a",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "green": {
+        "0": {
+          "value": "#ecfef2",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#b7f9ce",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#84f2ab",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#54e989",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#27dd69",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#20b456",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#1b9447",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#157338",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#105328",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a3319",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "emerald": {
+        "0": {
+          "value": "#ecfef8",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#b0fae1",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#74f6cb",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#39f0b3",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#12d796",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#10a977",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#118a62",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#106c4e",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#0d4f3a",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a3326",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "teal": {
+        "0": {
+          "value": "#ecfefc",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#b1faf2",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#76f5e7",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#3eeeda",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#16d6c1",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#14a898",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#138a7d",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#116c62",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#0e4f48",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a332f",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "cyan": {
+        "0": {
+          "value": "#ecfcfe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#b2f3fb",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#78eaf9",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#3de2f8",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#07d5f1",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#09aac0",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#0c8a9a",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#0e6a75",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#0d4c53",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a2f33",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "sky": {
+        "0": {
+          "value": "#ecf8fe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#b9e5fb",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#86d3f8",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#53c1f5",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#20b0f2",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#1096d3",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#1179a8",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#115c7f",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#0e4158",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a2633",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "blue": {
+        "0": {
+          "value": "#ecf3fe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#c5dafc",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#9ec1fa",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#76a8f8",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#4f8ff7",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#135acd",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#134697",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#103063",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a1a33",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "indigo": {
+        "0": {
+          "value": "#ececfe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#cdcdfc",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#aeaff9",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#9091f6",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#7274f3",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#484bed",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#1b1edc",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#1819a1",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#121269",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#0a0a33",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "violet": {
+        "0": {
+          "value": "#f1ecfe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#daccfc",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#c3acfb",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#ac8cf9",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#966cf7",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#7741f2",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#5316e0",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#3f15a3",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#2b116a",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#160a33",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "purple": {
+        "0": {
+          "value": "#f5ecfe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#e4cbfc",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#d2a9fb",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#c188f9",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#b066f8",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#993bf3",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#7b14dd",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#5c14a1",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#3e1169",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#1f0a33",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "fuschia": {
+        "0": {
+          "value": "#fdecfe",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#f8c5fb",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#f19ff6",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#e87af0",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#de57e8",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#d430e0",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#b31fbc",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#87198e",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#5c1260",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#310a33",
+          "step": 900,
+          "type": "color"
+        }
+      },
+      "pink": {
+        "0": {
+          "value": "#feecf5",
+          "step": 0,
+          "type": "color"
+        },
+        "100": {
+          "value": "#fbc6e1",
+          "step": 100,
+          "type": "color"
+        },
+        "200": {
+          "value": "#f8a1cc",
+          "step": 200,
+          "type": "color"
+        },
+        "300": {
+          "value": "#f47db8",
+          "step": 300,
+          "type": "color"
+        },
+        "400": {
+          "value": "#ef59a3",
+          "step": 400,
+          "type": "color"
+        },
+        "500": {
+          "value": "#e8318c",
+          "step": 500,
+          "type": "color"
+        },
+        "600": {
+          "value": "#c71a71",
+          "step": 600,
+          "type": "color"
+        },
+        "700": {
+          "value": "#941756",
+          "step": 700,
+          "type": "color"
+        },
+        "800": {
+          "value": "#63113b",
+          "step": 800,
+          "type": "color"
+        },
+        "900": {
+          "value": "#330a1f",
+          "step": 900,
+          "type": "color"
+        }
+      }
+    },
+    "text": {
+      "family": {
+        "sans": {
+          "value": "Zed Sans",
+          "type": "fontFamily"
+        },
+        "mono": {
+          "value": "Zed Mono",
+          "type": "fontFamily"
+        }
+      },
+      "weight": {
+        "thin": {
+          "value": "thin",
+          "type": "fontWeight"
+        },
+        "extra_light": {
+          "value": "extra_light",
+          "type": "fontWeight"
+        },
+        "light": {
+          "value": "light",
+          "type": "fontWeight"
+        },
+        "normal": {
+          "value": "normal",
+          "type": "fontWeight"
+        },
+        "medium": {
+          "value": "medium",
+          "type": "fontWeight"
+        },
+        "semibold": {
+          "value": "semibold",
+          "type": "fontWeight"
+        },
+        "bold": {
+          "value": "bold",
+          "type": "fontWeight"
+        },
+        "extra_bold": {
+          "value": "extra_bold",
+          "type": "fontWeight"
+        },
+        "black": {
+          "value": "black",
+          "type": "fontWeight"
+        }
+      }
+    },
+    "size": {
+      "3xs": {
+        "value": 8,
+        "type": "fontSize"
+      },
+      "2xs": {
+        "value": 10,
+        "type": "fontSize"
+      },
+      "xs": {
+        "value": 12,
+        "type": "fontSize"
+      },
+      "sm": {
+        "value": 14,
+        "type": "fontSize"
+      },
+      "md": {
+        "value": 16,
+        "type": "fontSize"
+      },
+      "lg": {
+        "value": 18,
+        "type": "fontSize"
+      },
+      "xl": {
+        "value": 20,
+        "type": "fontSize"
+      }
+    }
+  },
+  "dark": {
+    "meta": {
+      "themeName": "dark"
+    },
+    "text": {
+      "primary": {
+        "value": "#f1f1f1",
+        "step": 50,
+        "type": "color"
+      },
+      "secondary": {
+        "value": "#9c9c9c",
+        "step": 350,
+        "type": "color"
+      },
+      "muted": {
+        "value": "#808080",
+        "step": 450,
+        "type": "color"
+      },
+      "placeholder": {
+        "value": "#474747",
+        "step": 650,
+        "type": "color"
+      },
+      "active": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "feature": {
+        "value": "#4f8ff7",
+        "step": 400,
+        "type": "color"
+      },
+      "ok": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "error": {
+        "value": "#f15656",
+        "step": 400,
+        "type": "color"
+      },
+      "warning": {
+        "value": "#f7bb57",
+        "step": 300,
+        "type": "color"
+      },
+      "info": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "icon": {
+      "primary": {
+        "value": "#c6c6c6",
+        "step": 200,
+        "type": "color"
+      },
+      "secondary": {
+        "value": "#9c9c9c",
+        "step": 350,
+        "type": "color"
+      },
+      "muted": {
+        "value": "#555555",
+        "step": 600,
+        "type": "color"
+      },
+      "placeholder": {
+        "value": "#393939",
+        "step": 700,
+        "type": "color"
+      },
+      "active": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "feature": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      },
+      "ok": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "error": {
+        "value": "#eb2d2d",
+        "step": 500,
+        "type": "color"
+      },
+      "warning": {
+        "value": "#f6a724",
+        "step": 400,
+        "type": "color"
+      },
+      "info": {
+        "value": "#135acd",
+        "step": 600,
+        "type": "color"
+      }
+    },
+    "background": {
+      "100": {
+        "base": {
+          "value": "#2b2b2b",
+          "step": 750,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#323232",
+          "step": 725,
+          "type": "color"
+        },
+        "active": {
+          "value": "#1c1c1c",
+          "step": 800,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#404040",
+          "step": 675,
+          "type": "color"
+        }
+      },
+      "300": {
+        "base": {
+          "value": "#1c1c1c",
+          "step": 800,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#232323",
+          "step": 775,
+          "type": "color"
+        },
+        "active": {
+          "value": "#2b2b2b",
+          "step": 750,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#232323",
+          "step": 775,
+          "type": "color"
+        }
+      },
+      "500": {
+        "base": {
+          "value": "#000000",
+          "step": 900,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#ffffff14",
+          "step": 0,
+          "type": "color"
+        },
+        "active": {
+          "value": "#ffffff1f",
+          "step": 0,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#151515",
+          "step": 825,
+          "type": "color"
+        }
+      },
+      "on300": {
+        "base": {
+          "value": "#0e0e0e80",
+          "step": 850,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#070707",
+          "step": 875,
+          "type": "color"
+        },
+        "active": {
+          "value": "#000000",
+          "step": 900,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#070707",
+          "step": 875,
+          "type": "color"
+        }
+      },
+      "on500": {
+        "base": {
+          "value": "#0e0e0e",
+          "step": 850,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#1c1c1c",
+          "step": 800,
+          "type": "color"
+        },
+        "active": {
+          "value": "#232323",
+          "step": 775,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#1c1c1c",
+          "step": 800,
+          "type": "color"
+        }
+      },
+      "ok": {
+        "base": {
+          "value": "#1b9447",
+          "step": 600,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#1b9447",
+          "step": 600,
+          "type": "color"
+        },
+        "active": {
+          "value": "#1b9447",
+          "step": 600,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#1b9447",
+          "step": 600,
+          "type": "color"
+        }
+      },
+      "error": {
+        "base": {
+          "value": "#f15656",
+          "step": 400,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#f15656",
+          "step": 400,
+          "type": "color"
+        },
+        "active": {
+          "value": "#f15656",
+          "step": 400,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#f15656",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "warning": {
+        "base": {
+          "value": "#f7bb57",
+          "step": 300,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#f7bb57",
+          "step": 300,
+          "type": "color"
+        },
+        "active": {
+          "value": "#f7bb57",
+          "step": 300,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#f7bb57",
+          "step": 300,
+          "type": "color"
+        }
+      },
+      "info": {
+        "base": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "active": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        }
+      }
+    },
+    "border": {
+      "primary": {
+        "value": "#070707",
+        "step": 875,
+        "type": "color"
+      },
+      "secondary": {
+        "value": "#232323",
+        "step": 775,
+        "type": "color"
+      },
+      "muted": {
+        "value": "#404040",
+        "step": 675,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#484bed",
+        "step": 500,
+        "type": "color"
+      },
+      "active": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      },
+      "ok": {
+        "value": "#20b456",
+        "step": 500,
+        "type": "color"
+      },
+      "error": {
+        "value": "#eb2d2d",
+        "step": 500,
+        "type": "color"
+      },
+      "warning": {
+        "value": "#de900c",
+        "step": 500,
+        "type": "color"
+      },
+      "info": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "editor": {
+      "background": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      },
+      "indent_guide": {
+        "value": "#404040",
+        "step": 675,
+        "type": "color"
+      },
+      "indent_guide_active": {
+        "value": "#232323",
+        "step": 775,
+        "type": "color"
+      },
+      "line": {
+        "active": {
+          "value": "#ffffff12",
+          "step": 0,
+          "type": "color"
+        },
+        "highlighted": {
+          "value": "#ffffff1f",
+          "step": 0,
+          "type": "color"
+        },
+        "inserted": {
+          "value": "#1b9447",
+          "step": 600,
+          "type": "color"
+        },
+        "deleted": {
+          "value": "#f15656",
+          "step": 400,
+          "type": "color"
+        },
+        "modified": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "highlight": {
+        "selection": {
+          "value": "#2472f23d",
+          "step": 500,
+          "type": "color"
+        },
+        "occurrence": {
+          "value": "#ffffff1f",
+          "step": 0,
+          "type": "color"
+        },
+        "activeOccurrence": {
+          "value": "#ffffff29",
+          "step": 0,
+          "type": "color"
+        },
+        "matchingBracket": {
+          "value": "#ffffff1f",
+          "step": 0,
+          "type": "color"
+        },
+        "match": {
+          "value": "#3f15a380",
+          "step": 700,
+          "type": "color"
+        },
+        "activeMatch": {
+          "value": "#5316e0b3",
+          "step": 600,
+          "type": "color"
+        },
+        "related": {
+          "value": "#151515",
+          "step": 825,
+          "type": "color"
+        }
+      },
+      "gutter": {
+        "primary": {
+          "value": "#474747",
+          "step": 650,
+          "type": "color"
+        },
+        "active": {
+          "value": "#ffffff",
+          "step": 0,
+          "type": "color"
+        }
+      }
+    },
+    "syntax": {
+      "primary": {
+        "value": "#d5d5d5",
+        "type": "color"
+      },
+      "comment": {
+        "value": "#aaaaaa",
+        "type": "color"
+      },
+      "keyword": {
+        "value": "#4f8ff7",
+        "type": "color"
+      },
+      "function": {
+        "value": "#f9da82",
+        "type": "color"
+      },
+      "type": {
+        "value": "#3eeeda",
+        "type": "color"
+      },
+      "variant": {
+        "value": "#53c1f5",
+        "type": "color"
+      },
+      "property": {
+        "value": "#4f8ff7",
+        "type": "color"
+      },
+      "enum": {
+        "value": "#ee670a",
+        "type": "color"
+      },
+      "operator": {
+        "value": "#ee670a",
+        "type": "color"
+      },
+      "string": {
+        "value": "#f99d5f",
+        "type": "color"
+      },
+      "number": {
+        "value": "#aeef4b",
+        "type": "color"
+      },
+      "boolean": {
+        "value": "#aeef4b",
+        "type": "color"
+      }
+    },
+    "player": {
+      "1": {
+        "baseColor": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#2472f23d",
+          "step": 500,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#2472f2cc",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "2": {
+        "baseColor": {
+          "value": "#79ba16",
+          "step": 500,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#79ba16",
+          "step": 500,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#79ba163d",
+          "step": 500,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#79ba16cc",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "3": {
+        "baseColor": {
+          "value": "#d430e0",
+          "step": 500,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#d430e0",
+          "step": 500,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#d430e03d",
+          "step": 500,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#d430e0cc",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "4": {
+        "baseColor": {
+          "value": "#ee670a",
+          "step": 500,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#ee670a",
+          "step": 500,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#ee670a3d",
+          "step": 500,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#ee670acc",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "5": {
+        "baseColor": {
+          "value": "#993bf3",
+          "step": 500,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#993bf3",
+          "step": 500,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#993bf33d",
+          "step": 500,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#993bf3cc",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "6": {
+        "baseColor": {
+          "value": "#16d6c1",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#16d6c1",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#16d6c13d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#16d6c1cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "7": {
+        "baseColor": {
+          "value": "#ef59a3",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#ef59a3",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#ef59a33d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#ef59a3cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "8": {
+        "baseColor": {
+          "value": "#f7bf17",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#f7bf17",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#f7bf173d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#f7bf17cc",
+          "step": 400,
+          "type": "color"
+        }
+      }
+    },
+    "shadowAlpha": {
+      "value": 0.32,
+      "type": "number"
+    }
+  },
+  "light": {
+    "meta": {
+      "themeName": "light"
+    },
+    "text": {
+      "primary": {
+        "value": "#2b2b2b",
+        "step": 750,
+        "type": "color"
+      },
+      "secondary": {
+        "value": "#474747",
+        "step": 650,
+        "type": "color"
+      },
+      "muted": {
+        "value": "#636363",
+        "step": 550,
+        "type": "color"
+      },
+      "placeholder": {
+        "value": "#808080",
+        "step": 450,
+        "type": "color"
+      },
+      "active": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      },
+      "feature": {
+        "value": "#484bed",
+        "step": 500,
+        "type": "color"
+      },
+      "ok": {
+        "value": "#20b456",
+        "step": 500,
+        "type": "color"
+      },
+      "error": {
+        "value": "#eb2d2d",
+        "step": 500,
+        "type": "color"
+      },
+      "warning": {
+        "value": "#d3a20b",
+        "step": 500,
+        "type": "color"
+      },
+      "info": {
+        "value": "#2472f2",
+        "step": 500,
+        "type": "color"
+      }
+    },
+    "icon": {
+      "primary": {
+        "value": "#393939",
+        "step": 700,
+        "type": "color"
+      },
+      "secondary": {
+        "value": "#717171",
+        "step": 500,
+        "type": "color"
+      },
+      "muted": {
+        "value": "#9c9c9c",
+        "step": 350,
+        "type": "color"
+      },
+      "placeholder": {
+        "value": "#aaaaaa",
+        "step": 300,
+        "type": "color"
+      },
+      "active": {
+        "value": "#000000",
+        "step": 900,
+        "type": "color"
+      },
+      "feature": {
+        "value": "#484bed",
+        "step": 500,
+        "type": "color"
+      },
+      "ok": {
+        "value": "#1b9447",
+        "step": 600,
+        "type": "color"
+      },
+      "error": {
+        "value": "#c91818",
+        "step": 600,
+        "type": "color"
+      },
+      "warning": {
+        "value": "#f7bf17",
+        "step": 400,
+        "type": "color"
+      },
+      "info": {
+        "value": "#135acd",
+        "step": 600,
+        "type": "color"
+      }
+    },
+    "background": {
+      "100": {
+        "base": {
+          "value": "#eaeaea",
+          "step": 75,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#e3e3e3",
+          "step": 100,
+          "type": "color"
+        },
+        "active": {
+          "value": "#d5d5d5",
+          "step": 150,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#e3e3e3",
+          "step": 100,
+          "type": "color"
+        }
+      },
+      "300": {
+        "base": {
+          "value": "#f8f8f8",
+          "step": 25,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#eaeaea",
+          "step": 75,
+          "type": "color"
+        },
+        "active": {
+          "value": "#e3e3e3",
+          "step": 100,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#eaeaea",
+          "step": 75,
+          "type": "color"
+        }
+      },
+      "500": {
+        "base": {
+          "value": "#ffffff",
+          "step": 0,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#00000008",
+          "step": 900,
+          "type": "color"
+        },
+        "active": {
+          "value": "#0000000f",
+          "step": 900,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#f1f1f1",
+          "step": 50,
+          "type": "color"
+        }
+      },
+      "on300": {
+        "base": {
+          "value": "#f1f1f1",
+          "step": 50,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#e3e3e3",
+          "step": 100,
+          "type": "color"
+        },
+        "active": {
+          "value": "#d5d5d5",
+          "step": 150,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#e3e3e3",
+          "step": 100,
+          "type": "color"
+        }
+      },
+      "on500": {
+        "base": {
+          "value": "#f1f1f1",
+          "step": 50,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#f8f8f8",
+          "step": 25,
+          "type": "color"
+        },
+        "active": {
+          "value": "#ffffff",
+          "step": 0,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#f8f8f8",
+          "step": 25,
+          "type": "color"
+        }
+      },
+      "ok": {
+        "base": {
+          "value": "#b7f9ce",
+          "step": 100,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#b7f9ce",
+          "step": 100,
+          "type": "color"
+        },
+        "active": {
+          "value": "#b7f9ce",
+          "step": 100,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#b7f9ce",
+          "step": 100,
+          "type": "color"
+        }
+      },
+      "error": {
+        "base": {
+          "value": "#fcc6c6",
+          "step": 100,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#fcc6c6",
+          "step": 100,
+          "type": "color"
+        },
+        "active": {
+          "value": "#fcc6c6",
+          "step": 100,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#fcc6c6",
+          "step": 100,
+          "type": "color"
+        }
+      },
+      "warning": {
+        "base": {
+          "value": "#fce9b7",
+          "step": 100,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#fce9b7",
+          "step": 100,
+          "type": "color"
+        },
+        "active": {
+          "value": "#fce9b7",
+          "step": 100,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#fce9b7",
+          "step": 100,
+          "type": "color"
+        }
+      },
+      "info": {
+        "base": {
+          "value": "#c5dafc",
+          "step": 100,
+          "type": "color"
+        },
+        "hovered": {
+          "value": "#c5dafc",
+          "step": 100,
+          "type": "color"
+        },
+        "active": {
+          "value": "#c5dafc",
+          "step": 100,
+          "type": "color"
+        },
+        "focused": {
+          "value": "#c5dafc",
+          "step": 100,
+          "type": "color"
+        }
+      }
+    },
+    "border": {
+      "primary": {
+        "value": "#d5d5d5",
+        "step": 150,
+        "type": "color"
+      },
+      "secondary": {
+        "value": "#d5d5d5",
+        "step": 150,
+        "type": "color"
+      },
+      "muted": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      },
+      "focused": {
+        "value": "#484bed",
+        "step": 500,
+        "type": "color"
+      },
+      "active": {
+        "value": "#b8b8b8",
+        "step": 250,
+        "type": "color"
+      },
+      "ok": {
+        "value": "#84f2ab",
+        "step": 200,
+        "type": "color"
+      },
+      "error": {
+        "value": "#f9a0a0",
+        "step": 200,
+        "type": "color"
+      },
+      "warning": {
+        "value": "#f9da82",
+        "step": 200,
+        "type": "color"
+      },
+      "info": {
+        "value": "#9ec1fa",
+        "step": 200,
+        "type": "color"
+      }
+    },
+    "editor": {
+      "background": {
+        "value": "#ffffff",
+        "step": 0,
+        "type": "color"
+      },
+      "indent_guide": {
+        "value": "#e3e3e3",
+        "step": 100,
+        "type": "color"
+      },
+      "indent_guide_active": {
+        "value": "#d5d5d5",
+        "step": 150,
+        "type": "color"
+      },
+      "line": {
+        "active": {
+          "value": "#0000000f",
+          "step": 900,
+          "type": "color"
+        },
+        "highlighted": {
+          "value": "#0000001f",
+          "step": 900,
+          "type": "color"
+        },
+        "inserted": {
+          "value": "#b7f9ce",
+          "step": 100,
+          "type": "color"
+        },
+        "deleted": {
+          "value": "#fcc6c6",
+          "step": 100,
+          "type": "color"
+        },
+        "modified": {
+          "value": "#c5dafc",
+          "step": 100,
+          "type": "color"
+        }
+      },
+      "highlight": {
+        "selection": {
+          "value": "#2472f23d",
+          "step": 500,
+          "type": "color"
+        },
+        "occurrence": {
+          "value": "#0000000f",
+          "step": 900,
+          "type": "color"
+        },
+        "activeOccurrence": {
+          "value": "#00000029",
+          "step": 900,
+          "type": "color"
+        },
+        "matchingBracket": {
+          "value": "#ffffff",
+          "step": 0,
+          "type": "color"
+        },
+        "match": {
+          "value": "#fce9b7",
+          "step": 100,
+          "type": "color"
+        },
+        "activeMatch": {
+          "value": "#f9da82",
+          "step": 200,
+          "type": "color"
+        },
+        "related": {
+          "value": "#ffffff",
+          "step": 0,
+          "type": "color"
+        }
+      },
+      "gutter": {
+        "primary": {
+          "value": "#aaaaaa",
+          "step": 300,
+          "type": "color"
+        },
+        "active": {
+          "value": "#000000",
+          "step": 900,
+          "type": "color"
+        }
+      }
+    },
+    "syntax": {
+      "primary": {
+        "value": "#1c1c1c",
+        "type": "color"
+      },
+      "comment": {
+        "value": "#717171",
+        "type": "color"
+      },
+      "keyword": {
+        "value": "#1819a1",
+        "type": "color"
+      },
+      "function": {
+        "value": "#bb550e",
+        "type": "color"
+      },
+      "type": {
+        "value": "#a8820e",
+        "type": "color"
+      },
+      "variant": {
+        "value": "#97142a",
+        "type": "color"
+      },
+      "property": {
+        "value": "#106c4e",
+        "type": "color"
+      },
+      "enum": {
+        "value": "#eb2d2d",
+        "type": "color"
+      },
+      "operator": {
+        "value": "#eb2d2d",
+        "type": "color"
+      },
+      "string": {
+        "value": "#eb2d2d",
+        "type": "color"
+      },
+      "number": {
+        "value": "#484bed",
+        "type": "color"
+      },
+      "boolean": {
+        "value": "#eb2d2d",
+        "type": "color"
+      }
+    },
+    "player": {
+      "1": {
+        "baseColor": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#2472f2",
+          "step": 500,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#2472f23d",
+          "step": 500,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#2472f2cc",
+          "step": 500,
+          "type": "color"
+        }
+      },
+      "2": {
+        "baseColor": {
+          "value": "#12d796",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#12d796",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#12d7963d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#12d796cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "3": {
+        "baseColor": {
+          "value": "#de57e8",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#de57e8",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#de57e83d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#de57e8cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "4": {
+        "baseColor": {
+          "value": "#f9812e",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#f9812e",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#f9812e3d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#f9812ecc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "5": {
+        "baseColor": {
+          "value": "#b066f8",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#b066f8",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#b066f83d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#b066f8cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "6": {
+        "baseColor": {
+          "value": "#16d6c1",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#16d6c1",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#16d6c13d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#16d6c1cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "7": {
+        "baseColor": {
+          "value": "#ef59a3",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#ef59a3",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#ef59a33d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#ef59a3cc",
+          "step": 400,
+          "type": "color"
+        }
+      },
+      "8": {
+        "baseColor": {
+          "value": "#f7bf17",
+          "step": 400,
+          "type": "color"
+        },
+        "cursorColor": {
+          "value": "#f7bf17",
+          "step": 400,
+          "type": "color"
+        },
+        "selectionColor": {
+          "value": "#f7bf173d",
+          "step": 400,
+          "type": "color"
+        },
+        "borderColor": {
+          "value": "#f7bf17cc",
+          "step": 400,
+          "type": "color"
+        }
+      }
+    },
+    "shadowAlpha": {
+      "value": 0.12,
+      "type": "number"
+    }
+  }
+}

styles/nodemon.json 🔗

@@ -0,0 +1,8 @@
+{
+    "watch": [
+        "./**/*"
+    ],
+    "ext": "ts",
+    "ignore": [],
+    "exec": "ts-node src/buildThemes.ts"
+}

styles/package-lock.json 🔗

@@ -0,0 +1,2321 @@
+{
+    "name": "styles",
+    "version": "1.0.0",
+    "lockfileVersion": 2,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "styles",
+            "version": "1.0.0",
+            "license": "ISC",
+            "dependencies": {
+                "@types/chroma-js": "^2.1.3",
+                "@types/node": "^17.0.23",
+                "case-anything": "^2.1.10",
+                "chroma-js": "^2.4.2",
+                "nodemon": "^2.0.15",
+                "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"
+            }
+        },
+        "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==",
+            "dependencies": {
+                "@cspotcode/source-map-consumer": "0.8.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@sindresorhus/is": {
+            "version": "0.14.0",
+            "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+            "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/@szmarczak/http-timer": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+            "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+            "dependencies": {
+                "defer-to-connect": "^1.0.1"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/@tsconfig/node10": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
+            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+        },
+        "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=="
+        },
+        "node_modules/@tsconfig/node14": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
+            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+        },
+        "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=="
+        },
+        "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=="
+        },
+        "node_modules/@types/node": {
+            "version": "17.0.23",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
+            "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+        },
+        "node_modules/abbrev": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+        },
+        "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==",
+            "bin": {
+                "acorn": "bin/acorn"
+            },
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/ansi-align": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+            "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+            "dependencies": {
+                "string-width": "^4.1.0"
+            }
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/anymatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+            "dependencies": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
+        },
+        "node_modules/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+        },
+        "node_modules/binary-extensions": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/boxen": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
+            "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
+            "dependencies": {
+                "ansi-align": "^3.0.0",
+                "camelcase": "^6.2.0",
+                "chalk": "^4.1.0",
+                "cli-boxes": "^2.2.1",
+                "string-width": "^4.2.2",
+                "type-fest": "^0.20.2",
+                "widest-line": "^3.1.0",
+                "wrap-ansi": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "node_modules/braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dependencies": {
+                "fill-range": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cacheable-request": {
+            "version": "6.1.0",
+            "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+            "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+            "dependencies": {
+                "clone-response": "^1.0.2",
+                "get-stream": "^5.1.0",
+                "http-cache-semantics": "^4.0.0",
+                "keyv": "^3.0.0",
+                "lowercase-keys": "^2.0.0",
+                "normalize-url": "^4.1.0",
+                "responselike": "^1.0.2"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/cacheable-request/node_modules/get-stream": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+            "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+            "dependencies": {
+                "pump": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/cacheable-request/node_modules/lowercase-keys": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+            "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/camelcase": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+            "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/case-anything": {
+            "version": "2.1.10",
+            "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
+            "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==",
+            "engines": {
+                "node": ">=12.13"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/mesqueeb"
+            }
+        },
+        "node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
+        },
+        "node_modules/chalk/node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/chalk/node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/chokidar": {
+            "version": "3.5.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+            "funding": [
+                {
+                    "type": "individual",
+                    "url": "https://paulmillr.com/funding/"
+                }
+            ],
+            "dependencies": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "engines": {
+                "node": ">= 8.10.0"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
+        "node_modules/chroma-js": {
+            "version": "2.4.2",
+            "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+            "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+        },
+        "node_modules/ci-info": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+            "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
+        },
+        "node_modules/cli-boxes": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
+            "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
+            "engines": {
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/clone-response": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+            "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+            "dependencies": {
+                "mimic-response": "^1.0.0"
+            }
+        },
+        "node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+        },
+        "node_modules/configstore": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+            "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+            "dependencies": {
+                "dot-prop": "^5.2.0",
+                "graceful-fs": "^4.1.2",
+                "make-dir": "^3.0.0",
+                "unique-string": "^2.0.0",
+                "write-file-atomic": "^3.0.0",
+                "xdg-basedir": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/create-require": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+        },
+        "node_modules/crypto-random-string": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+            "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/decompress-response": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+            "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+            "dependencies": {
+                "mimic-response": "^1.0.0"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/deep-extend": {
+            "version": "0.6.0",
+            "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+            "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+            "engines": {
+                "node": ">=4.0.0"
+            }
+        },
+        "node_modules/defer-to-connect": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+            "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
+        },
+        "node_modules/diff": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+            "engines": {
+                "node": ">=0.3.1"
+            }
+        },
+        "node_modules/dot-prop": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+            "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+            "dependencies": {
+                "is-obj": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/duplexer3": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+            "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+        },
+        "node_modules/emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+        },
+        "node_modules/end-of-stream": {
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+            "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+            "dependencies": {
+                "once": "^1.4.0"
+            }
+        },
+        "node_modules/escape-goat": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+            "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/fsevents": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+            "hasInstallScript": true,
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+            }
+        },
+        "node_modules/get-stream": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+            "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+            "dependencies": {
+                "pump": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/global-dirs": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+            "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+            "dependencies": {
+                "ini": "2.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/got": {
+            "version": "9.6.0",
+            "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+            "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+            "dependencies": {
+                "@sindresorhus/is": "^0.14.0",
+                "@szmarczak/http-timer": "^1.1.2",
+                "cacheable-request": "^6.0.0",
+                "decompress-response": "^3.3.0",
+                "duplexer3": "^0.1.4",
+                "get-stream": "^4.1.0",
+                "lowercase-keys": "^1.0.1",
+                "mimic-response": "^1.0.1",
+                "p-cancelable": "^1.0.0",
+                "to-readable-stream": "^1.0.0",
+                "url-parse-lax": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8.6"
+            }
+        },
+        "node_modules/graceful-fs": {
+            "version": "4.2.9",
+            "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+            "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
+        },
+        "node_modules/has-flag": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+            "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/has-yarn": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+            "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/http-cache-semantics": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+            "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
+        },
+        "node_modules/ignore-by-default": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+            "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
+        },
+        "node_modules/import-lazy": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+            "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/imurmurhash": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+            "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+            "engines": {
+                "node": ">=0.8.19"
+            }
+        },
+        "node_modules/ini": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+            "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "dependencies": {
+                "binary-extensions": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-ci": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+            "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+            "dependencies": {
+                "ci-info": "^2.0.0"
+            },
+            "bin": {
+                "is-ci": "bin.js"
+            }
+        },
+        "node_modules/is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dependencies": {
+                "is-extglob": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-installed-globally": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+            "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+            "dependencies": {
+                "global-dirs": "^3.0.0",
+                "is-path-inside": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/is-npm": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
+            "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
+        "node_modules/is-obj": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+            "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-path-inside": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-typedarray": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+            "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+        },
+        "node_modules/is-yarn-global": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+            "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
+        },
+        "node_modules/json-buffer": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+            "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
+        },
+        "node_modules/keyv": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+            "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+            "dependencies": {
+                "json-buffer": "3.0.0"
+            }
+        },
+        "node_modules/latest-version": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+            "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+            "dependencies": {
+                "package-json": "^6.3.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/lowercase-keys": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+            "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/make-dir": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "dependencies": {
+                "semver": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/make-dir/node_modules/semver": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+        },
+        "node_modules/mimic-response": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+            "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minimist": {
+            "version": "1.2.6",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+            "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+        },
+        "node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        },
+        "node_modules/nodemon": {
+            "version": "2.0.15",
+            "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz",
+            "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "chokidar": "^3.5.2",
+                "debug": "^3.2.7",
+                "ignore-by-default": "^1.0.1",
+                "minimatch": "^3.0.4",
+                "pstree.remy": "^1.1.8",
+                "semver": "^5.7.1",
+                "supports-color": "^5.5.0",
+                "touch": "^3.1.0",
+                "undefsafe": "^2.0.5",
+                "update-notifier": "^5.1.0"
+            },
+            "bin": {
+                "nodemon": "bin/nodemon.js"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/nodemon"
+            }
+        },
+        "node_modules/nopt": {
+            "version": "1.0.10",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+            "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+            "dependencies": {
+                "abbrev": "1"
+            },
+            "bin": {
+                "nopt": "bin/nopt.js"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/normalize-url": {
+            "version": "4.5.1",
+            "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
+            "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/p-cancelable": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+            "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/package-json": {
+            "version": "6.5.0",
+            "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+            "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+            "dependencies": {
+                "got": "^9.6.0",
+                "registry-auth-token": "^4.0.0",
+                "registry-url": "^5.0.0",
+                "semver": "^6.2.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/package-json/node_modules/semver": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/prepend-http": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+            "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/pstree.remy": {
+            "version": "1.1.8",
+            "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+            "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+        },
+        "node_modules/pump": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+            "dependencies": {
+                "end-of-stream": "^1.1.0",
+                "once": "^1.3.1"
+            }
+        },
+        "node_modules/pupa": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
+            "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
+            "dependencies": {
+                "escape-goat": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/rc": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+            "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+            "dependencies": {
+                "deep-extend": "^0.6.0",
+                "ini": "~1.3.0",
+                "minimist": "^1.2.0",
+                "strip-json-comments": "~2.0.1"
+            },
+            "bin": {
+                "rc": "cli.js"
+            }
+        },
+        "node_modules/rc/node_modules/ini": {
+            "version": "1.3.8",
+            "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+            "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+        },
+        "node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dependencies": {
+                "picomatch": "^2.2.1"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            }
+        },
+        "node_modules/registry-auth-token": {
+            "version": "4.2.1",
+            "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
+            "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
+            "dependencies": {
+                "rc": "^1.2.8"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/registry-url": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+            "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
+            "dependencies": {
+                "rc": "^1.2.8"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/responselike": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+            "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+            "dependencies": {
+                "lowercase-keys": "^1.0.0"
+            }
+        },
+        "node_modules/semver": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+            "bin": {
+                "semver": "bin/semver"
+            }
+        },
+        "node_modules/semver-diff": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+            "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+            "dependencies": {
+                "semver": "^6.3.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/semver-diff/node_modules/semver": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+        },
+        "node_modules/string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-json-comments": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+            "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/supports-color": {
+            "version": "5.5.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+            "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+            "dependencies": {
+                "has-flag": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/to-readable-stream": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+            "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
+        "node_modules/touch": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+            "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+            "dependencies": {
+                "nopt": "~1.0.10"
+            },
+            "bin": {
+                "nodetouch": "bin/nodetouch.js"
+            }
+        },
+        "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==",
+            "dependencies": {
+                "@cspotcode/source-map-support": "0.7.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.0",
+                "yn": "3.1.1"
+            },
+            "bin": {
+                "ts-node": "dist/bin.js",
+                "ts-node-cwd": "dist/bin-cwd.js",
+                "ts-node-esm": "dist/bin-esm.js",
+                "ts-node-script": "dist/bin-script.js",
+                "ts-node-transpile-only": "dist/bin-transpile.js",
+                "ts-script": "dist/bin-script-deprecated.js"
+            },
+            "peerDependencies": {
+                "@swc/core": ">=1.2.50",
+                "@swc/wasm": ">=1.2.50",
+                "@types/node": "*",
+                "typescript": ">=2.7"
+            },
+            "peerDependenciesMeta": {
+                "@swc/core": {
+                    "optional": true
+                },
+                "@swc/wasm": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/typedarray-to-buffer": {
+            "version": "3.1.5",
+            "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+            "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+            "dependencies": {
+                "is-typedarray": "^1.0.0"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "4.6.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "peer": true,
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=4.2.0"
+            }
+        },
+        "node_modules/undefsafe": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+            "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+        },
+        "node_modules/unique-string": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+            "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+            "dependencies": {
+                "crypto-random-string": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/update-notifier": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
+            "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
+            "dependencies": {
+                "boxen": "^5.0.0",
+                "chalk": "^4.1.0",
+                "configstore": "^5.0.1",
+                "has-yarn": "^2.1.0",
+                "import-lazy": "^2.1.0",
+                "is-ci": "^2.0.0",
+                "is-installed-globally": "^0.4.0",
+                "is-npm": "^5.0.0",
+                "is-yarn-global": "^0.3.0",
+                "latest-version": "^5.1.0",
+                "pupa": "^2.1.1",
+                "semver": "^7.3.4",
+                "semver-diff": "^3.1.1",
+                "xdg-basedir": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/yeoman/update-notifier?sponsor=1"
+            }
+        },
+        "node_modules/update-notifier/node_modules/semver": {
+            "version": "7.3.5",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+            "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/url-parse-lax": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+            "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+            "dependencies": {
+                "prepend-http": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "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=="
+        },
+        "node_modules/widest-line": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+            "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+            "dependencies": {
+                "string-width": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+        },
+        "node_modules/write-file-atomic": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+            "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+            "dependencies": {
+                "imurmurhash": "^0.1.4",
+                "is-typedarray": "^1.0.0",
+                "signal-exit": "^3.0.2",
+                "typedarray-to-buffer": "^3.1.5"
+            }
+        },
+        "node_modules/xdg-basedir": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+            "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "node_modules/yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+            "engines": {
+                "node": ">=6"
+            }
+        }
+    },
+    "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==",
+            "requires": {
+                "@cspotcode/source-map-consumer": "0.8.0"
+            }
+        },
+        "@sindresorhus/is": {
+            "version": "0.14.0",
+            "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+            "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
+        },
+        "@szmarczak/http-timer": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+            "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+            "requires": {
+                "defer-to-connect": "^1.0.1"
+            }
+        },
+        "@tsconfig/node10": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
+            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+        },
+        "@tsconfig/node12": {
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
+            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+        },
+        "@tsconfig/node14": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
+            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+        },
+        "@tsconfig/node16": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
+            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+        },
+        "@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=="
+        },
+        "@types/node": {
+            "version": "17.0.23",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
+            "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+        },
+        "abbrev": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+        },
+        "acorn": {
+            "version": "8.7.0",
+            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
+            "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
+        },
+        "acorn-walk": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
+        },
+        "ansi-align": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+            "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+            "requires": {
+                "string-width": "^4.1.0"
+            }
+        },
+        "ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+        },
+        "ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "requires": {
+                "color-convert": "^2.0.1"
+            }
+        },
+        "anymatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+            "requires": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            }
+        },
+        "arg": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
+        },
+        "balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+        },
+        "binary-extensions": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
+        },
+        "boxen": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
+            "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
+            "requires": {
+                "ansi-align": "^3.0.0",
+                "camelcase": "^6.2.0",
+                "chalk": "^4.1.0",
+                "cli-boxes": "^2.2.1",
+                "string-width": "^4.2.2",
+                "type-fest": "^0.20.2",
+                "widest-line": "^3.1.0",
+                "wrap-ansi": "^7.0.0"
+            }
+        },
+        "brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "requires": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "requires": {
+                "fill-range": "^7.0.1"
+            }
+        },
+        "cacheable-request": {
+            "version": "6.1.0",
+            "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+            "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+            "requires": {
+                "clone-response": "^1.0.2",
+                "get-stream": "^5.1.0",
+                "http-cache-semantics": "^4.0.0",
+                "keyv": "^3.0.0",
+                "lowercase-keys": "^2.0.0",
+                "normalize-url": "^4.1.0",
+                "responselike": "^1.0.2"
+            },
+            "dependencies": {
+                "get-stream": {
+                    "version": "5.2.0",
+                    "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+                    "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+                    "requires": {
+                        "pump": "^3.0.0"
+                    }
+                },
+                "lowercase-keys": {
+                    "version": "2.0.0",
+                    "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+                    "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
+                }
+            }
+        },
+        "camelcase": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+            "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
+        },
+        "case-anything": {
+            "version": "2.1.10",
+            "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
+            "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ=="
+        },
+        "chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "requires": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "dependencies": {
+                "has-flag": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+                },
+                "supports-color": {
+                    "version": "7.2.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+                    "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+                    "requires": {
+                        "has-flag": "^4.0.0"
+                    }
+                }
+            }
+        },
+        "chokidar": {
+            "version": "3.5.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+            "requires": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "fsevents": "~2.3.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            }
+        },
+        "chroma-js": {
+            "version": "2.4.2",
+            "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+            "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+        },
+        "ci-info": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+            "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
+        },
+        "cli-boxes": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
+            "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw=="
+        },
+        "clone-response": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+            "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+            "requires": {
+                "mimic-response": "^1.0.0"
+            }
+        },
+        "color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "requires": {
+                "color-name": "~1.1.4"
+            }
+        },
+        "color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+        },
+        "configstore": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+            "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+            "requires": {
+                "dot-prop": "^5.2.0",
+                "graceful-fs": "^4.1.2",
+                "make-dir": "^3.0.0",
+                "unique-string": "^2.0.0",
+                "write-file-atomic": "^3.0.0",
+                "xdg-basedir": "^4.0.0"
+            }
+        },
+        "create-require": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+        },
+        "crypto-random-string": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+            "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
+        },
+        "debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "requires": {
+                "ms": "^2.1.1"
+            }
+        },
+        "decompress-response": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+            "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+            "requires": {
+                "mimic-response": "^1.0.0"
+            }
+        },
+        "deep-extend": {
+            "version": "0.6.0",
+            "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+            "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+        },
+        "defer-to-connect": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+            "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
+        },
+        "diff": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
+        },
+        "dot-prop": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+            "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+            "requires": {
+                "is-obj": "^2.0.0"
+            }
+        },
+        "duplexer3": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+            "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+        },
+        "emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+        },
+        "end-of-stream": {
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+            "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+            "requires": {
+                "once": "^1.4.0"
+            }
+        },
+        "escape-goat": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+            "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q=="
+        },
+        "fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "requires": {
+                "to-regex-range": "^5.0.1"
+            }
+        },
+        "fsevents": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+            "optional": true
+        },
+        "get-stream": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+            "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+            "requires": {
+                "pump": "^3.0.0"
+            }
+        },
+        "glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "requires": {
+                "is-glob": "^4.0.1"
+            }
+        },
+        "global-dirs": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+            "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+            "requires": {
+                "ini": "2.0.0"
+            }
+        },
+        "got": {
+            "version": "9.6.0",
+            "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+            "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+            "requires": {
+                "@sindresorhus/is": "^0.14.0",
+                "@szmarczak/http-timer": "^1.1.2",
+                "cacheable-request": "^6.0.0",
+                "decompress-response": "^3.3.0",
+                "duplexer3": "^0.1.4",
+                "get-stream": "^4.1.0",
+                "lowercase-keys": "^1.0.1",
+                "mimic-response": "^1.0.1",
+                "p-cancelable": "^1.0.0",
+                "to-readable-stream": "^1.0.0",
+                "url-parse-lax": "^3.0.0"
+            }
+        },
+        "graceful-fs": {
+            "version": "4.2.9",
+            "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+            "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
+        },
+        "has-flag": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+            "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+        },
+        "has-yarn": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+            "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw=="
+        },
+        "http-cache-semantics": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+            "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
+        },
+        "ignore-by-default": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+            "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
+        },
+        "import-lazy": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+            "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM="
+        },
+        "imurmurhash": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+            "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
+        },
+        "ini": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+            "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
+        },
+        "is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "requires": {
+                "binary-extensions": "^2.0.0"
+            }
+        },
+        "is-ci": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+            "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+            "requires": {
+                "ci-info": "^2.0.0"
+            }
+        },
+        "is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+        },
+        "is-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+        },
+        "is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "requires": {
+                "is-extglob": "^2.1.1"
+            }
+        },
+        "is-installed-globally": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+            "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+            "requires": {
+                "global-dirs": "^3.0.0",
+                "is-path-inside": "^3.0.2"
+            }
+        },
+        "is-npm": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
+            "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA=="
+        },
+        "is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+        },
+        "is-obj": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+            "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
+        },
+        "is-path-inside": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
+        },
+        "is-typedarray": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+            "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+        },
+        "is-yarn-global": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+            "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
+        },
+        "json-buffer": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+            "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
+        },
+        "keyv": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+            "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+            "requires": {
+                "json-buffer": "3.0.0"
+            }
+        },
+        "latest-version": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+            "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+            "requires": {
+                "package-json": "^6.3.0"
+            }
+        },
+        "lowercase-keys": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+            "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
+        },
+        "lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "requires": {
+                "yallist": "^4.0.0"
+            }
+        },
+        "make-dir": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "requires": {
+                "semver": "^6.0.0"
+            },
+            "dependencies": {
+                "semver": {
+                    "version": "6.3.0",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+                    "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+                }
+            }
+        },
+        "make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+        },
+        "mimic-response": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+            "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
+        },
+        "minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "requires": {
+                "brace-expansion": "^1.1.7"
+            }
+        },
+        "minimist": {
+            "version": "1.2.6",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+            "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+        },
+        "ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        },
+        "nodemon": {
+            "version": "2.0.15",
+            "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz",
+            "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==",
+            "requires": {
+                "chokidar": "^3.5.2",
+                "debug": "^3.2.7",
+                "ignore-by-default": "^1.0.1",
+                "minimatch": "^3.0.4",
+                "pstree.remy": "^1.1.8",
+                "semver": "^5.7.1",
+                "supports-color": "^5.5.0",
+                "touch": "^3.1.0",
+                "undefsafe": "^2.0.5",
+                "update-notifier": "^5.1.0"
+            }
+        },
+        "nopt": {
+            "version": "1.0.10",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+            "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+            "requires": {
+                "abbrev": "1"
+            }
+        },
+        "normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+        },
+        "normalize-url": {
+            "version": "4.5.1",
+            "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
+            "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="
+        },
+        "once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+            "requires": {
+                "wrappy": "1"
+            }
+        },
+        "p-cancelable": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+            "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
+        },
+        "package-json": {
+            "version": "6.5.0",
+            "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+            "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+            "requires": {
+                "got": "^9.6.0",
+                "registry-auth-token": "^4.0.0",
+                "registry-url": "^5.0.0",
+                "semver": "^6.2.0"
+            },
+            "dependencies": {
+                "semver": {
+                    "version": "6.3.0",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+                    "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+                }
+            }
+        },
+        "picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+        },
+        "prepend-http": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+            "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
+        },
+        "pstree.remy": {
+            "version": "1.1.8",
+            "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+            "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+        },
+        "pump": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+            "requires": {
+                "end-of-stream": "^1.1.0",
+                "once": "^1.3.1"
+            }
+        },
+        "pupa": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
+            "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
+            "requires": {
+                "escape-goat": "^2.0.0"
+            }
+        },
+        "rc": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+            "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+            "requires": {
+                "deep-extend": "^0.6.0",
+                "ini": "~1.3.0",
+                "minimist": "^1.2.0",
+                "strip-json-comments": "~2.0.1"
+            },
+            "dependencies": {
+                "ini": {
+                    "version": "1.3.8",
+                    "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+                    "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+                }
+            }
+        },
+        "readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "requires": {
+                "picomatch": "^2.2.1"
+            }
+        },
+        "registry-auth-token": {
+            "version": "4.2.1",
+            "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
+            "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
+            "requires": {
+                "rc": "^1.2.8"
+            }
+        },
+        "registry-url": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+            "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
+            "requires": {
+                "rc": "^1.2.8"
+            }
+        },
+        "responselike": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+            "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+            "requires": {
+                "lowercase-keys": "^1.0.0"
+            }
+        },
+        "semver": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+        },
+        "semver-diff": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+            "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+            "requires": {
+                "semver": "^6.3.0"
+            },
+            "dependencies": {
+                "semver": {
+                    "version": "6.3.0",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+                    "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+                }
+            }
+        },
+        "signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+        },
+        "string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "requires": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            }
+        },
+        "strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "requires": {
+                "ansi-regex": "^5.0.1"
+            }
+        },
+        "strip-json-comments": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+            "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+        },
+        "supports-color": {
+            "version": "5.5.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+            "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+            "requires": {
+                "has-flag": "^3.0.0"
+            }
+        },
+        "to-readable-stream": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+            "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
+        },
+        "to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "requires": {
+                "is-number": "^7.0.0"
+            }
+        },
+        "touch": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+            "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+            "requires": {
+                "nopt": "~1.0.10"
+            }
+        },
+        "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==",
+            "requires": {
+                "@cspotcode/source-map-support": "0.7.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.0",
+                "yn": "3.1.1"
+            }
+        },
+        "type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
+        },
+        "typedarray-to-buffer": {
+            "version": "3.1.5",
+            "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+            "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+            "requires": {
+                "is-typedarray": "^1.0.0"
+            }
+        },
+        "typescript": {
+            "version": "4.6.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "peer": true
+        },
+        "undefsafe": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+            "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+        },
+        "unique-string": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+            "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+            "requires": {
+                "crypto-random-string": "^2.0.0"
+            }
+        },
+        "update-notifier": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
+            "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
+            "requires": {
+                "boxen": "^5.0.0",
+                "chalk": "^4.1.0",
+                "configstore": "^5.0.1",
+                "has-yarn": "^2.1.0",
+                "import-lazy": "^2.1.0",
+                "is-ci": "^2.0.0",
+                "is-installed-globally": "^0.4.0",
+                "is-npm": "^5.0.0",
+                "is-yarn-global": "^0.3.0",
+                "latest-version": "^5.1.0",
+                "pupa": "^2.1.1",
+                "semver": "^7.3.4",
+                "semver-diff": "^3.1.1",
+                "xdg-basedir": "^4.0.0"
+            },
+            "dependencies": {
+                "semver": {
+                    "version": "7.3.5",
+                    "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+                    "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+                    "requires": {
+                        "lru-cache": "^6.0.0"
+                    }
+                }
+            }
+        },
+        "url-parse-lax": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+            "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+            "requires": {
+                "prepend-http": "^2.0.0"
+            }
+        },
+        "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=="
+        },
+        "widest-line": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+            "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+            "requires": {
+                "string-width": "^4.0.0"
+            }
+        },
+        "wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "requires": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            }
+        },
+        "wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+        },
+        "write-file-atomic": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+            "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+            "requires": {
+                "imurmurhash": "^0.1.4",
+                "is-typedarray": "^1.0.0",
+                "signal-exit": "^3.0.2",
+                "typedarray-to-buffer": "^3.1.5"
+            }
+        },
+        "xdg-basedir": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+            "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="
+        },
+        "yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
+        }
+    }
+}

styles/package.json 🔗

@@ -0,0 +1,22 @@
+{
+    "name": "styles",
+    "version": "1.0.0",
+    "description": "",
+    "main": "index.js",
+    "scripts": {
+        "build": "npm run build-themes && npm run build-tokens",
+        "build-themes": "ts-node ./src/buildThemes.ts",
+        "build-tokens": "ts-node ./src/buildTokens.ts",
+        "watch": "nodemon"
+    },
+    "author": "",
+    "license": "ISC",
+    "dependencies": {
+        "@types/chroma-js": "^2.1.3",
+        "@types/node": "^17.0.23",
+        "case-anything": "^2.1.10",
+        "chroma-js": "^2.4.2",
+        "ts-node": "^10.7.0",
+        "nodemon": "^2.0.15"
+    }
+}

styles/src/buildThemes.ts 🔗

@@ -0,0 +1,17 @@
+import * as fs from "fs";
+import * as path from "path";
+import app from "./styleTree/app";
+import dark from "./themes/dark";
+import light from "./themes/light";
+import snakeCase from "./utils/snakeCase";
+
+const themes = [dark, light];
+for (let theme of themes) {
+  let styleTree = snakeCase(app(theme));
+  let styleTreeJSON = JSON.stringify(styleTree, null, 2);
+  let outPath = path.resolve(
+    `${__dirname}/../../assets/themes/${theme.name}.json`
+  );
+  fs.writeFileSync(outPath, styleTreeJSON);
+  console.log(`- ${outPath} created`);
+}

styles/src/buildTokens.ts 🔗

@@ -0,0 +1,110 @@
+import * as fs from "fs";
+import * as path from "path";
+import dark from "./themes/dark";
+import light from "./themes/light";
+import Theme from "./themes/theme";
+import { colors, fontFamilies, fontSizes, fontWeights } from "./tokens";
+
+// Organize theme tokens
+function themeTokens(theme: Theme) {
+  return {
+    meta: {
+      themeName: theme.name,
+    },
+    text: theme.textColor,
+    icon: theme.iconColor,
+    background: theme.backgroundColor,
+    border: theme.borderColor,
+    editor: theme.editor,
+    syntax: {
+      primary: {
+        value: theme.syntax.primary.color.value,
+        type: "color",
+      },
+      comment: {
+        value: theme.syntax.comment.color.value,
+        type: "color",
+      },
+      keyword: {
+        value: theme.syntax.keyword.color.value,
+        type: "color",
+      },
+      function: {
+        value: theme.syntax.function.color.value,
+        type: "color",
+      },
+      type: {
+        value: theme.syntax.type.color.value,
+        type: "color",
+      },
+      variant: {
+        value: theme.syntax.variant.color.value,
+        type: "color",
+      },
+      property: {
+        value: theme.syntax.property.color.value,
+        type: "color",
+      },
+      enum: {
+        value: theme.syntax.enum.color.value,
+        type: "color",
+      },
+      operator: {
+        value: theme.syntax.operator.color.value,
+        type: "color",
+      },
+      string: {
+        value: theme.syntax.string.color.value,
+        type: "color",
+      },
+      number: {
+        value: theme.syntax.number.color.value,
+        type: "color",
+      },
+      boolean: {
+        value: theme.syntax.boolean.color.value,
+        type: "color",
+      },
+    },
+    player: theme.player,
+    shadowAlpha: theme.shadowAlpha,
+  };
+}
+
+// Organize core tokens
+const coreTokens = {
+  color: {
+    ...colors,
+  },
+  text: {
+    family: fontFamilies,
+    weight: fontWeights,
+  },
+  size: fontSizes,
+};
+
+const combinedTokens: any = {};
+
+const distPath = path.resolve(`${__dirname}/../dist`);
+
+// Add core tokens to the combined tokens and write `core.json`.
+// We write `core.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
+const corePath = path.join(distPath, "core.json");
+fs.writeFileSync(corePath, JSON.stringify(coreTokens, null, 2));
+console.log(`- ${corePath} created`);
+combinedTokens.core = coreTokens;
+
+// Add each theme to the combined tokens and write ${theme}.json.
+// We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
+let themes = [dark, light];
+themes.forEach((theme) => {
+  const themePath = `${distPath}/${theme.name}.json`
+  fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2));
+  console.log(`- ${themePath} created`);
+  combinedTokens[theme.name] = themeTokens(theme);
+});
+
+// Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app.
+const combinedPath = path.resolve(`${distPath}/tokens.json`);
+fs.writeFileSync(combinedPath, JSON.stringify(combinedTokens, null, 2));
+console.log(`- ${combinedPath} created`);

styles/src/styleTree/app.ts 🔗

@@ -0,0 +1,45 @@
+import Theme from "../themes/theme";
+import chatPanel from "./chatPanel";
+import { text } from "./components";
+import contactsPanel from "./contactsPanel";
+import commandPalette from "./commandPalette";
+import editor from "./editor";
+import projectPanel from "./projectPanel";
+import search from "./search";
+import selectorModal from "./selectorModal";
+import workspace from "./workspace";
+
+export const panel = {
+  padding: { top: 12, left: 12, bottom: 12, right: 12 },
+};
+
+export default function app(theme: Theme): Object {
+  return {
+    selector: selectorModal(theme),
+    workspace: workspace(theme),
+    editor: editor(theme),
+    projectDiagnostics: {
+      tabIconSpacing: 4,
+      tabIconWidth: 13,
+      tabSummarySpacing: 10,
+      emptyMessage: text(theme, "sans", "primary", { size: "lg" }),
+      statusBarItem: {
+        ...text(theme, "sans", "muted"),
+        margin: {
+          right: 10,
+        },
+      },
+    },
+    commandPalette: commandPalette(theme),
+    projectPanel: projectPanel(theme),
+    chatPanel: chatPanel(theme),
+    contactsPanel: contactsPanel(theme),
+    search: search(theme),
+    breadcrumbs: {
+      ...text(theme, "sans", "secondary"),
+      padding: {
+        left: 6,
+      },
+    }
+  };
+}

styles/src/styleTree/chatPanel.ts 🔗

@@ -0,0 +1,108 @@
+import Theme from "../themes/theme";
+import { panel } from "./app";
+import {
+  backgroundColor,
+  border,
+  player,
+  shadow,
+  text,
+  TextColor
+} from "./components";
+
+export default function chatPanel(theme: Theme) {
+  function channelSelectItem(
+    theme: Theme,
+    textColor: TextColor,
+    hovered: boolean
+  ) {
+    return {
+      name: text(theme, "sans", textColor),
+      padding: 4,
+      hash: {
+        ...text(theme, "sans", "muted"),
+        margin: {
+          right: 8,
+        },
+      },
+      background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
+      cornerRadius: hovered ? 6 : 0,
+    };
+  }
+
+  const message = {
+    body: text(theme, "sans", "secondary"),
+    timestamp: text(theme, "sans", "muted", { size: "sm" }),
+    padding: {
+      bottom: 6,
+    },
+    sender: {
+      ...text(theme, "sans", "primary", { weight: "bold" }),
+      margin: {
+        right: 8,
+      },
+    },
+  };
+
+  return {
+    ...panel,
+    channelName: text(theme, "sans", "primary", { weight: "bold" }),
+    channelNameHash: {
+      ...text(theme, "sans", "muted"),
+      padding: {
+        right: 8,
+      },
+    },
+    channelSelect: {
+      header: {
+        ...channelSelectItem(theme, "primary", false),
+        padding: {
+          bottom: 4,
+          left: 0,
+        },
+      },
+      item: channelSelectItem(theme, "secondary", false),
+      hoveredItem: channelSelectItem(theme, "secondary", true),
+      activeItem: channelSelectItem(theme, "primary", false),
+      hoveredActiveItem: channelSelectItem(theme, "primary", true),
+      menu: {
+        background: backgroundColor(theme, 500),
+        cornerRadius: 6,
+        padding: 4,
+        border: border(theme, "primary"),
+        shadow: shadow(theme),
+      },
+    },
+    signInPrompt: text(theme, "sans", "secondary", { underline: true }),
+    hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
+    message,
+    pendingMessage: {
+      ...message,
+      body: {
+        ...message.body,
+        color: theme.textColor.muted.value,
+      },
+      sender: {
+        ...message.sender,
+        color: theme.textColor.muted.value,
+      },
+      timestamp: {
+        ...message.timestamp,
+        color: theme.textColor.muted.value,
+      },
+    },
+    inputEditor: {
+      background: backgroundColor(theme, 500),
+      cornerRadius: 6,
+      text: text(theme, "mono", "primary"),
+      placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
+      selection: player(theme, 1).selection,
+      border: border(theme, "secondary"),
+      padding: {
+        bottom: 7,
+        left: 8,
+        right: 8,
+        top: 7,
+      },
+    },
+  };
+}

styles/src/styleTree/commandPalette.ts 🔗

@@ -0,0 +1,23 @@
+import Theme from "../themes/theme";
+import { text, backgroundColor, border } from "./components";
+
+export default function commandPalette(theme: Theme) {
+  return {
+    keystrokeSpacing: 8,
+    key: {
+      text: text(theme, "mono", "secondary", { size: "xs" }),
+      cornerRadius: 4,
+      background: backgroundColor(theme, "on300"),
+      border: border(theme, "secondary"),
+      padding: {
+        top: 2,
+        bottom: 2,
+        left: 8,
+        right: 8,
+      },
+      margin: {
+        left: 2
+      },
+    }
+  }
+}

styles/src/styleTree/components.ts 🔗

@@ -0,0 +1,93 @@
+import chroma from "chroma-js";
+import Theme, { BackgroundColorSet } from "../themes/theme";
+import { fontFamilies, fontSizes, FontWeight } from "../tokens";
+import { Color } from "../utils/color";
+
+export type TextColor = keyof Theme["textColor"];
+export function text(
+  theme: Theme,
+  fontFamily: keyof typeof fontFamilies,
+  color: TextColor,
+  properties?: {
+    size?: keyof typeof fontSizes;
+    weight?: FontWeight;
+    underline?: boolean;
+  }
+) {
+  let size = fontSizes[properties?.size || "sm"].value;
+  return {
+    family: fontFamilies[fontFamily].value,
+    color: theme.textColor[color].value,
+    ...properties,
+    size,
+  };
+}
+export function textColor(theme: Theme, color: TextColor) {
+  return theme.textColor[color].value;
+}
+
+export type BorderColor = keyof Theme["borderColor"];
+export interface BorderOptions {
+  width?: number;
+  top?: boolean;
+  bottom?: boolean;
+  left?: boolean;
+  right?: boolean;
+  overlay?: boolean;
+}
+export function border(
+  theme: Theme,
+  color: BorderColor,
+  options?: BorderOptions
+) {
+  return {
+    color: borderColor(theme, color),
+    width: 1,
+    ...options,
+  };
+}
+export function borderColor(theme: Theme, color: BorderColor) {
+  return theme.borderColor[color].value;
+}
+
+export type IconColor = keyof Theme["iconColor"];
+export function iconColor(theme: Theme, color: IconColor) {
+  return theme.iconColor[color].value;
+}
+
+export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
+export interface Player {
+  selection: {
+    cursor: Color;
+    selection: Color;
+  };
+}
+export function player(
+  theme: Theme,
+  playerNumber: PlayerIndex,
+): Player {
+  return {
+    selection: {
+      cursor: theme.player[playerNumber].cursorColor.value,
+      selection: theme.player[playerNumber].selectionColor.value,
+    },
+  };
+}
+
+export type BackgroundColor = keyof Theme["backgroundColor"];
+export type BackgroundState = keyof BackgroundColorSet;
+export function backgroundColor(
+  theme: Theme,
+  name: BackgroundColor,
+  state?: BackgroundState,
+): Color {
+  return theme.backgroundColor[name][state || "base"].value;
+}
+
+export function shadow(theme: Theme) {
+  return {
+    blur: 16,
+    color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
+    offset: [0, 2],
+  };
+}

styles/src/styleTree/contactsPanel.ts 🔗

@@ -0,0 +1,62 @@
+import Theme from "../themes/theme";
+import { panel } from "./app";
+import { backgroundColor, borderColor, text } from "./components";
+
+export default function(theme: Theme) {
+  const project = {
+    guestAvatarSpacing: 4,
+    height: 24,
+    guestAvatar: {
+      cornerRadius: 8,
+      width: 14,
+    },
+    name: {
+      ...text(theme, "mono", "placeholder", { size: "sm" }),
+      margin: {
+        right: 6,
+      },
+    },
+    padding: {
+      left: 8,
+    },
+  };
+
+  const sharedProject = {
+    ...project,
+    background: backgroundColor(theme, 300),
+    cornerRadius: 6,
+    name: {
+      ...project.name,
+      ...text(theme, "mono", "secondary", { size: "sm" }),
+    },
+  };
+
+  return {
+    ...panel,
+    hostRowHeight: 28,
+    treeBranchColor: borderColor(theme, "muted"),
+    treeBranchWidth: 1,
+    hostAvatar: {
+      cornerRadius: 10,
+      width: 18,
+    },
+    hostUsername: {
+      ...text(theme, "mono", "primary", { size: "sm" }),
+      padding: {
+        left: 8,
+      },
+    },
+    project,
+    sharedProject,
+    hoveredSharedProject: {
+      ...sharedProject,
+      background: backgroundColor(theme, 300, "hovered"),
+      cornerRadius: 6,
+    },
+    unsharedProject: project,
+    hoveredUnsharedProject: {
+      ...project,
+      cornerRadius: 6,
+    },
+  }
+}

styles/src/styleTree/editor.ts 🔗

@@ -0,0 +1,146 @@
+import Theme from "../themes/theme";
+import {
+  backgroundColor,
+  border,
+  iconColor,
+  player,
+  text,
+  TextColor
+} from "./components";
+
+export default function editor(theme: Theme) {
+  const autocompleteItem = {
+    cornerRadius: 6,
+    padding: {
+      bottom: 2,
+      left: 6,
+      right: 6,
+      top: 2,
+    },
+  };
+
+  function diagnostic(theme: Theme, color: TextColor) {
+    return {
+      textScaleFactor: 0.857,
+      header: {
+        border: border(theme, "primary", {
+          top: true,
+        }),
+      },
+      message: {
+        text: text(theme, "sans", color, { size: "sm" }),
+        highlightText: text(theme, "sans", color, {
+          size: "sm",
+          weight: "bold",
+        }),
+      },
+    };
+  }
+
+  return {
+    // textColor: theme.syntax.primary.color,
+    textColor: theme.syntax.primary.color.value,
+    background: backgroundColor(theme, 500),
+    activeLineBackground: theme.editor.line.active.value,
+    codeActionsIndicator: iconColor(theme, "muted"),
+    diffBackgroundDeleted: backgroundColor(theme, "error"),
+    diffBackgroundInserted: backgroundColor(theme, "ok"),
+    documentHighlightReadBackground: theme.editor.highlight.occurrence.value,
+    documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value,
+    errorColor: theme.textColor.error.value,
+    gutterBackground: backgroundColor(theme, 500),
+    gutterPaddingFactor: 3.5,
+    highlightedLineBackground: theme.editor.line.highlighted.value,
+    lineNumber: theme.editor.gutter.primary.value,
+    lineNumberActive: theme.editor.gutter.active.value,
+    renameFade: 0.6,
+    unnecessaryCodeFade: 0.5,
+    selection: player(theme, 1).selection,
+    guestSelections: [
+      player(theme, 2).selection,
+      player(theme, 3).selection,
+      player(theme, 4).selection,
+      player(theme, 5).selection,
+      player(theme, 6).selection,
+      player(theme, 7).selection,
+      player(theme, 8).selection,
+    ],
+    autocomplete: {
+      background: backgroundColor(theme, 500),
+      cornerRadius: 8,
+      padding: 4,
+      border: border(theme, "secondary"),
+      item: autocompleteItem,
+      hoveredItem: {
+        ...autocompleteItem,
+        background: backgroundColor(theme, 500, "hovered"),
+      },
+      margin: {
+        left: -14,
+      },
+      matchHighlight: text(theme, "mono", "feature"),
+      selectedItem: {
+        ...autocompleteItem,
+        background: backgroundColor(theme, 500, "active"),
+      },
+    },
+    diagnosticHeader: {
+      background: backgroundColor(theme, 300),
+      iconWidthFactor: 1.5,
+      textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
+      border: border(theme, "secondary", {
+        bottom: true,
+        top: true,
+      }),
+      code: {
+        ...text(theme, "mono", "muted", { size: "sm" }),
+        margin: {
+          left: 10,
+        },
+      },
+      message: {
+        highlightText: text(theme, "sans", "primary", {
+          size: "sm",
+          weight: "bold",
+        }),
+        text: text(theme, "sans", "secondary", { size: "sm" }),
+      },
+    },
+    diagnosticPathHeader: {
+      background: theme.editor.line.active.value,
+      textScaleFactor: 0.857,
+      filename: text(theme, "mono", "primary", { size: "sm" }),
+      path: {
+        ...text(theme, "mono", "muted", { size: "sm" }),
+        margin: {
+          left: 12,
+        },
+      },
+    },
+    errorDiagnostic: diagnostic(theme, "error"),
+    warningDiagnostic: diagnostic(theme, "warning"),
+    informationDiagnostic: diagnostic(theme, "info"),
+    hintDiagnostic: diagnostic(theme, "info"),
+    invalidErrorDiagnostic: diagnostic(theme, "muted"),
+    invalidHintDiagnostic: diagnostic(theme, "muted"),
+    invalidInformationDiagnostic: diagnostic(theme, "muted"),
+    invalidWarningDiagnostic: diagnostic(theme, "muted"),
+    syntax: {
+      keyword: theme.syntax.keyword.color.value,
+      function: theme.syntax.function.color.value,
+      string: theme.syntax.string.color.value,
+      type: theme.syntax.type.color.value,
+      number: theme.syntax.number.color.value,
+      comment: theme.syntax.comment.color.value,
+      property: theme.syntax.property.color.value,
+      variant: theme.syntax.variant.color.value,
+      constant: theme.syntax.constant.color.value,
+      title: { color: theme.syntax.title.color.value, weight: "bold" },
+      emphasis: theme.textColor.feature.value,
+      "emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
+      link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
+      link_text: { color: theme.syntax.linkText.color.value, italic: true },
+      list_marker: theme.syntax.punctuation.color.value,
+    },
+  };
+}

styles/src/styleTree/projectPanel.ts 🔗

@@ -0,0 +1,37 @@
+import Theme from "../themes/theme";
+import { Color } from "../utils/color";
+import { panel } from "./app";
+import { backgroundColor, iconColor, text, TextColor } from "./components";
+
+export default function projectPanel(theme: Theme) {
+  function entry(theme: Theme, textColor: TextColor, background?: Color) {
+    return {
+      height: 22,
+      background,
+      iconColor: iconColor(theme, "muted"),
+      iconSize: 8,
+      iconSpacing: 8,
+      text: text(theme, "mono", textColor, { size: "sm" }),
+    };
+  }
+
+  return {
+    ...panel,
+    entry: entry(theme, "secondary"),
+    hoveredEntry: entry(
+      theme,
+      "secondary",
+      backgroundColor(theme, 300, "hovered")
+    ),
+    selectedEntry: entry(theme, "primary"),
+    hoveredSelectedEntry: entry(
+      theme,
+      "primary",
+      backgroundColor(theme, 300, "hovered")
+    ),
+    padding: {
+      top: 6,
+      left: 12,
+    },
+  };
+}

styles/src/styleTree/search.ts 🔗

@@ -0,0 +1,84 @@
+import Theme from "../themes/theme";
+import { backgroundColor, border, player, text } from "./components";
+
+export default function search(theme: Theme) {
+  const optionButton = {
+    ...text(theme, "mono", "secondary"),
+    background: backgroundColor(theme, "on500"),
+    cornerRadius: 4,
+    border: border(theme, "secondary"),
+    margin: {
+      left: 2,
+      right: 2,
+    },
+    padding: {
+      bottom: 3,
+      left: 8,
+      right: 8,
+      top: 3,
+    },
+  };
+
+  const editor = {
+    background: backgroundColor(theme, 500),
+    cornerRadius: 8,
+    minWidth: 200,
+    maxWidth: 500,
+    placeholderText: text(theme, "mono", "placeholder"),
+    selection: player(theme, 1).selection,
+    text: text(theme, "mono", "active"),
+    border: border(theme, "secondary"),
+    margin: {
+      right: 6,
+    },
+    padding: {
+      top: 3,
+      bottom: 3,
+      left: 12,
+      right: 8,
+    },
+  };
+
+  return {
+    matchBackground: theme.editor.highlight.match.value,
+    tabIconSpacing: 8,
+    tabIconWidth: 14,
+    activeHoveredOptionButton: {
+      ...optionButton,
+      ...text(theme, "mono", "active"),
+      background: backgroundColor(theme, "on500", "active"),
+      border: border(theme, "muted"),
+    },
+    activeOptionButton: {
+      ...optionButton,
+      ...text(theme, "mono", "active"),
+      background: backgroundColor(theme, "on500", "active"),
+      border: border(theme, "muted"),
+    },
+    editor,
+    hoveredOptionButton: {
+      ...optionButton,
+      ...text(theme, "mono", "active"),
+      border: border(theme, "muted"),
+    },
+    invalidEditor: {
+      ...editor,
+      border: border(theme, "error"),
+    },
+    matchIndex: {
+      ...text(theme, "mono", "muted"),
+      padding: 6,
+    },
+    optionButton,
+    optionButtonGroup: {
+      padding: {
+        left: 4,
+        right: 4,
+      },
+    },
+    resultsStatus: {
+      ...text(theme, "mono", "primary"),
+      size: 18,
+    },
+  };
+}

styles/src/styleTree/selectorModal.ts 🔗

@@ -0,0 +1,59 @@
+import Theme from "../themes/theme";
+import { backgroundColor, border, player, shadow, text } from "./components";
+
+export default function selectorModal(theme: Theme): Object {
+  const item = {
+    padding: {
+      bottom: 4,
+      left: 12,
+      right: 12,
+      top: 4,
+    },
+    cornerRadius: 8,
+    text: text(theme, "sans", "secondary"),
+    highlightText: text(theme, "sans", "feature", { weight: "bold" }),
+  };
+
+  const activeItem = {
+    ...item,
+    background: backgroundColor(theme, 300, "active"),
+    text: text(theme, "sans", "primary"),
+  };
+
+  return {
+    background: backgroundColor(theme, 300),
+    cornerRadius: 8,
+    padding: 8,
+    item,
+    activeItem,
+    border: border(theme, "primary"),
+    empty: {
+      text: text(theme, "sans", "placeholder"),
+      padding: {
+        bottom: 4,
+        left: 12,
+        right: 12,
+        top: 8,
+      },
+    },
+    inputEditor: {
+      background: backgroundColor(theme, 500),
+      cornerRadius: 8,
+      placeholderText: text(theme, "sans", "placeholder"),
+      selection: player(theme, 1).selection,
+      text: text(theme, "mono", "primary"),
+      border: border(theme, "secondary"),
+      padding: {
+        bottom: 7,
+        left: 16,
+        right: 16,
+        top: 7,
+      },
+    },
+    margin: {
+      bottom: 52,
+      top: 52,
+    },
+    shadow: shadow(theme),
+  };
+}

styles/src/styleTree/workspace.ts 🔗

@@ -0,0 +1,158 @@
+import Theme from "../themes/theme";
+import { backgroundColor, border, iconColor, text } from "./components";
+
+export default function workspace(theme: Theme) {
+  const signInPrompt = {
+    ...text(theme, "sans", "secondary", { size: "xs" }),
+    border: border(theme, "primary"),
+    cornerRadius: 6,
+    margin: {
+      top: 1,
+      right: 6,
+    },
+    padding: {
+      left: 6,
+      right: 6,
+    },
+  };
+
+  const tab = {
+    height: 32,
+    background: backgroundColor(theme, 300),
+    iconClose: iconColor(theme, "muted"),
+    iconCloseActive: iconColor(theme, "active"),
+    iconConflict: iconColor(theme, "warning"),
+    iconDirty: iconColor(theme, "info"),
+    iconWidth: 8,
+    spacing: 8,
+    text: text(theme, "sans", "secondary", { size: "sm" }),
+    border: border(theme, "primary", {
+      left: true,
+      bottom: true,
+      overlay: true,
+    }),
+    padding: {
+      left: 8,
+      right: 8,
+    },
+  };
+
+  const activeTab = {
+    ...tab,
+    background: backgroundColor(theme, 500),
+    text: text(theme, "sans", "active", { size: "sm" }),
+    border: {
+      ...tab.border,
+      bottom: false,
+    },
+  };
+
+  const sidebarItem = {
+    height: 32,
+    iconColor: iconColor(theme, "secondary"),
+    iconSize: 18,
+  };
+  const sidebar = {
+    width: 30,
+    background: backgroundColor(theme, 300),
+    border: border(theme, "primary", { right: true }),
+    item: sidebarItem,
+    activeItem: {
+      ...sidebarItem,
+      iconColor: iconColor(theme, "active"),
+    },
+    resizeHandle: {
+      background: border(theme, "primary").color,
+      padding: {
+        left: 1,
+      },
+    },
+  };
+
+  return {
+    background: backgroundColor(theme, 300),
+    leaderBorderOpacity: 0.7,
+    leaderBorderWidth: 2.0,
+    tab,
+    activeTab,
+    leftSidebar: {
+      ...sidebar,
+      border: border(theme, "primary", { right: true }),
+    },
+    rightSidebar: {
+      ...sidebar,
+      border: border(theme, "primary", { left: true }),
+    },
+    paneDivider: {
+      color: border(theme, "secondary").color,
+      width: 1,
+    },
+    status_bar: {
+      height: 24,
+      itemSpacing: 8,
+      padding: {
+        left: 6,
+        right: 6,
+      },
+      border: border(theme, "primary", { top: true, overlay: true }),
+      cursorPosition: text(theme, "sans", "muted"),
+      diagnosticMessage: text(theme, "sans", "muted"),
+      lspMessage: text(theme, "sans", "muted"),
+      autoUpdateProgressMessage: text(theme, "sans", "muted"),
+      autoUpdateDoneMessage: text(theme, "sans", "muted"),
+    },
+    titlebar: {
+      avatarWidth: 18,
+      height: 32,
+      background: backgroundColor(theme, 100),
+      shareIconColor: iconColor(theme, "secondary"),
+      shareIconActiveColor: iconColor(theme, "feature"),
+      title: text(theme, "sans", "primary"),
+      avatar: {
+        cornerRadius: 10,
+        border: {
+          color: "#00000088",
+          width: 1,
+        },
+      },
+      avatarRibbon: {
+        height: 3,
+        width: 12,
+        // TODO: The background for this ideally should be 
+        // set with a token, not hardcoded in rust
+      },
+      border: border(theme, "primary", { bottom: true }),
+      signInPrompt,
+      hoveredSignInPrompt: {
+        ...signInPrompt,
+        ...text(theme, "sans", "active", { size: "xs" }),
+      },
+      offlineIcon: {
+        color: iconColor(theme, "secondary"),
+        width: 16,
+        padding: {
+          right: 4,
+        },
+      },
+      outdatedWarning: {
+        ...text(theme, "sans", "warning"),
+        size: 13,
+      },
+    },
+    toolbar: {
+      height: 34,
+      background: backgroundColor(theme, 500),
+      border: border(theme, "secondary", { bottom: true }),
+      itemSpacing: 8,
+      padding: { left: 16, right: 8, top: 4, bottom: 4 },
+    },
+    breadcrumbs: {
+      ...text(theme, "mono", "secondary"),
+      padding: { left: 6 },
+    },
+    disconnectedOverlay: {
+      ...text(theme, "sans", "active"),
+      background: "#000000aa",
+    },
+  };
+}

styles/src/themes/dark.ts 🔗

@@ -0,0 +1,241 @@
+import { colors, fontWeights, NumberToken } from "../tokens";
+import { withOpacity } from "../utils/color";
+import Theme, { buildPlayer, Syntax } from "./theme";
+
+const backgroundColor = {
+  100: {
+    base: colors.neutral[750],
+    hovered: colors.neutral[725],
+    active: colors.neutral[800],
+    focused: colors.neutral[675],
+  },
+  300: {
+    base: colors.neutral[800],
+    hovered: colors.neutral[775],
+    active: colors.neutral[750],
+    focused: colors.neutral[775],
+  },
+  500: {
+    base: colors.neutral[900],
+    hovered: withOpacity(colors.neutral[0], 0.08),
+    active: withOpacity(colors.neutral[0], 0.12),
+    focused: colors.neutral[825],
+  },
+  on300: {
+    base: withOpacity(colors.neutral[850], 0.5),
+    hovered: colors.neutral[875],
+    active: colors.neutral[900],
+    focused: colors.neutral[875],
+  },
+  on500: {
+    base: colors.neutral[850],
+    hovered: colors.neutral[800],
+    active: colors.neutral[775],
+    focused: colors.neutral[800],
+  },
+  ok: {
+    base: colors.green[600],
+    hovered: colors.green[600],
+    active: colors.green[600],
+    focused: colors.green[600],
+  },
+  error: {
+    base: colors.red[400],
+    hovered: colors.red[400],
+    active: colors.red[400],
+    focused: colors.red[400],
+  },
+  warning: {
+    base: colors.amber[300],
+    hovered: colors.amber[300],
+    active: colors.amber[300],
+    focused: colors.amber[300],
+  },
+  info: {
+    base: colors.blue[500],
+    hovered: colors.blue[500],
+    active: colors.blue[500],
+    focused: colors.blue[500],
+  },
+};
+
+const borderColor = {
+  primary: colors.neutral[875],
+  secondary: colors.neutral[775],
+  muted: colors.neutral[675],
+  focused: colors.indigo[500],
+  active: colors.neutral[900],
+  ok: colors.green[500],
+  error: colors.red[500],
+  warning: colors.amber[500],
+  info: colors.blue[500],
+};
+
+const textColor = {
+  primary: colors.neutral[50],
+  secondary: colors.neutral[350],
+  muted: colors.neutral[450],
+  placeholder: colors.neutral[650],
+  active: colors.neutral[0],
+  //TODO: (design) define feature and it's correct value
+  feature: colors.blue[400],
+  ok: colors.green[600],
+  error: colors.red[400],
+  warning: colors.amber[300],
+  info: colors.blue[500],
+};
+
+const iconColor = {
+  primary: colors.neutral[200],
+  secondary: colors.neutral[350],
+  muted: colors.neutral[600],
+  placeholder: colors.neutral[700],
+  active: colors.neutral[0],
+  //TODO: (design) define feature and it's correct value
+  feature: colors.blue[500],
+  ok: colors.green[600],
+  error: colors.red[500],
+  warning: colors.amber[400],
+  info: colors.blue[600],
+};
+
+const player = {
+  1: buildPlayer(colors.blue[500]),
+  2: buildPlayer(colors.lime[500]),
+  3: buildPlayer(colors.fuschia[500]),
+  4: buildPlayer(colors.orange[500]),
+  5: buildPlayer(colors.purple[500]),
+  6: buildPlayer(colors.teal[400]),
+  7: buildPlayer(colors.pink[400]),
+  8: buildPlayer(colors.yellow[400]),
+};
+
+const editor = {
+  background: backgroundColor[500].base,
+  indent_guide: borderColor.muted,
+  indent_guide_active: borderColor.secondary,
+  line: {
+    active: withOpacity(colors.neutral[0], 0.07),
+    highlighted: withOpacity(colors.neutral[0], 0.12),
+    inserted: backgroundColor.ok.active,
+    deleted: backgroundColor.error.active,
+    modified: backgroundColor.info.active,
+  },
+  highlight: {
+    selection: player[1].selectionColor,
+    occurrence: withOpacity(colors.neutral[0], 0.12),
+    activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side
+    matchingBracket: backgroundColor[500].active,
+    match: withOpacity(colors.violet[700], 0.5),
+    activeMatch: withOpacity(colors.violet[600], 0.7),
+    related: backgroundColor[500].focused,
+  },
+  gutter: {
+    primary: textColor.placeholder,
+    active: textColor.active,
+  },
+};
+
+const syntax: Syntax = {
+  primary: {
+    color: colors.neutral[150],
+    weight: fontWeights.normal,
+  },
+  comment: {
+    color: colors.neutral[300],
+    weight: fontWeights.normal,
+  },
+  punctuation: {
+    color: colors.neutral[200],
+    weight: fontWeights.normal,
+  },
+  constant: {
+    color: colors.neutral[150],
+    weight: fontWeights.normal,
+  },
+  keyword: {
+    color: colors.blue[400],
+    weight: fontWeights.normal,
+  },
+  function: {
+    color: colors.yellow[200],
+    weight: fontWeights.normal,
+  },
+  type: {
+    color: colors.teal[300],
+    weight: fontWeights.normal,
+  },
+  variant: {
+    color: colors.sky[300],
+    weight: fontWeights.normal,
+  },
+  property: {
+    color: colors.blue[400],
+    weight: fontWeights.normal,
+  },
+  enum: {
+    color: colors.orange[500],
+    weight: fontWeights.normal,
+  },
+  operator: {
+    color: colors.orange[500],
+    weight: fontWeights.normal,
+  },
+  string: {
+    color: colors.orange[300],
+    weight: fontWeights.normal,
+  },
+  number: {
+    color: colors.lime[300],
+    weight: fontWeights.normal,
+  },
+  boolean: {
+    color: colors.lime[300],
+    weight: fontWeights.normal,
+  },
+  predictive: {
+    color: textColor.muted,
+    weight: fontWeights.normal,
+  },
+  title: {
+    color: colors.amber[500],
+    weight: fontWeights.bold,
+  },
+  emphasis: {
+    color: textColor.active,
+    weight: fontWeights.normal,
+  },
+  emphasisStrong: {
+    color: textColor.active,
+    weight: fontWeights.bold,
+  },
+  linkUrl: {
+    color: colors.lime[500],
+    weight: fontWeights.normal,
+    // TODO: add underline
+  },
+  linkText: {
+    color: colors.orange[500],
+    weight: fontWeights.normal,
+    // TODO: add italic
+  },
+};
+
+const shadowAlpha: NumberToken = {
+  value: 0.32,
+  type: "number",
+};
+
+const theme: Theme = {
+  name: "dark",
+  backgroundColor,
+  borderColor,
+  textColor,
+  iconColor,
+  editor,
+  syntax,
+  player,
+  shadowAlpha,
+};
+
+export default theme;

styles/src/themes/light.ts 🔗

@@ -0,0 +1,239 @@
+import { colors, fontWeights, NumberToken } from "../tokens";
+import { withOpacity } from "../utils/color";
+import Theme, { buildPlayer, Syntax } from "./theme";
+
+const backgroundColor = {
+  100: {
+    base: colors.neutral[75],
+    hovered: colors.neutral[100],
+    active: colors.neutral[150],
+    focused: colors.neutral[100],
+  },
+  300: {
+    base: colors.neutral[25],
+    hovered: colors.neutral[75],
+    active: colors.neutral[100],
+    focused: colors.neutral[75],
+  },
+  500: {
+    base: colors.neutral[0],
+    hovered: withOpacity(colors.neutral[900], 0.03),
+    active: withOpacity(colors.neutral[900], 0.06),
+    focused: colors.neutral[50],
+  },
+  on300: {
+    base: colors.neutral[50],
+    hovered: colors.neutral[100],
+    active: colors.neutral[150],
+    focused: colors.neutral[100],
+  },
+  on500: {
+    base: colors.neutral[50],
+    hovered: colors.neutral[25],
+    active: colors.neutral[0],
+    focused: colors.neutral[25],
+  },
+  ok: {
+    base: colors.green[100],
+    hovered: colors.green[100],
+    active: colors.green[100],
+    focused: colors.green[100],
+  },
+  error: {
+    base: colors.red[100],
+    hovered: colors.red[100],
+    active: colors.red[100],
+    focused: colors.red[100],
+  },
+  warning: {
+    base: colors.yellow[100],
+    hovered: colors.yellow[100],
+    active: colors.yellow[100],
+    focused: colors.yellow[100],
+  },
+  info: {
+    base: colors.blue[100],
+    hovered: colors.blue[100],
+    active: colors.blue[100],
+    focused: colors.blue[100],
+  },
+};
+
+const borderColor = {
+  primary: colors.neutral[150],
+  secondary: colors.neutral[150],
+  muted: colors.neutral[100],
+  focused: colors.indigo[500],
+  active: colors.neutral[250],
+  ok: colors.green[200],
+  error: colors.red[200],
+  warning: colors.yellow[200],
+  info: colors.blue[200],
+};
+
+const textColor = {
+  primary: colors.neutral[750],
+  secondary: colors.neutral[650],
+  muted: colors.neutral[550],
+  placeholder: colors.neutral[450],
+  active: colors.neutral[900],
+  feature: colors.indigo[500],
+  ok: colors.green[500],
+  error: colors.red[500],
+  warning: colors.yellow[500],
+  info: colors.blue[500],
+};
+
+const iconColor = {
+  primary: colors.neutral[700],
+  secondary: colors.neutral[500],
+  muted: colors.neutral[350],
+  placeholder: colors.neutral[300],
+  active: colors.neutral[900],
+  feature: colors.indigo[500],
+  ok: colors.green[600],
+  error: colors.red[600],
+  warning: colors.yellow[400],
+  info: colors.blue[600],
+};
+
+const player = {
+  1: buildPlayer(colors.blue[500]),
+  2: buildPlayer(colors.emerald[400]),
+  3: buildPlayer(colors.fuschia[400]),
+  4: buildPlayer(colors.orange[400]),
+  5: buildPlayer(colors.purple[400]),
+  6: buildPlayer(colors.teal[400]),
+  7: buildPlayer(colors.pink[400]),
+  8: buildPlayer(colors.yellow[400]),
+};
+
+const editor = {
+  background: backgroundColor[500].base,
+  indent_guide: borderColor.muted,
+  indent_guide_active: borderColor.secondary,
+  line: {
+    active: withOpacity(colors.neutral[900], 0.06),
+    highlighted: withOpacity(colors.neutral[900], 0.12),
+    inserted: backgroundColor.ok.active,
+    deleted: backgroundColor.error.active,
+    modified: backgroundColor.info.active,
+  },
+  highlight: {
+    selection: player[1].selectionColor,
+    occurrence: withOpacity(colors.neutral[900], 0.06),
+    activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side
+    matchingBracket: colors.neutral[0],
+    match: colors.yellow[100],
+    activeMatch: colors.yellow[200], // TODO: This is not hooked up to occurences on the rust side
+    related: colors.neutral[0],
+  },
+  gutter: {
+    primary: colors.neutral[300],
+    active: textColor.active,
+  },
+};
+
+const syntax: Syntax = {
+  primary: {
+    color: colors.neutral[800],
+    weight: fontWeights.normal,
+  },
+  comment: {
+    color: colors.neutral[500],
+    weight: fontWeights.normal,
+  },
+  punctuation: {
+    color: colors.neutral[600],
+    weight: fontWeights.normal,
+  },
+  constant: {
+    color: colors.neutral[800],
+    weight: fontWeights.normal,
+  },
+  keyword: {
+    color: colors.indigo[700],
+    weight: fontWeights.normal,
+  },
+  function: {
+    color: colors.orange[600],
+    weight: fontWeights.normal,
+  },
+  type: {
+    color: colors.yellow[600],
+    weight: fontWeights.normal,
+  },
+  variant: {
+    color: colors.rose[700],
+    weight: fontWeights.normal,
+  },
+  property: {
+    color: colors.emerald[700],
+    weight: fontWeights.normal,
+  },
+  enum: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  operator: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  string: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  number: {
+    color: colors.indigo[500],
+    weight: fontWeights.normal,
+  },
+  boolean: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+  },
+  predictive: {
+    color: textColor.placeholder,
+    weight: fontWeights.normal,
+  },
+  title: {
+    color: colors.sky[500],
+    weight: fontWeights.bold,
+  },
+  emphasis: {
+    color: textColor.active,
+    weight: fontWeights.normal,
+  },
+  emphasisStrong: {
+    color: textColor.active,
+    weight: fontWeights.bold,
+  },
+  linkUrl: {
+    color: colors.lime[500],
+    weight: fontWeights.normal,
+    // TODO: add underline
+  },
+  linkText: {
+    color: colors.red[500],
+    weight: fontWeights.normal,
+    // TODO: add italic
+  },
+};
+
+const shadowAlpha: NumberToken = {
+  value: 0.12,
+  type: "number",
+};
+
+const theme: Theme = {
+  name: "light",
+  backgroundColor,
+  borderColor,
+  textColor,
+  iconColor,
+  editor,
+  syntax,
+  player,
+  shadowAlpha,
+};
+
+export default theme;

styles/src/themes/theme.ts 🔗

@@ -0,0 +1,147 @@
+import { ColorToken, FontWeightToken, NumberToken } from "../tokens";
+import { withOpacity } from "../utils/color";
+
+export interface SyntaxHighlightStyle {
+  color: ColorToken;
+  weight: FontWeightToken;
+}
+
+export interface Player {
+  baseColor: ColorToken;
+  cursorColor: ColorToken;
+  selectionColor: ColorToken;
+  borderColor: ColorToken;
+}
+export function buildPlayer(
+  color: ColorToken,
+  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: ColorToken;
+  hovered: ColorToken;
+  active: ColorToken;
+  focused: ColorToken;
+}
+
+export interface Syntax {
+  primary: SyntaxHighlightStyle;
+  comment: SyntaxHighlightStyle;
+  punctuation: SyntaxHighlightStyle;
+  constant: SyntaxHighlightStyle;
+  keyword: SyntaxHighlightStyle;
+  function: SyntaxHighlightStyle;
+  type: SyntaxHighlightStyle;
+  variant: SyntaxHighlightStyle;
+  property: SyntaxHighlightStyle;
+  enum: SyntaxHighlightStyle;
+  operator: SyntaxHighlightStyle;
+  string: SyntaxHighlightStyle;
+  number: SyntaxHighlightStyle;
+  boolean: SyntaxHighlightStyle;
+  predictive: SyntaxHighlightStyle;
+  // TODO: Either move the following or rename
+  title: SyntaxHighlightStyle;
+  emphasis: SyntaxHighlightStyle;
+  emphasisStrong: SyntaxHighlightStyle;
+  linkUrl: SyntaxHighlightStyle;
+  linkText: SyntaxHighlightStyle;
+};
+
+export default interface Theme {
+  name: string;
+  backgroundColor: {
+    100: BackgroundColorSet;
+    300: BackgroundColorSet;
+    500: BackgroundColorSet;
+    on300: BackgroundColorSet;
+    on500: BackgroundColorSet;
+    ok: BackgroundColorSet;
+    error: BackgroundColorSet;
+    warning: BackgroundColorSet;
+    info: BackgroundColorSet;
+  };
+  borderColor: {
+    primary: ColorToken;
+    secondary: ColorToken;
+    muted: ColorToken;
+    focused: ColorToken;
+    active: ColorToken;
+    ok: ColorToken;
+    error: ColorToken;
+    warning: ColorToken;
+    info: ColorToken;
+  };
+  textColor: {
+    primary: ColorToken;
+    secondary: ColorToken;
+    muted: ColorToken;
+    placeholder: ColorToken;
+    active: ColorToken;
+    feature: ColorToken;
+    ok: ColorToken;
+    error: ColorToken;
+    warning: ColorToken;
+    info: ColorToken;
+  };
+  iconColor: {
+    primary: ColorToken;
+    secondary: ColorToken;
+    muted: ColorToken;
+    placeholder: ColorToken;
+    active: ColorToken;
+    feature: ColorToken;
+    ok: ColorToken;
+    error: ColorToken;
+    warning: ColorToken;
+    info: ColorToken;
+  };
+  editor: {
+    background: ColorToken;
+    indent_guide: ColorToken;
+    indent_guide_active: ColorToken;
+    line: {
+      active: ColorToken;
+      highlighted: ColorToken;
+      inserted: ColorToken;
+      deleted: ColorToken;
+      modified: ColorToken;
+    };
+    highlight: {
+      selection: ColorToken;
+      occurrence: ColorToken;
+      activeOccurrence: ColorToken;
+      matchingBracket: ColorToken;
+      match: ColorToken;
+      activeMatch: ColorToken;
+      related: ColorToken;
+    };
+    gutter: {
+      primary: ColorToken;
+      active: ColorToken;
+    };
+  };
+
+  syntax: Syntax,
+
+  player: {
+    1: Player;
+    2: Player;
+    3: Player;
+    4: Player;
+    5: Player;
+    6: Player;
+    7: Player;
+    8: Player;
+  };
+  shadowAlpha: NumberToken;
+}

styles/src/tokens.ts 🔗

@@ -0,0 +1,102 @@
+import { colorRamp } from "./utils/color";
+
+interface Token<V, T> {
+  value: V,
+  type: T
+}
+
+export type FontFamily = string;
+export type FontFamilyToken = Token<FontFamily, "fontFamily">;
+function fontFamily(value: FontFamily): FontFamilyToken {
+  return {
+    value,
+    type: "fontFamily"
+  }
+}
+export const fontFamilies = {
+  sans: fontFamily("Zed Sans"),
+  mono: fontFamily("Zed Mono"),
+}
+
+export type FontSize = number;
+export type FontSizeToken = Token<FontSize, "fontSize">;
+function fontSize(value: FontSize) {
+  return {
+    value,
+    type: "fontSize"
+  };
+}
+export const fontSizes = {
+  "3xs": fontSize(8),
+  "2xs": fontSize(10),
+  xs: fontSize(12),
+  sm: fontSize(14),
+  md: fontSize(16),
+  lg: fontSize(18),
+  xl: fontSize(20),
+};
+
+export type FontWeight =
+  | "thin"
+  | "extra_light"
+  | "light"
+  | "normal"
+  | "medium"
+  | "semibold"
+  | "bold"
+  | "extra_bold"
+  | "black";
+export type FontWeightToken = Token<FontWeight, "fontWeight">;
+function fontWeight(value: FontWeight): FontWeightToken {
+  return {
+    value,
+    type: "fontWeight"
+  };
+}
+export const fontWeights = {
+  "thin": fontWeight("thin"),
+  "extra_light": fontWeight("extra_light"),
+  "light": fontWeight("light"),
+  "normal": fontWeight("normal"),
+  "medium": fontWeight("medium"),
+  "semibold": fontWeight("semibold"),
+  "bold": fontWeight("bold"),
+  "extra_bold": fontWeight("extra_bold"),
+  "black": fontWeight("black"),
+}
+
+export type Color = string;
+export interface ColorToken {
+  value: Color,
+  type: "color",
+  step?: number,
+}
+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"),
+}
+
+export type NumberToken = Token<number, "number">;
+
+export default {
+  fontFamilies,
+  fontSizes,
+  fontWeights,
+  colors,
+};

styles/src/utils/color.ts 🔗

@@ -0,0 +1,52 @@
+import chroma, { Scale } from "chroma-js";
+import { ColorToken } from "../tokens";
+
+export type Color = string;
+export type ColorRampStep = { value: Color; type: "color"; step: number };
+export type ColorRamp = {
+  [index: number]: ColorRampStep;
+};
+
+export function colorRamp(
+  color: Color | [Color, Color],
+  options?: { steps?: number; increment?: number; }
+): ColorRamp {
+  let scale: Scale;
+  if (Array.isArray(color)) {
+    const [startColor, endColor] = color;
+    scale = chroma.scale([startColor, endColor]);
+  } else {
+    let hue = Math.round(chroma(color).hsl()[0]);
+    let startColor = chroma.hsl(hue, 0.88, 0.96);
+    let endColor = chroma.hsl(hue, 0.68, 0.12);
+    scale = chroma
+      .scale([startColor, color, endColor])
+      .domain([0, 0.5, 1])
+      .mode("hsl")
+      .gamma(1)
+      // .correctLightness(true)
+      .padding([0, 0]);
+  }
+
+  const ramp: ColorRamp = {};
+  const steps = options?.steps || 10;
+  const increment = options?.increment || 100;
+
+  scale.colors(steps, "hex").forEach((color, ix) => {
+    const step = ix * increment;
+    ramp[step] = {
+      value: color,
+      step,
+      type: "color",
+    };
+  });
+
+  return ramp;
+}
+
+export function withOpacity(color: ColorToken, opacity: number): ColorToken {
+  return {
+    ...color,
+    value: chroma(color.value).alpha(opacity).hex()
+  };
+}

styles/src/utils/snakeCase.ts 🔗

@@ -0,0 +1,35 @@
+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 :
+  S;
+
+type SnakeCased<Type> = {
+  [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)] = snakeCaseValue(object[key]);
+  }
+  return snakeObject;
+}
+
+function snakeCaseValue(value: any): any {
+  if (typeof value === "object") {
+    if (Array.isArray(value)) {
+      return value.map(snakeCaseValue);
+    } else {
+      return snakeCaseTree(value);
+    }
+  } else {
+    return value;
+  }
+}

styles/tsconfig.json 🔗

@@ -0,0 +1,14 @@
+{
+    "compilerOptions": {
+        "target": "es2015",
+        "module": "commonjs",
+        "esModuleInterop": true,
+        "noImplicitAny": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": true
+    },
+    "exclude": [
+        "node_modules"
+    ]
+}