Merge branch 'main' into randomized-test-improvements

Antonio Scandurra created

Change summary

.gitignore                                                                |   4 
Cargo.lock                                                                | 122 
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                                               | 259 
assets/keymaps/vim.json                                                   |  58 
assets/themes/dark.json                                                   |   0 
assets/themes/light.json                                                  |   0 
crates/assets/Cargo.toml                                                  |  14 
crates/assets/src/assets.rs                                               |   2 
crates/chat_panel/src/chat_panel.rs                                       |   3 
crates/collab/.env.template.toml                                          |   0 
crates/collab/Cargo.toml                                                  |   7 
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                                   |  14 
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                                                  |   0 
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                                                 |   0 
crates/collab/src/main.rs                                                 |   0 
crates/collab/src/releases.rs                                             |   0 
crates/collab/src/rpc.rs                                                  |  14 
crates/collab/src/rpc/store.rs                                            |   0 
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/diagnostics/src/diagnostics.rs                                     |  29 
crates/diagnostics/src/items.rs                                           |   7 
crates/editor/src/editor.rs                                               | 475 
crates/file_finder/src/file_finder.rs                                     |  15 
crates/go_to_line/src/go_to_line.rs                                       |   9 
crates/gpui/src/app.rs                                                    | 174 
crates/gpui/src/app/action.rs                                             | 111 
crates/gpui/src/gpui.rs                                                   |   3 
crates/gpui/src/keymap.rs                                                 |  97 
crates/gpui/src/platform/mac/atlas.rs                                     |  34 
crates/gpui/src/platform/mac/image_cache.rs                               |   5 
crates/gpui/src/platform/mac/renderer.rs                                  |  14 
crates/gpui/src/platform/mac/sprite_cache.rs                              |  33 
crates/gpui/src/presenter.rs                                              |  20 
crates/gpui/src/views/select.rs                                           |   4 
crates/journal/src/journal.rs                                             |   3 
crates/outline/src/outline.rs                                             |  12 
crates/project/src/project.rs                                             | 161 
crates/project_panel/src/project_panel.rs                                 |   9 
crates/project_symbols/src/project_symbols.rs                             |  11 
crates/search/Cargo.toml                                                  |   1 
crates/search/src/buffer_search.rs                                        | 115 
crates/search/src/project_search.rs                                       |  71 
crates/search/src/search.rs                                               |   8 
crates/server/.env.toml                                                   |  42 
crates/settings/Cargo.toml                                                |   2 
crates/settings/src/keymap_file.rs                                        |  62 
crates/settings/src/settings.rs                                           |   4 
crates/theme_selector/src/theme_selector.rs                               |  35 
crates/vim/Cargo.toml                                                     |   2 
crates/vim/src/insert.rs                                                  |   8 
crates/vim/src/mode.rs                                                    |   5 
crates/vim/src/normal.rs                                                  |  30 
crates/vim/src/normal/g_prefix.rs                                         |   8 
crates/vim/src/vim.rs                                                     |   3 
crates/vim/src/vim_test_context.rs                                        |   3 
crates/workspace/Cargo.toml                                               |   2 
crates/workspace/src/menu.rs                                              |  18 
crates/workspace/src/pane.rs                                              |  65 
crates/workspace/src/pane_group.rs                                        |   3 
crates/workspace/src/sidebar.rs                                           |   6 
crates/workspace/src/workspace.rs                                         |  66 
crates/zed/Cargo.toml                                                     |   1 
crates/zed/src/main.rs                                                    |  40 
crates/zed/src/settings_file.rs                                           |  34 
crates/zed/src/test.rs                                                    |   3 
crates/zed/src/zed.rs                                                     |  76 
script/build-css                                                          |   2 
script/deploy                                                             |   6 
script/seed-db                                                            |   4 
script/sqlx                                                               |   2 
script/tailwind.config.js                                                 |   4 
styles/src/buildThemes.ts                                                 |   2 
205 files changed, 1,463 insertions(+), 1,015 deletions(-)

Detailed changes

.gitignore 🔗

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

Cargo.lock 🔗

@@ -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"
@@ -1067,6 +1076,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.0.0-beta.2",
+ "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"
@@ -4300,6 +4363,7 @@ dependencies = [
  "log",
  "postage",
  "project",
+ "serde",
  "serde_json",
  "settings",
  "theme",
@@ -4464,6 +4528,8 @@ name = "settings"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "assets",
+ "collections",
  "gpui",
  "schemars",
  "serde",
@@ -5787,6 +5853,7 @@ checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
 name = "vim"
 version = "0.1.0"
 dependencies = [
+ "assets",
  "collections",
  "editor",
  "gpui",
@@ -5794,6 +5861,7 @@ dependencies = [
  "language",
  "log",
  "project",
+ "serde",
  "settings",
  "util",
  "workspace",
@@ -6061,6 +6129,7 @@ name = "zed"
 version = "0.25.0"
 dependencies = [
  "anyhow",
+ "assets",
  "async-compression",
  "async-recursion",
  "async-trait",
@@ -6136,59 +6205,6 @@ dependencies = [
  "workspace",
 ]
 
-[[package]]
-name = "zed-server"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "async-io",
- "async-sqlx-session",
- "async-std",
- "async-trait",
- "async-tungstenite",
- "base64 0.13.0",
- "clap 3.0.0-beta.2",
- "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",
- "tide",
- "tide-compress",
- "time 0.2.27",
- "toml",
- "util",
- "workspace",
-]
-
 [[package]]
 name = "zeroize"
 version = "1.3.0"

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,259 @@
+{
+    "*": {
+        "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"
+    },
+    "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",
+        "alt-shift-D": "diagnostics::Deploy",
+        "ctrl-alt-cmd-j": "journal::NewJournalEntry"
+    },
+    "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"
+    },
+    "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"
+        ]
+    },
+    "OutlineView": {
+        "escape": "outline::Toggle"
+    },
+    "ProjectSymbolsView": {
+        "escape": "project_symbols::Toggle"
+    },
+    "ThemeSelector": {
+        "escape": "theme_selector::Toggle"
+    },
+    "GoToLine": {
+        "escape": "go_to_line::Toggle",
+        "enter": "go_to_line::Confirm"
+    },
+    "FileFinder": {
+        "escape": "file_finder::Toggle"
+    },
+    "ChatPanel": {
+        "enter": "chat_panel::Send"
+    },
+    "ProjectPanel": {
+        "left": "project_panel::CollapseSelectedEntry",
+        "right": "project_panel::ExpandSelectedEntry"
+    }
+}

assets/keymaps/vim.json 🔗

@@ -0,0 +1,58 @@
+{
+    "Editor && vim_mode == insert": {
+        "escape": "vim::NormalBefore",
+        "ctrl-c": "vim::NormalBefore"
+    },
+    "Editor && vim_mode == normal && vim_submode == g": {
+        "g": "vim::MoveToStart",
+        "escape": [
+            "vim::SwitchMode",
+            {
+                "Normal": "None"
+            }
+        ]
+    },
+    "Editor && vim_mode == normal": {
+        "i": [
+            "vim::SwitchMode",
+            "Insert"
+        ],
+        "g": [
+            "vim::SwitchMode",
+            {
+                "Normal": "GPrefix"
+            }
+        ],
+        "h": "vim::MoveLeft",
+        "j": "vim::MoveDown",
+        "k": "vim::MoveUp",
+        "l": "vim::MoveRight",
+        "0": "vim::MoveToStartOfLine",
+        "shift-$": "vim::MoveToEndOfLine",
+        "shift-G": "vim::MoveToEnd",
+        "w": [
+            "vim::MoveToNextWordStart",
+            false
+        ],
+        "shift-W": [
+            "vim::MoveToNextWordStart",
+            true
+        ],
+        "e": [
+            "vim::MoveToNextWordEnd",
+            false
+        ],
+        "shift-E": [
+            "vim::MoveToNextWordEnd",
+            true
+        ],
+        "b": [
+            "vim::MoveToPreviousWordStart",
+            false
+        ],
+        "shift-B": [
+            "vim::MoveToPreviousWordStart",
+            true
+        ]
+    }
+}

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

@@ -6,7 +6,6 @@ use editor::Editor;
 use gpui::{
     actions,
     elements::*,
-    keymap::Binding,
     platform::CursorStyle,
     views::{ItemType, Select, SelectStyle},
     AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
@@ -38,8 +37,6 @@ 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/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"
@@ -67,6 +67,7 @@ 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

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

@@ -1227,6 +1227,8 @@ mod tests {
         },
         time::Duration,
     };
+    use theme::ThemeRegistry;
+    use util::TryFutureExt;
     use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
 
     #[cfg(test)]
@@ -2527,7 +2529,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() }");
         });
 
@@ -3716,7 +3718,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())
@@ -3727,7 +3734,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, _, _>(
@@ -5829,6 +5836,7 @@ mod tests {
                         project: project.clone(),
                         user_store: self.user_store.clone(),
                         languages: self.language_registry.clone(),
+                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
                         channel_list: cx.add_model(|cx| {
                             ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
                         }),

crates/diagnostics/src/diagnostics.rs 🔗

@@ -8,7 +8,7 @@ use editor::{
     highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
 };
 use gpui::{
-    actions, 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,7 @@ 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},
@@ -33,7 +34,6 @@ 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);
 }
 
@@ -92,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,6 +1,7 @@
 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 settings::Settings;
@@ -67,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/src/editor.rs 🔗

@@ -22,8 +22,7 @@ use gpui::{
     executor,
     fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::{vec2f, Vector2F},
-    impl_actions,
-    keymap::Binding,
+    impl_actions, impl_internal_actions,
     platform::CursorStyle,
     text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
@@ -66,8 +65,11 @@ const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
 
-#[derive(Clone)]
-pub struct SelectNext(pub bool);
+#[derive(Clone, Deserialize)]
+pub struct SelectNext {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
 
 #[derive(Clone)]
 pub struct GoToDiagnostic(pub Direction);
@@ -78,47 +80,38 @@ pub struct Scroll(pub Vector2F);
 #[derive(Clone)]
 pub struct Select(pub SelectPhase);
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct Input(pub String);
 
-#[derive(Clone)]
-pub struct Tab(pub Direction);
-
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SelectToBeginningOfLine {
+    #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SelectToEndOfLine {
+    #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[derive(Clone)]
-pub struct ToggleCodeActions(pub bool);
-
-#[derive(Clone)]
-pub struct ConfirmCompletion(pub Option<usize>);
+#[derive(Clone, Deserialize)]
+pub struct ToggleCodeActions {
+    #[serde(default)]
+    pub deployed_from_indicator: bool,
+}
 
-#[derive(Clone)]
-pub struct ConfirmCodeAction(pub Option<usize>);
+#[derive(Clone, Default, Deserialize)]
+pub struct ConfirmCompletion {
+    #[serde(default)]
+    pub item_ix: Option<usize>,
+}
 
-impl_actions!(
-    editor,
-    [
-        SelectNext,
-        GoToDiagnostic,
-        Scroll,
-        Select,
-        Input,
-        Tab,
-        SelectToBeginningOfLine,
-        SelectToEndOfLine,
-        ToggleCodeActions,
-        ConfirmCompletion,
-        ConfirmCodeAction,
-    ]
-);
+#[derive(Clone, Default, Deserialize)]
+pub struct ConfirmCodeAction {
+    #[serde(default)]
+    pub item_ix: Option<usize>,
+}
 
 actions!(
     editor,
@@ -127,6 +120,8 @@ actions!(
         Backspace,
         Delete,
         Newline,
+        GoToNextDiagnostic,
+        GoToPrevDiagnostic,
         Indent,
         Outdent,
         DeleteLine,
@@ -172,6 +167,8 @@ actions!(
         SplitSelectionIntoLines,
         AddSelectionAbove,
         AddSelectionBelow,
+        Tab,
+        TabPrev,
         ToggleComments,
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
@@ -193,6 +190,21 @@ actions!(
     ]
 );
 
+impl_actions!(
+    editor,
+    [
+        Input,
+        SelectNext,
+        SelectToBeginningOfLine,
+        SelectToEndOfLine,
+        ToggleCodeActions,
+        ConfirmCompletion,
+        ConfirmCodeAction,
+    ]
+);
+
+impl_internal_actions!(editor, [Scroll, Select]);
+
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
 
@@ -203,175 +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 {
-                stop_at_soft_wraps: 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 {
-                stop_at_soft_wraps: true,
-            },
-            Some("Editor"),
-        ),
-        Binding::new(
-            "cmd-shift-right",
-            SelectToEndOfLine {
-                stop_at_soft_wraps: true,
-            },
-            Some("Editor"),
-        ),
-        Binding::new(
-            "ctrl-shift-E",
-            SelectToEndOfLine {
-                stop_at_soft_wraps: 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);
@@ -381,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);
@@ -435,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);
@@ -833,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(),
                 );
@@ -959,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(),
                     );
@@ -1159,7 +1008,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(
@@ -1851,33 +1700,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"))]
@@ -2457,7 +2310,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 _;
@@ -2470,7 +2323,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)?;
 
@@ -2560,11 +2413,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(_))
@@ -2574,6 +2423,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 {
@@ -2608,7 +2458,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)?;
@@ -2619,7 +2469,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;
@@ -2846,7 +2696,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(),
             )
@@ -2940,8 +2792,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 {
@@ -3046,54 +2898,46 @@ 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 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 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);
         }
     }
 
@@ -4237,7 +4081,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);
@@ -4276,7 +4119,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)
                         {
@@ -4547,11 +4390,15 @@ impl Editor {
         self.selection_history.mode = SelectionHistoryMode::Normal;
     }
 
-    pub fn go_to_diagnostic(
-        &mut self,
-        &GoToDiagnostic(direction): &GoToDiagnostic,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic(Direction::Next, cx)
+    }
+
+    fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext<Self>) {
+        self.go_to_diagnostic(Direction::Prev, cx)
+    }
+
+    pub fn go_to_diagnostic(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         let buffer = self.buffer.read(cx).snapshot(cx);
         let selection = self.newest_selection_with_snapshot::<usize>(&buffer);
         let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
@@ -7771,7 +7618,7 @@ mod tests {
             );
 
             // indent from mid-tabstop to full tabstop
-            view.tab(&Tab(Direction::Next), cx);
+            view.tab(&Tab, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7782,7 +7629,7 @@ mod tests {
             );
 
             // outdent from 1 tabstop to 0 tabstops
-            view.tab(&Tab(Direction::Prev), cx);
+            view.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7803,7 +7650,7 @@ mod tests {
             );
 
             // indent and outdent affect only the preceding line
-            view.tab(&Tab(Direction::Next), cx);
+            view.tab(&Tab, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7812,7 +7659,7 @@ mod tests {
                     ] four"},
                 cx,
             );
-            view.tab(&Tab(Direction::Prev), cx);
+            view.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7831,7 +7678,7 @@ mod tests {
                      four"},
                 cx,
             );
-            view.tab(&Tab(Direction::Next), cx);
+            view.tab(&Tab, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7849,7 +7696,7 @@ mod tests {
                      four"},
                 cx,
             );
-            view.tab(&Tab(Direction::Prev), cx);
+            view.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 view,
                 indoc! {"
@@ -7939,7 +7786,7 @@ mod tests {
                 cx,
             );
 
-            editor.tab(&Tab(Direction::Next), cx);
+            editor.tab(&Tab, cx);
             assert_text_with_selections(
                 &mut editor,
                 indoc! {"
@@ -7950,7 +7797,7 @@ mod tests {
                 "},
                 cx,
             );
-            editor.tab(&Tab(Direction::Prev), cx);
+            editor.tab_prev(&TabPrev, cx);
             assert_text_with_selections(
                 &mut editor,
                 indoc! {"
@@ -8693,10 +8540,20 @@ mod tests {
 
         view.update(cx, |view, cx| {
             view.select_ranges([ranges[1].start + 1..ranges[1].start + 1], None, cx);
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[1..2]);
 
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[1..3]);
 
             view.undo_selection(&UndoSelection, cx);
@@ -8705,10 +8562,20 @@ mod tests {
             view.redo_selection(&RedoSelection, cx);
             assert_eq!(view.selected_ranges(cx), &ranges[1..3]);
 
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[1..4]);
 
-            view.select_next(&SelectNext(false), cx);
+            view.select_next(
+                &SelectNext {
+                    replace_newest: false,
+                },
+                cx,
+            );
             assert_eq!(view.selected_ranges(cx), &ranges[0..4]);
         });
     }
@@ -9363,7 +9230,7 @@ mod tests {
         let apply_additional_edits = editor.update(cx, |editor, cx| {
             editor.move_down(&MoveDown, cx);
             let apply_additional_edits = editor
-                .confirm_completion(&ConfirmCompletion(None), cx)
+                .confirm_completion(&ConfirmCompletion::default(), cx)
                 .unwrap();
             assert_eq!(
                 editor.text(cx),
@@ -9446,7 +9313,7 @@ mod tests {
 
         let apply_additional_edits = editor.update(cx, |editor, cx| {
             let apply_additional_edits = editor
-                .confirm_completion(&ConfirmCompletion(None), cx)
+                .confirm_completion(&ConfirmCompletion::default(), cx)
                 .unwrap();
             assert_eq!(
                 editor.text(cx),

crates/file_finder/src/file_finder.rs 🔗

@@ -1,12 +1,8 @@
 use editor::Editor;
 use fuzzy::PathMatch;
 use gpui::{
-    actions,
-    elements::*,
-    impl_actions,
-    keymap::{self, Binding},
-    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, impl_internal_actions, keymap, AppContext, Axis, Entity, ModelHandle,
+    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectPath, WorktreeId};
 use settings::Settings;
@@ -41,8 +37,8 @@ pub struct FileFinder {
 #[derive(Clone)]
 pub struct Select(pub ProjectPath);
 
-impl_actions!(file_finder, [Select]);
 actions!(file_finder, [Toggle]);
+impl_internal_actions!(file_finder, [Select]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(FileFinder::toggle);
@@ -50,11 +46,6 @@ pub fn init(cx: &mut MutableAppContext) {
     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")),
-    ]);
 }
 
 pub enum Event {

crates/go_to_line/src/go_to_line.rs 🔗

@@ -1,7 +1,7 @@
 use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
 use gpui::{
-    actions, 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};
@@ -10,11 +10,6 @@ use workspace::Workspace;
 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/src/app.rs 🔗

@@ -10,7 +10,7 @@ use crate::{
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
 };
 pub use action::*;
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use collections::btree_map;
 use keymap::MatchResult;
 use lazy_static::lazy_static;
@@ -62,6 +62,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 {
@@ -715,12 +718,14 @@ type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext
 type ObservationCallback = 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 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, DeserializeActionCallback>,
     capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
     global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
@@ -773,6 +778,7 @@ impl MutableAppContext {
                 font_cache,
                 platform,
             },
+            action_deserializers: HashMap::new(),
             capture_actions: HashMap::new(),
             actions: HashMap::new(),
             global_actions: HashMap::new(),
@@ -857,6 +863,19 @@ 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))?;
+        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,
@@ -899,6 +918,10 @@ impl MutableAppContext {
             },
         );
 
+        self.action_deserializers
+            .entry(A::qualified_name())
+            .or_insert(A::from_json_str);
+
         let actions = if capture {
             &mut self.capture_actions
         } else {
@@ -934,6 +957,10 @@ impl MutableAppContext {
             handler(action, cx);
         });
 
+        self.action_deserializers
+            .entry(A::qualified_name())
+            .or_insert(A::from_json_str);
+
         if self
             .global_actions
             .insert(TypeId::of::<A>(), handler)
@@ -1334,6 +1361,10 @@ 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,
@@ -2249,6 +2280,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)
@@ -2562,6 +2599,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
@@ -2625,6 +2663,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> {
@@ -3899,6 +3941,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 {
@@ -4575,7 +4623,8 @@ impl RefCounts {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{elements::*, impl_actions};
+    use crate::{actions, elements::*, impl_actions};
+    use serde::Deserialize;
     use smol::future::poll_once;
     use std::{
         cell::Cell,
@@ -5683,6 +5732,42 @@ mod tests {
         );
     }
 
+    #[crate::test(self)]
+    fn test_deserialize_actions(cx: &mut MutableAppContext) {
+        #[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
+        pub struct ComplexAction {
+            arg: String,
+            count: usize,
+        }
+
+        actions!(test::something, [SimpleAction]);
+        impl_actions!(test::something, [ComplexAction]);
+
+        cx.add_global_action(move |_: &SimpleAction, _: &mut MutableAppContext| {});
+        cx.add_global_action(move |_: &ComplexAction, _: &mut MutableAppContext| {});
+
+        let action1 = cx
+            .deserialize_action(
+                "test::something::ComplexAction",
+                Some(r#"{"arg": "a", "count": 5}"#),
+            )
+            .unwrap();
+        let action2 = cx
+            .deserialize_action("test::something::SimpleAction", None)
+            .unwrap();
+        assert_eq!(
+            action1.as_any().downcast_ref::<ComplexAction>().unwrap(),
+            &ComplexAction {
+                arg: "a".to_string(),
+                count: 5,
+            }
+        );
+        assert_eq!(
+            action2.as_any().downcast_ref::<SimpleAction>().unwrap(),
+            &SimpleAction
+        );
+    }
+
     #[crate::test(self)]
     fn test_dispatch_action(cx: &mut MutableAppContext) {
         struct ViewA {
@@ -5721,32 +5806,32 @@ mod tests {
             }
         }
 
-        #[derive(Clone)]
-        pub struct Action(pub &'static str);
+        #[derive(Clone, Deserialize)]
+        pub struct Action(pub String);
 
         impl_actions!(test, [Action]);
 
         let actions = Rc::new(RefCell::new(Vec::new()));
 
-        {
+        cx.add_global_action({
             let actions = actions.clone();
-            cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
+            move |_: &Action, _: &mut MutableAppContext| {
                 actions.borrow_mut().push("global".to_string());
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewA, action: &Action, cx| {
+            move |view: &mut ViewA, action: &Action, cx| {
                 assert_eq!(action.0, "bar");
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} a", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewA, _: &Action, cx| {
+            move |view: &mut ViewA, _: &Action, cx| {
                 if view.id != 1 {
                     cx.add_view(|cx| {
                         cx.propagate_action(); // Still works on a nested ViewContext
@@ -5754,32 +5839,32 @@ mod tests {
                     });
                 }
                 actions.borrow_mut().push(format!("{} b", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
+            move |view: &mut ViewB, _: &Action, cx| {
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} c", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
+            move |view: &mut ViewB, _: &Action, cx| {
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} d", view.id));
-            });
-        }
+            }
+        });
 
-        {
+        cx.capture_action({
             let actions = actions.clone();
-            cx.capture_action(move |view: &mut ViewA, _: &Action, cx| {
+            move |view: &mut ViewA, _: &Action, cx| {
                 cx.propagate_action();
                 actions.borrow_mut().push(format!("{} capture", view.id));
-            });
-        }
+            }
+        });
 
         let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 });
         let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 });
@@ -5789,7 +5874,7 @@ mod tests {
         cx.dispatch_action(
             window_id,
             vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()],
-            &Action("bar"),
+            &Action("bar".to_string()),
         );
 
         assert_eq!(
@@ -5812,7 +5897,7 @@ mod tests {
         cx.dispatch_action(
             window_id,
             vec![view_2.id(), view_3.id(), view_4.id()],
-            &Action("bar"),
+            &Action("bar".to_string()),
         );
 
         assert_eq!(
@@ -5832,8 +5917,8 @@ mod tests {
 
     #[crate::test(self)]
     fn test_dispatch_keystroke(cx: &mut MutableAppContext) {
-        #[derive(Clone)]
-        pub struct Action(pub &'static str);
+        #[derive(Clone, Deserialize)]
+        pub struct Action(String);
 
         impl_actions!(test, [Action]);
 
@@ -5887,16 +5972,20 @@ mod tests {
         // "a" and "b" in its context, but not "c".
         cx.add_bindings(vec![keymap::Binding::new(
             "a",
-            Action("a"),
+            Action("a".to_string()),
             Some("a && b && !c"),
         )]);
 
-        cx.add_bindings(vec![keymap::Binding::new("b", Action("b"), None)]);
+        cx.add_bindings(vec![keymap::Binding::new(
+            "b",
+            Action("b".to_string()),
+            None,
+        )]);
 
         let actions = Rc::new(RefCell::new(Vec::new()));
-        {
+        cx.add_action({
             let actions = actions.clone();
-            cx.add_action(move |view: &mut View, action: &Action, cx| {
+            move |view: &mut View, action: &Action, cx| {
                 if action.0 == "a" {
                     actions.borrow_mut().push(format!("{} a", view.id));
                 } else {
@@ -5905,14 +5994,15 @@ mod tests {
                         .push(format!("{} {}", view.id, action.0));
                     cx.propagate_action();
                 }
-            });
-        }
-        {
+            }
+        });
+
+        cx.add_global_action({
             let actions = actions.clone();
-            cx.add_global_action(move |action: &Action, _| {
+            move |action: &Action, _| {
                 actions.borrow_mut().push(format!("global {}", action.0));
-            });
-        }
+            }
+        });
 
         cx.dispatch_keystroke(
             window_id,

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

@@ -2,55 +2,108 @@ use std::any::{Any, TypeId};
 
 pub trait Action: 'static {
     fn id(&self) -> TypeId;
-    fn namespace(&self) -> &'static str;
     fn name(&self) -> &'static str;
     fn as_any(&self) -> &dyn Any;
     fn boxed_clone(&self) -> Box<dyn Action>;
-    fn boxed_clone_as_any(&self) -> Box<dyn Any>;
+
+    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! impl_actions {
+macro_rules! actions {
     ($namespace:path, [ $($name:ident),* $(,)? ]) => {
         $(
-            impl $crate::action::Action for $name {
-                fn id(&self) -> std::any::TypeId {
-                    std::any::TypeId::of::<$name>()
-                }
-
-                fn namespace(&self) -> &'static str {
-                    stringify!($namespace)
-                }
-
-                fn name(&self) -> &'static str {
-                    stringify!($name)
-                }
-
-                fn as_any(&self) -> &dyn std::any::Any {
-                    self
-                }
-
-                fn boxed_clone(&self) -> Box<dyn $crate::action::Action> {
-                    Box::new(self.clone())
+            #[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))
                 }
+            }
+        )*
+    };
+}
 
-                fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
-                    Box::new(self.clone())
+/// 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! actions {
+macro_rules! impl_internal_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>> {
+                    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())
+            }
 
-        $crate::impl_actions!($namespace, [ $($name),* ]);
+            $from_json_fn
+        }
     };
 }

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,5 +1,5 @@
 use crate::Action;
-use anyhow::anyhow;
+use anyhow::{anyhow, Result};
 use std::{
     any::Any,
     collections::{HashMap, HashSet},
@@ -106,6 +106,11 @@ impl Matcher {
         self.keymap.add_bindings(bindings);
     }
 
+    pub fn clear_bindings(&mut self) {
+        self.pending.clear();
+        self.keymap.clear();
+    }
+
     pub fn clear_pending(&mut self) {
         self.pending.clear();
     }
@@ -164,24 +169,34 @@ impl Keymap {
     fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
         self.0.extend(bindings.into_iter());
     }
+
+    fn clear(&mut self) {
+        self.0.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,
-        }
+        })
     }
 }
 
@@ -328,6 +343,8 @@ impl ContextPredicate {
 
 #[cfg(test)]
 mod tests {
+    use serde::Deserialize;
+
     use crate::{actions, impl_actions};
 
     use super::*;
@@ -419,30 +436,18 @@ mod tests {
 
     #[test]
     fn test_matcher() -> anyhow::Result<()> {
-        #[derive(Clone)]
-        pub struct A(pub &'static str);
+        #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+        pub struct A(pub String);
         impl_actions!(test, [A]);
         actions!(test, [B, 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, Debug, Eq, PartialEq)]
         struct ActionArg {
             a: &'static str,
         }
 
         let keymap = Keymap(vec![
-            Binding::new("a", A("x"), Some("a")),
+            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 +461,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/mac/atlas.rs 🔗

@@ -2,9 +2,9 @@ use crate::geometry::{
     rect::RectI,
     vector::{vec2i, Vector2I},
 };
-use anyhow::anyhow;
 use etagere::BucketedAtlasAllocator;
 use foreign_types::ForeignType;
+use log::warn;
 use metal::{Device, TextureDescriptor};
 use objc::{msg_send, sel, sel_impl};
 
@@ -41,36 +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)
-                    .ok_or_else(|| {
-                        anyhow!("could not allocate requested size {:?}", 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/image_cache.rs 🔗

@@ -1,3 +1,4 @@
+use anyhow::anyhow;
 use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
 
 use super::atlas::{AllocId, AtlasAllocator};
@@ -31,7 +32,9 @@ 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)
     }

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

@@ -9,6 +9,7 @@ use crate::{
     scene::{Glyph, Icon, Image, 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};
@@ -172,7 +173,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,
@@ -569,6 +577,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)

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

@@ -4,6 +4,7 @@ use crate::{
     geometry::vector::{vec2f, Vector2F, Vector2I},
     platform,
 };
+use collections::hash_map::Entry;
 use metal::{MTLPixelFormat, TextureDescriptor};
 use ordered_float::OrderedFloat;
 use std::{borrow::Cow, collections::HashMap, sync::Arc};
@@ -114,7 +115,9 @@ impl SpriteCache {
                     scale_factor,
                 )?;
 
-                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,15 +133,15 @@ 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(|| {
+        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).unwrap();
                 resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
                 let mask = pixmap
@@ -146,15 +149,15 @@ impl SpriteCache {
                     .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/presenter.rs 🔗

@@ -209,15 +209,18 @@ 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,
+                    })
                 })
-            })
+        }))
     }
 }
 
@@ -554,6 +557,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/views/select.rs 🔗

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

crates/journal/src/journal.rs 🔗

@@ -1,6 +1,6 @@
 use chrono::{Datelike, Local, Timelike};
 use editor::{Autoscroll, Editor};
-use gpui::{actions, keymap::Binding, MutableAppContext};
+use gpui::{actions, MutableAppContext};
 use std::{fs::OpenOptions, sync::Arc};
 use util::TryFutureExt as _;
 use workspace::AppState;
@@ -8,7 +8,6 @@ use workspace::AppState;
 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));
 }
 

crates/outline/src/outline.rs 🔗

@@ -4,12 +4,8 @@ use editor::{
 };
 use fuzzy::StringMatch;
 use gpui::{
-    actions,
-    elements::*,
-    geometry::vector::Vector2F,
-    keymap::{self, Binding},
-    AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    actions, elements::*, geometry::vector::Vector2F, keymap, AppContext, Axis, Entity,
+    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
@@ -23,10 +19,6 @@ use workspace::{
 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);

crates/project/src/project.rs 🔗

@@ -28,6 +28,7 @@ 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};
@@ -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,7 +154,7 @@ pub struct ProjectPath {
     pub path: Arc<Path>,
 }
 
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, PartialEq, Serialize)]
 pub struct DiagnosticSummary {
     pub error_count: usize,
     pub warning_count: usize,
@@ -467,7 +470,6 @@ impl Project {
             .and_then(|buffer| buffer.upgrade(cx))
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn languages(&self) -> &Arc<LanguageRegistry> {
         &self.languages
     }
@@ -813,13 +815,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)
@@ -2270,86 +2278,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 {
@@ -6581,7 +6584,9 @@ mod tests {
             .unwrap();
         let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 
-        let buffer = project.update(cx, |project, cx| project.create_buffer(cx).unwrap());
+        let buffer = project.update(cx, |project, cx| {
+            project.create_buffer("", None, cx).unwrap()
+        });
         buffer.update(cx, |buffer, cx| {
             buffer.edit([0..0], "abc", cx);
             assert!(buffer.is_dirty());

crates/project_panel/src/project_panel.rs 🔗

@@ -4,8 +4,7 @@ use gpui::{
         Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
         Svg, UniformList, UniformListState,
     },
-    impl_actions,
-    keymap::{self, Binding},
+    impl_internal_actions, keymap,
     platform::CursorStyle,
     AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
     ViewHandle, WeakViewHandle,
@@ -54,7 +53,7 @@ pub struct ToggleExpanded(pub ProjectEntryId);
 pub struct Open(pub ProjectEntryId);
 
 actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]);
-impl_actions!(project_panel, [Open, ToggleExpanded]);
+impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectPanel::expand_selected_entry);
@@ -63,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/src/project_symbols.rs 🔗

@@ -3,11 +3,8 @@ use editor::{
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions,
-    elements::*,
-    keymap::{self, Binding},
-    AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, keymap, AppContext, Axis, Entity, ModelHandle, MutableAppContext,
+    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use ordered_float::OrderedFloat;
 use project::{Project, Symbol};
@@ -25,10 +22,6 @@ use workspace::{
 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);

crates/search/Cargo.toml 🔗

@@ -20,6 +20,7 @@ workspace = { path = "../workspace" }
 anyhow = "1.0"
 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,55 +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::{
-    actions, elements::*, impl_actions, 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, ToolbarItemLocation, ToolbarItemView};
 
-#[derive(Clone)]
-pub struct Deploy(pub bool);
+#[derive(Clone, Deserialize)]
+pub struct Deploy {
+    pub focus: bool,
+}
 
 #[derive(Clone)]
 pub struct ToggleSearchOption(pub SearchOption);
 
 actions!(buffer_search, [Dismiss, FocusEditor]);
-impl_actions!(buffer_search, [Deploy, ToggleSearchOption]);
+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 {
@@ -325,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;
             }
         }
@@ -368,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| {
@@ -389,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, |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, |search_bar, cx| search_bar.select_match(action, cx));
+            search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
         }
     }
 
@@ -699,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)]
@@ -710,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)]
@@ -721,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)]
@@ -732,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)]
@@ -743,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)]
@@ -754,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)]
@@ -765,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)]
@@ -782,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)]
@@ -799,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)]
@@ -816,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)]
@@ -833,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)]
@@ -850,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,13 +1,13 @@
 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::{
-    actions, 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;
@@ -28,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);
 }
@@ -136,6 +128,7 @@ impl ProjectSearch {
 
 pub enum ViewEvent {
     UpdateTab,
+    EditorEvent(editor::Event),
 }
 
 impl Entity for ProjectSearchView {
@@ -177,11 +170,7 @@ 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 {
-            self.focus_results_editor(cx);
-        }
+        self.focus_query_editor(cx);
     }
 }
 
@@ -304,6 +293,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)
     }
@@ -338,6 +335,11 @@ 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();
 
         let results_editor = cx.add_view(|cx| {
             let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
@@ -350,6 +352,8 @@ impl ProjectSearchView {
             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();
 
@@ -390,6 +394,7 @@ 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(
@@ -545,18 +550,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();
         }
@@ -635,7 +645,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::{impl_actions, MutableAppContext};
+use gpui::{actions, impl_internal_actions, MutableAppContext};
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
 use std::{
     cmp::{self, Ordering},
@@ -18,10 +18,8 @@ pub fn init(cx: &mut MutableAppContext) {
 #[derive(Clone)]
 pub struct ToggleSearchOption(pub SearchOption);
 
-#[derive(Clone)]
-pub struct SelectMatch(pub Direction);
-
-impl_actions!(search, [ToggleSearchOption, SelectMatch]);
+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 🔗

@@ -11,6 +11,8 @@ doctest = false
 test-support = []
 
 [dependencies]
+assets = { path = "../assets" }
+collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 theme = { path = "../theme" }
 util = { path = "../util" }

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 🔗

@@ -1,3 +1,5 @@
+mod keymap_file;
+
 use anyhow::Result;
 use gpui::font_cache::{FamilyId, FontCache};
 use schemars::{
@@ -13,6 +15,8 @@ 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,

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,11 +1,8 @@
 use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    elements::*,
-    impl_actions,
-    keymap::{self, Binding},
-    AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
-    ViewContext, ViewHandle,
+    actions, elements::*, keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext,
+    RenderContext, View, ViewContext, ViewHandle,
 };
 use settings::Settings;
 use std::{cmp, sync::Arc};
@@ -25,26 +22,14 @@ pub struct ThemeSelector {
     selection_completed: bool,
 }
 
-#[derive(Clone)]
-pub struct Toggle(pub Arc<ThemeRegistry>);
+actions!(theme_selector, [Toggle, Reload]);
 
-#[derive(Clone)]
-pub struct Reload(pub Arc<ThemeRegistry>);
-
-impl_actions!(theme_selector, [Toggle, Reload]);
-
-pub fn init(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
+pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ThemeSelector::confirm);
     cx.add_action(ThemeSelector::select_prev);
     cx.add_action(ThemeSelector::select_next);
     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")),
-    ]);
 }
 
 pub enum Event {
@@ -79,18 +64,20 @@ impl ThemeSelector {
         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));
+            let selector = cx.add_view(|cx| Self::new(themes, cx));
             cx.subscribe(&selector, Self::on_event).detach();
             selector
         });
     }
 
-    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);

crates/vim/Cargo.toml 🔗

@@ -8,10 +8,12 @@ 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 = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/vim/src/insert.rs 🔗

@@ -1,18 +1,12 @@
 use crate::{mode::Mode, SwitchMode, VimState};
 use editor::Bias;
-use gpui::{actions, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
 
 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);
 }
 

crates/vim/src/mode.rs 🔗

@@ -1,7 +1,8 @@
 use editor::CursorShape;
 use gpui::keymap::Context;
+use serde::Deserialize;
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
 pub enum Mode {
     Normal(NormalState),
     Insert,
@@ -44,7 +45,7 @@ impl Default for Mode {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
 pub enum NormalState {
     None,
     GPrefix,

crates/vim/src/normal.rs 🔗

@@ -1,18 +1,19 @@
 mod g_prefix;
 
-use crate::{mode::NormalState, Mode, SwitchMode, VimState};
+use crate::VimState;
 use editor::{char_kind, movement, Bias};
-use gpui::{actions, impl_actions, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
 use language::SelectionGoal;
+use serde::Deserialize;
 use workspace::Workspace;
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 struct MoveToNextWordStart(pub bool);
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 struct MoveToNextWordEnd(pub bool);
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 struct MoveToPreviousWordStart(pub bool);
 
 impl_actions!(
@@ -39,26 +40,7 @@ actions!(
 );
 
 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);

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

@@ -1,16 +1,10 @@
 use crate::{mode::Mode, SwitchMode, VimState};
-use gpui::{actions, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, MutableAppContext, ViewContext};
 use workspace::Workspace;
 
 actions!(vim, [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);
 }
 

crates/vim/src/vim.rs 🔗

@@ -8,12 +8,13 @@ mod vim_test_context;
 use collections::HashMap;
 use editor::{CursorShape, Editor};
 use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
+use serde::Deserialize;
 
 use mode::Mode;
 use settings::Settings;
 use workspace::{self, Workspace};
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct SwitchMode(pub Mode);
 
 impl_actions!(vim, [SwitchMode]);

crates/vim/src/vim_test_context.rs 🔗

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

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

crates/workspace/src/menu.rs 🔗

@@ -1,18 +1,4 @@
-use gpui::{actions, keymap::Binding, MutableAppContext};
-
-actions!(
+gpui::actions!(
     menu,
-    [Confirm, SelectPrev, SelectNext, SelectFirst, SelectLast,]
+    [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")),
-    ]);
-}

crates/workspace/src/pane.rs 🔗

@@ -7,13 +7,13 @@ use gpui::{
     actions,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    impl_actions,
-    keymap::Binding,
+    impl_actions, impl_internal_actions,
     platform::{CursorStyle, NavigationDirection},
     AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
 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;
@@ -28,29 +28,33 @@ actions!(
     ]
 );
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct Split(pub SplitDirection);
 
 #[derive(Clone)]
-pub struct CloseItem(pub CloseItemParams);
+pub struct CloseItem {
+    pub item_id: usize,
+    pub pane: WeakViewHandle<Pane>,
+}
 
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
 pub struct ActivateItem(pub usize);
 
-#[derive(Clone)]
-pub struct GoBack(pub Option<WeakViewHandle<Pane>>);
-
-#[derive(Clone)]
-pub struct GoForward(pub Option<WeakViewHandle<Pane>>);
-
-impl_actions!(pane, [Split, CloseItem, ActivateItem, GoBack, GoForward,]);
+#[derive(Clone, Deserialize)]
+pub struct GoBack {
+    #[serde(skip_deserializing)]
+    pub pane: Option<WeakViewHandle<Pane>>,
+}
 
-#[derive(Clone)]
-pub struct CloseItemParams {
-    pub item_id: usize,
-    pub pane: 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;
 
 pub fn init(cx: &mut MutableAppContext) {
@@ -66,8 +70,8 @@ pub fn init(cx: &mut MutableAppContext) {
     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.0.pane.upgrade(cx)?;
-        Some(Pane::close_item(workspace, pane, action.0.item_id, 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);
@@ -76,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,
@@ -87,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 {
@@ -747,10 +738,10 @@ impl Pane {
                                             .on_click({
                                                 let pane = pane.clone();
                                                 move |cx| {
-                                                    cx.dispatch_action(CloseItem(CloseItemParams {
+                                                    cx.dispatch_action(CloseItem {
                                                         item_id,
                                                         pane: pane.clone(),
-                                                    }))
+                                                    })
                                                 }
                                             })
                                             .named("close-tab-icon")
@@ -816,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

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

@@ -1,5 +1,7 @@
 use super::Workspace;
-use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
+use gpui::{
+    elements::*, impl_internal_actions, platform::CursorStyle, AnyViewHandle, RenderContext,
+};
 use std::{cell::RefCell, rc::Rc};
 use theme::Theme;
 
@@ -27,7 +29,7 @@ pub struct ToggleSidebarItem(pub SidebarItemId);
 #[derive(Clone)]
 pub struct ToggleSidebarItemFocus(pub SidebarItemId);
 
-impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+impl_internal_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
 
 #[derive(Clone)]
 pub struct SidebarItemId {

crates/workspace/src/workspace.rs 🔗

@@ -17,13 +17,12 @@ use gpui::{
     color::Color,
     elements::*,
     geometry::{rect::RectF, vector::vec2f, PathBuilder},
-    impl_actions,
-    json::{self, to_string_pretty, ToJson},
-    keymap::Binding,
+    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;
@@ -32,7 +31,7 @@ pub use pane_group::*;
 use postage::prelude::Stream;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
 use settings::Settings;
-use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
+use sidebar::{Side, Sidebar, ToggleSidebarItem, ToggleSidebarItemFocus};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
@@ -76,7 +75,6 @@ actions!(
         ToggleShare,
         Unfollow,
         Save,
-        DebugElements,
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
@@ -101,14 +99,13 @@ pub struct ToggleFollow(pub PeerId);
 #[derive(Clone)]
 pub struct JoinProject(pub JoinProjectParams);
 
-impl_actions!(
+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| {
@@ -135,7 +132,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| {
@@ -144,29 +140,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);
@@ -630,6 +603,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>,
 }
@@ -659,6 +633,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,
@@ -677,6 +652,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(),
@@ -694,6 +670,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,
@@ -802,6 +779,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(),
@@ -834,6 +812,10 @@ impl Workspace {
         &self.project
     }
 
+    pub fn themes(&self) -> Arc<ThemeRegistry> {
+        self.themes.clone()
+    }
+
     pub fn worktrees<'a>(
         &self,
         cx: &'a AppContext,
@@ -1069,22 +1051,6 @@ 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(cx));
         let pane_id = pane.id();

crates/zed/Cargo.toml 🔗

@@ -29,6 +29,7 @@ test-support = [
 ]
 
 [dependencies]
+assets = { path = "../assets" }
 breadcrumbs = { path = "../breadcrumbs" }
 chat_panel = { path = "../chat_panel" }
 collections = { path = "../collections" }

crates/zed/src/main.rs 🔗

@@ -2,6 +2,7 @@
 #![allow(non_snake_case)]
 
 use anyhow::{anyhow, Context, Result};
+use assets::Assets;
 use client::{self, http, ChannelList, UserStore};
 use fs::OpenOptions;
 use futures::{channel::oneshot, StreamExt};
@@ -9,19 +10,17 @@ use gpui::{App, AssetSource, Task};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
-use settings::{self, Settings};
+use settings::{self, KeymapFile, Settings, SettingsFileContent};
 use smol::process::Command;
 use std::{env, fs, path::PathBuf, sync::Arc};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::ResultExt;
 use workspace::{self, AppState, OpenNew, OpenPaths};
 use zed::{
-    self,
-    assets::Assets,
-    build_window_options, build_workspace,
+    self, build_window_options, build_workspace,
     fs::RealFs,
     languages, menus,
-    settings_file::{settings_from_files, SettingsFile},
+    settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
 };
 
 fn main() {
@@ -62,8 +61,16 @@ fn main() {
                 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(())
@@ -112,13 +119,16 @@ fn main() {
         })
         .detach_and_log_err(cx);
 
-        let settings_file = cx.background().block(settings_file).unwrap();
+        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 {
@@ -145,8 +155,8 @@ fn main() {
             build_workspace: &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()));
 
@@ -254,14 +264,22 @@ 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

crates/zed/src/settings_file.rs 🔗

@@ -1,17 +1,21 @@
 use futures::{stream, StreamExt};
-use gpui::{executor, FontCache};
+use gpui::{executor, AsyncAppContext, FontCache};
 use postage::sink::Sink as _;
 use postage::{prelude::Stream, watch};
 use project::Fs;
-use settings::{Settings, SettingsFileContent};
+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 SettingsFile(watch::Receiver<SettingsFileContent>);
+pub struct WatchedJsonFile<T>(watch::Receiver<T>);
 
-impl SettingsFile {
+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,
@@ -35,21 +39,21 @@ impl SettingsFile {
         Self(rx)
     }
 
-    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
+    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(SettingsFileContent::default())
+            Some(T::default())
         }
     }
 }
 
 pub fn settings_from_files(
     defaults: Settings,
-    sources: Vec<SettingsFile>,
+    sources: Vec<WatchedJsonFile<SettingsFileContent>>,
     theme_registry: Arc<ThemeRegistry>,
     font_cache: Arc<FontCache>,
 ) -> impl futures::stream::Stream<Item = Settings> {
@@ -72,6 +76,16 @@ pub fn settings_from_files(
     })
 }
 
+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::*;
@@ -102,9 +116,9 @@ mod tests {
         .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 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),

crates/zed/src/test.rs 🔗

@@ -1,4 +1,5 @@
-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;

crates/zed/src/zed.rs 🔗

@@ -1,4 +1,3 @@
-pub mod assets;
 pub mod languages;
 pub mod menus;
 pub mod settings_file;
@@ -11,11 +10,10 @@ pub use client;
 pub use contacts_panel;
 use contacts_panel::ContactsPanel;
 pub use editor;
+use editor::Editor;
 use gpui::{
     actions,
     geometry::vector::vec2f,
-    impl_actions,
-    keymap::Binding,
     platform::{WindowBounds, WindowOptions},
     ModelHandle, ViewContext,
 };
@@ -25,17 +23,24 @@ use project::Project;
 pub use project::{self, fs};
 use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
+use serde_json::to_string_pretty;
 use settings::Settings;
 use std::{path::PathBuf, sync::Arc};
+use util::ResultExt;
 pub use workspace;
 use workspace::{AppState, Workspace, WorkspaceParams};
 
-actions!(zed, [About, Quit, OpenSettings]);
-
-#[derive(Clone)]
-pub struct AdjustBufferFontSize(pub f32);
-
-impl_actions!(zed, [AdjustBufferFontSize]);
+actions!(
+    zed,
+    [
+        About,
+        Quit,
+        DebugElements,
+        OpenSettings,
+        IncreaseBufferFontSize,
+        DecreaseBufferFontSize
+    ]
+);
 
 const MIN_FONT_SIZE: f32 = 6.0;
 
@@ -44,20 +49,23 @@ 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({
-        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_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
@@ -96,14 +104,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(
@@ -134,6 +160,7 @@ 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(),
     };
@@ -205,9 +232,8 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 
 #[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};

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/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/src/buildThemes.ts 🔗

@@ -10,7 +10,7 @@ for (let theme of themes) {
   let styleTree = snakeCase(app(theme));
   let styleTreeJSON = JSON.stringify(styleTree, null, 2);
   let outPath = path.resolve(
-    `${__dirname}/../../crates/zed/assets/themes/${theme.name}.json`
+    `${__dirname}/../assets/themes/${theme.name}.json`
   );
   fs.writeFileSync(outPath, styleTreeJSON);
   console.log(`- ${outPath} created`);