.gitignore 🔗
@@ -2,6 +2,7 @@
/zed.xcworkspace
.DS_Store
/script/node_modules
-/crates/server/.env.toml
-/crates/server/static/styles.css
+/styles/node_modules
+/crates/collab/.env.toml
+/crates/collab/static/styles.css
/vendor/bin
Antonio Scandurra created
.gitignore | 5
.zed.toml | 2
Cargo.lock | 442
Dockerfile | 14
Dockerfile.migrator | 2
Procfile | 2
README.md | 2
assets/fonts/zed-mono/zed-mono-extended.ttf | 0
assets/fonts/zed-mono/zed-mono-extendedbold.ttf | 0
assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf | 0
assets/fonts/zed-mono/zed-mono-extendeditalic.ttf | 0
assets/fonts/zed-sans/zed-sans-extended.ttf | 0
assets/fonts/zed-sans/zed-sans-extendedbold.ttf | 0
assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf | 0
assets/fonts/zed-sans/zed-sans-extendeditalic.ttf | 0
assets/icons/broadcast-24.svg | 0
assets/icons/comment-16.svg | 0
assets/icons/diagnostic-error-10.svg | 0
assets/icons/diagnostic-summary-error.svg | 0
assets/icons/diagnostic-summary-warning.svg | 0
assets/icons/diagnostic-warning-10.svg | 0
assets/icons/disclosure-closed.svg | 0
assets/icons/disclosure-open.svg | 0
assets/icons/file-16.svg | 0
assets/icons/folder-tree-16.svg | 0
assets/icons/magnifier.svg | 0
assets/icons/offline-14.svg | 0
assets/icons/signed-out-12.svg | 0
assets/icons/user-16.svg | 0
assets/icons/x.svg | 0
assets/icons/zap.svg | 0
assets/keymaps/default.json | 264
assets/keymaps/vim.json | 93
assets/themes/dark.json | 1338
assets/themes/light.json | 1338
crates/assets/Cargo.toml | 14
crates/assets/src/assets.rs | 2
crates/auto_update/Cargo.toml | 3
crates/auto_update/src/auto_update.rs | 20
crates/breadcrumbs/Cargo.toml | 1
crates/breadcrumbs/src/breadcrumbs.rs | 3
crates/chat_panel/Cargo.toml | 1
crates/chat_panel/src/chat_panel.rs | 10
crates/cli/Cargo.toml | 24
crates/cli/src/cli.rs | 22
crates/cli/src/main.rs | 124
crates/client/Cargo.toml | 2
crates/client/src/client.rs | 4
crates/client/src/test.rs | 6
crates/collab/.env.template.toml | 0
crates/collab/Cargo.toml | 14
crates/collab/Procfile | 2
crates/collab/README.md | 0
crates/collab/basic.conf | 0
crates/collab/favicon.ico | 0
crates/collab/k8s/environments/production.sh | 0
crates/collab/k8s/environments/staging.sh | 0
crates/collab/k8s/manifest.template.yml | 18
crates/collab/k8s/migrate.template.yml | 0
crates/collab/migrations/20210527024318_initial_schema.sql | 0
crates/collab/migrations/20210607190313_create_access_tokens.sql | 0
crates/collab/migrations/20210805175147_create_chat_tables.sql | 0
crates/collab/migrations/20210916123647_add_nonce_to_channel_messages.sql | 0
crates/collab/migrations/20210920192001_add_interests_to_signups.sql | 0
crates/collab/src/admin.rs | 0
crates/collab/src/api.rs | 8
crates/collab/src/assets.rs | 0
crates/collab/src/auth.rs | 0
crates/collab/src/bin/dotenv.rs | 0
crates/collab/src/bin/seed.rs | 0
crates/collab/src/careers.rs | 0
crates/collab/src/community.rs | 0
crates/collab/src/db.rs | 0
crates/collab/src/env.rs | 0
crates/collab/src/errors.rs | 0
crates/collab/src/expiring.rs | 0
crates/collab/src/github.rs | 0
crates/collab/src/home.rs | 9
crates/collab/src/main.rs | 8
crates/collab/src/releases.rs | 0
crates/collab/src/rpc.rs | 454
crates/collab/src/rpc/store.rs | 48
crates/collab/src/team.rs | 0
crates/collab/static/browserconfig.xml | 0
crates/collab/static/fonts/VisbyCF-Bold.eot | 0
crates/collab/static/fonts/VisbyCF-Bold.woff | 0
crates/collab/static/fonts/VisbyCF-Bold.woff2 | 0
crates/collab/static/fonts/VisbyCF-BoldOblique.eot | 0
crates/collab/static/fonts/VisbyCF-BoldOblique.woff | 0
crates/collab/static/fonts/VisbyCF-BoldOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-DemiBold.eot | 0
crates/collab/static/fonts/VisbyCF-DemiBold.woff | 0
crates/collab/static/fonts/VisbyCF-DemiBold.woff2 | 0
crates/collab/static/fonts/VisbyCF-DemiBoldOblique.eot | 0
crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff | 0
crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-ExtraBold.eot | 0
crates/collab/static/fonts/VisbyCF-ExtraBold.woff | 0
crates/collab/static/fonts/VisbyCF-ExtraBold.woff2 | 0
crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.eot | 0
crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff | 0
crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-Heavy.eot | 0
crates/collab/static/fonts/VisbyCF-Heavy.woff | 0
crates/collab/static/fonts/VisbyCF-Heavy.woff2 | 0
crates/collab/static/fonts/VisbyCF-HeavyOblique.eot | 0
crates/collab/static/fonts/VisbyCF-HeavyOblique.woff | 0
crates/collab/static/fonts/VisbyCF-HeavyOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-Light.eot | 0
crates/collab/static/fonts/VisbyCF-Light.woff | 0
crates/collab/static/fonts/VisbyCF-Light.woff2 | 0
crates/collab/static/fonts/VisbyCF-LightOblique.eot | 0
crates/collab/static/fonts/VisbyCF-LightOblique.woff | 0
crates/collab/static/fonts/VisbyCF-LightOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-Medium.eot | 0
crates/collab/static/fonts/VisbyCF-Medium.woff | 0
crates/collab/static/fonts/VisbyCF-Medium.woff2 | 0
crates/collab/static/fonts/VisbyCF-MediumOblique.eot | 0
crates/collab/static/fonts/VisbyCF-MediumOblique.woff | 0
crates/collab/static/fonts/VisbyCF-MediumOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-Regular.eot | 0
crates/collab/static/fonts/VisbyCF-Regular.woff | 0
crates/collab/static/fonts/VisbyCF-Regular.woff2 | 0
crates/collab/static/fonts/VisbyCF-RegularOblique.eot | 0
crates/collab/static/fonts/VisbyCF-RegularOblique.woff | 0
crates/collab/static/fonts/VisbyCF-RegularOblique.woff2 | 0
crates/collab/static/fonts/VisbyCF-Thin.eot | 0
crates/collab/static/fonts/VisbyCF-Thin.woff | 0
crates/collab/static/fonts/VisbyCF-Thin.woff2 | 0
crates/collab/static/fonts/VisbyCF-ThinOblique.eot | 0
crates/collab/static/fonts/VisbyCF-ThinOblique.woff | 0
crates/collab/static/fonts/VisbyCF-ThinOblique.woff2 | 0
crates/collab/static/images/android-chrome-192x192.png | 0
crates/collab/static/images/android-chrome-512x512.png | 0
crates/collab/static/images/apple-touch-icon.png | 0
crates/collab/static/images/favicon-16x16.png | 0
crates/collab/static/images/favicon-32x32.png | 0
crates/collab/static/images/favicon.png | 0
crates/collab/static/images/favicon.svg | 0
crates/collab/static/images/mstile-144x144.png | 0
crates/collab/static/images/mstile-150x150.png | 0
crates/collab/static/images/mstile-310x150.png | 0
crates/collab/static/images/mstile-310x310.png | 0
crates/collab/static/images/mstile-70x70.png | 0
crates/collab/static/images/safari-pinned-tab.svg | 0
crates/collab/static/images/zed-og-image.png | 0
crates/collab/static/images/zed-twitter-image.png | 0
crates/collab/static/prism.js | 0
crates/collab/static/prose.css | 0
crates/collab/static/prose.css.map | 0
crates/collab/static/prose.scss | 0
crates/collab/static/site.webmanifest | 0
crates/collab/static/svg/hero.svg | 0
crates/collab/styles.css | 0
crates/collab/templates/admin.hbs | 0
crates/collab/templates/careers.hbs | 0
crates/collab/templates/community.hbs | 0
crates/collab/templates/docs.hbs | 0
crates/collab/templates/error.hbs | 0
crates/collab/templates/home.hbs | 0
crates/collab/templates/partials/layout.hbs | 0
crates/collab/templates/releases.hbs | 0
crates/collab/templates/signup.hbs | 0
crates/collab/templates/team.hbs | 0
crates/command_palette/Cargo.toml | 26
crates/command_palette/src/command_palette.rs | 362
crates/contacts_panel/Cargo.toml | 1
crates/contacts_panel/src/contacts_panel.rs | 10
crates/diagnostics/Cargo.toml | 1
crates/diagnostics/src/diagnostics.rs | 34
crates/diagnostics/src/items.rs | 10
crates/editor/Cargo.toml | 5
crates/editor/src/context_menu.rs | 272
crates/editor/src/display_map.rs | 102
crates/editor/src/display_map/block_map.rs | 17
crates/editor/src/display_map/fold_map.rs | 5
crates/editor/src/display_map/tab_map.rs | 48
crates/editor/src/display_map/wrap_map.rs | 45
crates/editor/src/editor.rs | 659
crates/editor/src/element.rs | 6
crates/editor/src/items.rs | 20
crates/editor/src/movement.rs | 11
crates/editor/src/multi_buffer.rs | 70
crates/editor/src/test.rs | 25
crates/file_finder/Cargo.toml | 2
crates/file_finder/src/file_finder.rs | 390
crates/go_to_line/Cargo.toml | 1
crates/go_to_line/src/go_to_line.rs | 15
crates/gpui/Cargo.toml | 4
crates/gpui/examples/text.rs | 1
crates/gpui/src/app.rs | 517
crates/gpui/src/app/action.rs | 109
crates/gpui/src/elements.rs | 10
crates/gpui/src/elements/align.rs | 3
crates/gpui/src/elements/canvas.rs | 1
crates/gpui/src/elements/constrained_box.rs | 1
crates/gpui/src/elements/container.rs | 1
crates/gpui/src/elements/empty.rs | 1
crates/gpui/src/elements/event_handler.rs | 11
crates/gpui/src/elements/expanded.rs | 1
crates/gpui/src/elements/flex.rs | 98
crates/gpui/src/elements/hook.rs | 1
crates/gpui/src/elements/image.rs | 1
crates/gpui/src/elements/label.rs | 1
crates/gpui/src/elements/list.rs | 2
crates/gpui/src/elements/mouse_event_handler.rs | 7
crates/gpui/src/elements/overlay.rs | 1
crates/gpui/src/elements/stack.rs | 1
crates/gpui/src/elements/svg.rs | 1
crates/gpui/src/elements/text.rs | 1
crates/gpui/src/elements/uniform_list.rs | 16
crates/gpui/src/executor.rs | 44
crates/gpui/src/gpui.rs | 3
crates/gpui/src/keymap.rs | 162
crates/gpui/src/platform.rs | 15
crates/gpui/src/platform/event.rs | 17
crates/gpui/src/platform/mac/atlas.rs | 28
crates/gpui/src/platform/mac/fonts.rs | 207
crates/gpui/src/platform/mac/image_cache.rs | 78
crates/gpui/src/platform/mac/platform.rs | 86
crates/gpui/src/platform/mac/renderer.rs | 59
crates/gpui/src/platform/mac/sprite_cache.rs | 38
crates/gpui/src/platform/mac/window.rs | 3
crates/gpui/src/platform/test.rs | 8
crates/gpui/src/presenter.rs | 45
crates/gpui/src/scene.rs | 36
crates/gpui/src/text_layout.rs | 47
crates/gpui/src/views/select.rs | 13
crates/gpui_macros/src/gpui_macros.rs | 99
crates/journal/Cargo.toml | 2
crates/journal/src/journal.rs | 7
crates/language/Cargo.toml | 3
crates/language/src/buffer.rs | 68
crates/language/src/language.rs | 8
crates/language/src/tests.rs | 121
crates/lsp/Cargo.toml | 2
crates/lsp/src/lsp.rs | 3
crates/outline/Cargo.toml | 2
crates/outline/src/outline.rs | 239
crates/picker/Cargo.toml | 23
crates/picker/src/picker.rs | 277
crates/project/Cargo.toml | 3
crates/project/src/project.rs | 726
crates/project/src/worktree.rs | 90
crates/project_panel/Cargo.toml | 1
crates/project_panel/src/project_panel.rs | 23
crates/project_symbols/Cargo.toml | 10
crates/project_symbols/src/project_symbols.rs | 474
crates/rpc/Cargo.toml | 13
crates/rpc/proto/zed.proto | 2
crates/rpc/src/conn.rs | 36
crates/rpc/src/peer.rs | 54
crates/rpc/src/rpc.rs | 2
crates/search/Cargo.toml | 4
crates/search/src/buffer_search.rs | 128
crates/search/src/project_search.rs | 100
crates/search/src/search.rs | 9
crates/server/.env.toml | 42
crates/settings/Cargo.toml | 24
crates/settings/src/keymap_file.rs | 62
crates/settings/src/settings.rs | 262
crates/sum_tree/Cargo.toml | 2
crates/text/Cargo.toml | 2
crates/text/src/text.rs | 5
crates/theme/src/resolution.rs | 497
crates/theme/src/theme.rs | 12
crates/theme/src/theme_registry.rs | 234
crates/theme_selector/Cargo.toml | 4
crates/theme_selector/src/theme_selector.rs | 359
crates/util/Cargo.toml | 2
crates/vim/Cargo.toml | 8
crates/vim/src/editor_events.rs | 24
crates/vim/src/insert.rs | 23
crates/vim/src/mode.rs | 72
crates/vim/src/motion.rs | 271
crates/vim/src/normal.rs | 776
crates/vim/src/normal/g_prefix.rs | 82
crates/vim/src/state.rs | 82
crates/vim/src/vim.rs | 95
crates/vim/src/vim_test_context.rs | 40
crates/workspace/Cargo.toml | 7
crates/workspace/src/lsp_status.rs | 11
crates/workspace/src/menu.rs | 31
crates/workspace/src/pane.rs | 496
crates/workspace/src/pane_group.rs | 3
crates/workspace/src/settings.rs | 325
crates/workspace/src/sidebar.rs | 16
crates/workspace/src/status_bar.rs | 3
crates/workspace/src/toolbar.rs | 3
crates/workspace/src/workspace.rs | 230
crates/zed/Cargo.toml | 10
crates/zed/assets/themes/_base.toml | 413
crates/zed/assets/themes/black.toml | 67
crates/zed/assets/themes/dark.toml | 67
crates/zed/assets/themes/light.toml | 67
crates/zed/src/languages.rs | 5
crates/zed/src/languages/javascript/brackets.scm | 5
crates/zed/src/languages/javascript/config.toml | 12
crates/zed/src/languages/javascript/highlights.scm | 219
crates/zed/src/languages/javascript/indents.scm | 15
crates/zed/src/languages/javascript/outline.scm | 55
crates/zed/src/languages/tsx/config.toml | 2
crates/zed/src/main.rs | 233
crates/zed/src/menus.rs | 8
crates/zed/src/settings_file.rs | 171
crates/zed/src/test.rs | 5
crates/zed/src/zed.rs | 219
script/build-css | 2
script/build-themes | 7
script/bundle | 30
script/deploy | 6
script/seed-db | 4
script/sqlx | 2
script/tailwind.config.js | 4
styles/.gitignore | 1
styles/dist/core.json | 1155
styles/dist/dark.json | 681
styles/dist/light.json | 681
styles/dist/tokens.json | 2519
styles/nodemon.json | 8
styles/package-lock.json | 2321
styles/package.json | 22
styles/src/buildThemes.ts | 17
styles/src/buildTokens.ts | 110
styles/src/styleTree/app.ts | 45
styles/src/styleTree/chatPanel.ts | 108
styles/src/styleTree/commandPalette.ts | 23
styles/src/styleTree/components.ts | 93
styles/src/styleTree/contactsPanel.ts | 62
styles/src/styleTree/editor.ts | 146
styles/src/styleTree/projectPanel.ts | 37
styles/src/styleTree/search.ts | 84
styles/src/styleTree/selectorModal.ts | 59
styles/src/styleTree/workspace.ts | 158
styles/src/themes/dark.ts | 241
styles/src/themes/light.ts | 239
styles/src/themes/theme.ts | 147
styles/src/tokens.ts | 102
styles/src/utils/color.ts | 52
styles/src/utils/snakeCase.ts | 35
styles/tsconfig.json | 14
341 files changed, 20,293 insertions(+), 5,244 deletions(-)
@@ -2,6 +2,7 @@
/zed.xcworkspace
.DS_Store
/script/node_modules
-/crates/server/.env.toml
-/crates/server/static/styles.css
+/styles/node_modules
+/crates/collab/.env.toml
+/crates/collab/static/styles.css
/vendor/bin
@@ -1 +1 @@
-collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "Kethku"]
+collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "gibusu", "Kethku"]
@@ -135,7 +135,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -168,6 +168,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+[[package]]
+name = "assets"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui",
+ "rust-embed",
+]
+
[[package]]
name = "async-attributes"
version = "1.1.2"
@@ -300,7 +309,7 @@ dependencies = [
"polling",
"vec-arena",
"waker-fn",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -355,7 +364,7 @@ dependencies = [
"futures-lite",
"once_cell",
"signal-hook",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -449,7 +458,7 @@ dependencies = [
"async-io",
"async-lock",
"async-process",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
"futures-channel",
"futures-core",
"futures-io",
@@ -541,7 +550,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -555,6 +564,7 @@ dependencies = [
"log",
"serde",
"serde_json",
+ "settings",
"smol",
"surf",
"tempdir",
@@ -632,7 +642,7 @@ dependencies = [
"cexpr",
"clang-sys",
"clap 2.33.3",
- "env_logger",
+ "env_logger 0.8.3",
"lazy_static",
"lazycell",
"log",
@@ -747,6 +757,7 @@ dependencies = [
"language",
"project",
"search",
+ "settings",
"theme",
"workspace",
]
@@ -899,6 +910,7 @@ dependencies = [
"editor",
"gpui",
"postage",
+ "settings",
"theme",
"time 0.3.7",
"util",
@@ -916,7 +928,7 @@ dependencies = [
"num-traits",
"serde",
"time 0.1.44",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -971,9 +983,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "3.0.0-beta.2"
+version = "3.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
+checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c"
dependencies = [
"atty",
"bitflags",
@@ -983,24 +995,36 @@ dependencies = [
"os_str_bytes",
"strsim 0.10.0",
"termcolor",
- "textwrap 0.12.1",
- "unicode-width",
- "vec_map",
+ "textwrap 0.15.0",
]
[[package]]
name = "clap_derive"
-version = "3.0.0-beta.2"
+version = "3.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
+checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
dependencies = [
- "heck",
+ "heck 0.4.0",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap 3.1.8",
+ "core-foundation",
+ "core-services",
+ "dirs 3.0.1",
+ "ipc-channel",
+ "plist",
+ "serde",
+]
+
[[package]]
name = "client"
version = "0.1.0"
@@ -1083,6 +1107,60 @@ dependencies = [
"objc",
]
+[[package]]
+name = "collab"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-io",
+ "async-sqlx-session",
+ "async-std",
+ "async-trait",
+ "async-tungstenite",
+ "base64 0.13.0",
+ "clap 3.1.8",
+ "client",
+ "collections",
+ "comrak",
+ "ctor",
+ "editor",
+ "either",
+ "env_logger 0.8.3",
+ "envy",
+ "futures",
+ "gpui",
+ "handlebars",
+ "http-auth-basic",
+ "json_env_logger",
+ "jwt-simple",
+ "language",
+ "lazy_static",
+ "lipsum",
+ "log",
+ "lsp",
+ "oauth2",
+ "oauth2-surf",
+ "parking_lot",
+ "project",
+ "rand 0.8.3",
+ "rpc",
+ "rust-embed",
+ "scrypt",
+ "serde",
+ "serde_json",
+ "settings",
+ "sha-1 0.9.6",
+ "sqlx 0.5.5",
+ "surf",
+ "theme",
+ "tide",
+ "tide-compress",
+ "time 0.2.27",
+ "toml",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "collections"
version = "0.1.0"
@@ -1096,6 +1174,23 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+[[package]]
+name = "command_palette"
+version = "0.1.0"
+dependencies = [
+ "ctor",
+ "editor",
+ "env_logger 0.8.3",
+ "fuzzy",
+ "gpui",
+ "picker",
+ "serde_json",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "comrak"
version = "0.10.1"
@@ -1149,6 +1244,7 @@ dependencies = [
"client",
"gpui",
"postage",
+ "settings",
"theme",
"workspace",
]
@@ -1207,6 +1303,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "core-services"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b344b958cae90858bf6086f49599ecc5ec8698eacad0ea155509ba11fab347"
+dependencies = [
+ "core-foundation",
+]
+
[[package]]
name = "core-text"
version = "19.2.0"
@@ -1261,6 +1366,16 @@ dependencies = [
"cfg-if 1.0.0",
]
+[[package]]
+name = "crossbeam-channel"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
+dependencies = [
+ "crossbeam-utils 0.7.2",
+ "maybe-uninit",
+]
+
[[package]]
name = "crossbeam-channel"
version = "0.5.0"
@@ -1268,7 +1383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
]
[[package]]
@@ -1279,7 +1394,7 @@ checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
]
[[package]]
@@ -1289,7 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d60ab4a8dba064f2fbb5aa270c28da5cf4bbd0e72dae1140a6b0353a779dbe00"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
"lazy_static",
"loom",
"memoffset",
@@ -1303,7 +1418,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
+dependencies = [
+ "autocfg 1.0.1",
+ "cfg-if 0.1.10",
+ "lazy_static",
]
[[package]]
@@ -1407,7 +1533,7 @@ dependencies = [
"openssl-sys",
"schannel",
"socket2 0.4.0",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -1423,7 +1549,7 @@ dependencies = [
"openssl-sys",
"pkg-config",
"vcpkg",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -1498,6 +1624,7 @@ dependencies = [
"postage",
"project",
"serde_json",
+ "settings",
"theme",
"unindent",
"util",
@@ -1568,7 +1695,7 @@ checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
dependencies = [
"libc",
"redox_users",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -1579,7 +1706,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -1602,7 +1729,7 @@ checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
dependencies = [
"lazy_static",
"libc",
- "winapi",
+ "winapi 0.3.9",
"wio",
]
@@ -1648,10 +1775,11 @@ dependencies = [
"clock",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.8.3",
"futures",
"fuzzy",
"gpui",
+ "indoc",
"itertools",
"language",
"lazy_static",
@@ -1664,6 +1792,7 @@ dependencies = [
"rand 0.8.3",
"rpc",
"serde",
+ "settings",
"smallvec",
"smol",
"snippet",
@@ -1714,6 +1843,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+[[package]]
+name = "env_logger"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+dependencies = [
+ "log",
+]
+
[[package]]
name = "env_logger"
version = "0.8.3"
@@ -1736,6 +1874,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "erased-serde"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad132dd8d0d0b546348d7d86cb3191aad14b34e5f979781fc005c80d4ac67ffd"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "etagere"
version = "0.2.4"
@@ -1779,9 +1926,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "fastrand"
-version = "1.4.0"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
dependencies = [
"instant",
]
@@ -1818,12 +1965,14 @@ version = "0.1.0"
dependencies = [
"ctor",
"editor",
- "env_logger",
+ "env_logger 0.8.3",
"fuzzy",
"gpui",
+ "picker",
"postage",
"project",
"serde_json",
+ "settings",
"theme",
"util",
"workspace",
@@ -1897,7 +2046,7 @@ dependencies = [
"pathfinder_simd",
"servo-fontconfig",
"walkdir",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -1982,6 +2131,22 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
[[package]]
name = "funty"
version = "1.1.0"
@@ -2113,7 +2278,7 @@ dependencies = [
"libc",
"log",
"rustc_version",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -2224,6 +2389,7 @@ dependencies = [
"editor",
"gpui",
"postage",
+ "settings",
"text",
"workspace",
]
@@ -2245,7 +2411,7 @@ dependencies = [
"core-text",
"ctor",
"dhat",
- "env_logger",
+ "env_logger 0.8.3",
"etagere",
"font-kit",
"foreign-types",
@@ -2360,6 +2526,12 @@ dependencies = [
"unicode-segmentation",
]
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
[[package]]
name = "hermit-abi"
version = "0.1.18"
@@ -2441,7 +2613,7 @@ checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
dependencies = [
"bytes 1.0.1",
"fnv",
- "itoa",
+ "itoa 0.4.7",
]
[[package]]
@@ -2519,7 +2691,7 @@ version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
dependencies = [
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
"globset",
"lazy_static",
"log",
@@ -2584,6 +2756,34 @@ dependencies = [
"cfg-if 1.0.0",
]
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ipc-channel"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cb1d9211085f0ea6f1379d944b93c4d07e8207aa3bcf49f37eda12b85081887"
+dependencies = [
+ "bincode",
+ "crossbeam-channel 0.4.4",
+ "fnv",
+ "lazy_static",
+ "libc",
+ "mio",
+ "rand 0.7.3",
+ "serde",
+ "tempfile",
+ "uuid",
+ "winapi 0.3.9",
+]
+
[[package]]
name = "isahc"
version = "0.9.14"
@@ -2591,7 +2791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a"
dependencies = [
"bytes 0.5.6",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
"curl",
"curl-sys",
"flume",
@@ -2622,6 +2822,12 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
[[package]]
name = "jobserver"
version = "0.1.24"
@@ -2662,6 +2868,18 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "json_env_logger"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e2ec540ea0448b187d3a8b4a9f13e75527d06ef76b3a2baa1cd982aecb62ce2"
+dependencies = [
+ "env_logger 0.7.1",
+ "kv-log-macro",
+ "log",
+ "serde_json",
+]
+
[[package]]
name = "jwt-simple"
version = "0.10.1"
@@ -2696,6 +2914,16 @@ dependencies = [
"sha2 0.9.5",
]
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
[[package]]
name = "kurbo"
version = "0.8.1"
@@ -2725,7 +2953,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.8.3",
"futures",
"fuzzy",
"gpui",
@@ -2745,6 +2973,7 @@ dependencies = [
"text",
"theme",
"tree-sitter",
+ "tree-sitter-json",
"tree-sitter-rust",
"unindent",
"util",
@@ -2791,7 +3020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
dependencies = [
"cfg-if 1.0.0",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -2822,6 +3051,15 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
[[package]]
name = "lipsum"
version = "0.8.0"
@@ -2843,11 +3081,12 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.14"
+version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
dependencies = [
"cfg-if 1.0.0",
+ "serde",
"value-bag",
]
@@ -2880,7 +3119,7 @@ dependencies = [
"async-pipe",
"collections",
"ctor",
- "env_logger",
+ "env_logger 0.8.3",
"futures",
"gpui",
"log",
@@ -2928,6 +3167,12 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+[[package]]
+name = "maybe-uninit"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
[[package]]
name = "md-5"
version = "0.9.1"
@@ -3021,6 +3266,37 @@ dependencies = [
"autocfg 1.0.1",
]
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
[[package]]
name = "multimap"
version = "0.8.3"
@@ -3037,6 +3313,17 @@ dependencies = [
"socket2 0.3.19",
]
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
[[package]]
name = "nom"
version = "5.1.2"
@@ -3258,9 +3545,12 @@ dependencies = [
[[package]]
name = "os_str_bytes"
-version = "2.4.0"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
[[package]]
name = "outline"
@@ -3271,7 +3561,9 @@ dependencies = [
"gpui",
"language",
"ordered-float",
+ "picker",
"postage",
+ "settings",
"smol",
"text",
"workspace",
@@ -3316,7 +3608,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -3443,6 +3735,21 @@ dependencies = [
"indexmap",
]
+[[package]]
+name = "picker"
+version = "0.1.0"
+dependencies = [
+ "ctor",
+ "editor",
+ "env_logger 0.8.3",
+ "gpui",
+ "serde_json",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
[[package]]
name = "pico-args"
version = "0.4.0"
@@ -3505,6 +3812,20 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+[[package]]
+name = "plist"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
+dependencies = [
+ "base64 0.13.0",
+ "indexmap",
+ "line-wrap",
+ "serde",
+ "time 0.3.7",
+ "xml-rs",
+]
+
[[package]]
name = "png"
version = "0.16.8"
@@ -3527,7 +3848,7 @@ dependencies = [
"libc",
"log",
"wepoll-sys",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -3607,9 +3928,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
-version = "1.0.24"
+version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
@@ -3641,6 +3962,7 @@ dependencies = [
"rpc",
"serde",
"serde_json",
+ "settings",
"sha2 0.10.2",
"similar",
"smol",
@@ -3660,6 +3982,7 @@ dependencies = [
"postage",
"project",
"serde_json",
+ "settings",
"theme",
"util",
"workspace",
@@ -3671,11 +3994,16 @@ version = "0.1.0"
dependencies = [
"anyhow",
"editor",
+ "futures",
"fuzzy",
"gpui",
+ "language",
+ "lsp",
"ordered-float",
+ "picker",
"postage",
"project",
+ "settings",
"smol",
"text",
"util",
@@ -3699,7 +4027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603"
dependencies = [
"bytes 1.0.1",
- "heck",
+ "heck 0.3.3",
"itertools",
"log",
"multimap",
@@ -3764,7 +4092,7 @@ dependencies = [
"libc",
"rand_core 0.3.1",
"rdrand",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -3881,9 +4209,9 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
dependencies = [
- "crossbeam-channel",
+ "crossbeam-channel 0.5.0",
"crossbeam-deque",
- "crossbeam-utils",
+ "crossbeam-utils 0.8.2",
"lazy_static",
"num_cpus",
]
@@ -3945,7 +4273,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -3985,7 +4313,7 @@ dependencies = [
"spin",
"untrusted",
"web-sys",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -4012,11 +4340,11 @@ dependencies = [
"async-tungstenite",
"base64 0.13.0",
"clock",
+ "collections",
"futures",
"gpui",
"log",
"parking_lot",
- "postage",
"prost",
"prost-build",
"rand 0.8.3",
@@ -4162,6 +4490,12 @@ dependencies = [
"bytemuck",
]
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
[[package]]
name = "salsa20"
version = "0.8.0"
@@ -4187,7 +4521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
- "winapi",
+ "winapi 0.3.9",
]
[[package]]
@@ -4274,7 +4608,9 @@ dependencies = [
"log",
"postage",
"project",
+ "serde",
"serde_json",
+ "settings",
"theme",
"unindent",
"util",
@@ -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"]
@@ -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"]
@@ -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
@@ -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
@@ -0,0 +1,264 @@
+{
+ "*": {
+ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
+ "cmd-s": "workspace::Save",
+ "cmd-alt-i": "zed::DebugElements",
+ "cmd-k cmd-left": "workspace::ActivatePreviousPane",
+ "cmd-k cmd-right": "workspace::ActivateNextPane",
+ "cmd-=": "zed::IncreaseBufferFontSize",
+ "cmd--": "zed::DecreaseBufferFontSize",
+ "cmd-,": "zed::OpenSettings"
+ },
+ "menu": {
+ "up": "menu::SelectPrev",
+ "ctrl-p": "menu::SelectPrev",
+ "down": "menu::SelectNext",
+ "ctrl-n": "menu::SelectNext",
+ "cmd-up": "menu::SelectFirst",
+ "cmd-down": "menu::SelectLast",
+ "enter": "menu::Confirm",
+ "escape": "menu::Cancel"
+ },
+ "Pane": {
+ "shift-cmd-{": "pane::ActivatePrevItem",
+ "shift-cmd-}": "pane::ActivateNextItem",
+ "cmd-w": "pane::CloseActiveItem",
+ "alt-cmd-w": "pane::CloseInactiveItems",
+ "ctrl--": "pane::GoBack",
+ "shift-ctrl-_": "pane::GoForward",
+ "cmd-k up": [
+ "pane::Split",
+ "Up"
+ ],
+ "cmd-k down": [
+ "pane::Split",
+ "Down"
+ ],
+ "cmd-k left": [
+ "pane::Split",
+ "Left"
+ ],
+ "cmd-k right": [
+ "pane::Split",
+ "Right"
+ ],
+ "cmd-shift-F": "project_search::ToggleFocus",
+ "cmd-f": "project_search::ToggleFocus",
+ "cmd-g": "search::SelectNextMatch",
+ "cmd-shift-G": "search::SelectPrevMatch"
+ },
+ "Workspace": {
+ "cmd-shift-F": "project_search::Deploy",
+ "cmd-k cmd-t": "theme_selector::Toggle",
+ "cmd-k t": "theme_selector::Reload",
+ "cmd-t": "project_symbols::Toggle",
+ "cmd-p": "file_finder::Toggle",
+ "cmd-shift-P": "command_palette::Toggle",
+ "alt-shift-D": "diagnostics::Deploy",
+ "ctrl-alt-cmd-j": "journal::NewJournalEntry",
+ "cmd-1": [
+ "workspace::ToggleSidebarItemFocus",
+ {
+ "side": "Left",
+ "item_index": 0
+ }
+ ],
+ "cmd-shift-!": [
+ "workspace::ToggleSidebarItem",
+ {
+ "side": "Left",
+ "item_index": 0
+ }
+ ]
+ },
+ "ProjectSearchBar": {
+ "enter": "project_search::Search",
+ "cmd-enter": "project_search::SearchInNew"
+ },
+ "BufferSearchBar": {
+ "escape": "buffer_search::Dismiss",
+ "cmd-f": "buffer_search::FocusEditor",
+ "enter": "search::SelectNextMatch",
+ "shift-enter": "search::SelectPrevMatch"
+ },
+ "Editor": {
+ "escape": "editor::Cancel",
+ "backspace": "editor::Backspace",
+ "ctrl-h": "editor::Backspace",
+ "delete": "editor::Delete",
+ "ctrl-d": "editor::Delete",
+ "tab": "editor::Tab",
+ "shift-tab": "editor::TabPrev",
+ "cmd-[": "editor::Outdent",
+ "cmd-]": "editor::Indent",
+ "ctrl-shift-K": "editor::DeleteLine",
+ "alt-backspace": "editor::DeleteToPreviousWordStart",
+ "alt-h": "editor::DeleteToPreviousWordStart",
+ "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+ "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
+ "alt-delete": "editor::DeleteToNextWordEnd",
+ "alt-d": "editor::DeleteToNextWordEnd",
+ "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+ "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
+ "cmd-backspace": "editor::DeleteToBeginningOfLine",
+ "cmd-delete": "editor::DeleteToEndOfLine",
+ "ctrl-k": "editor::CutToEndOfLine",
+ "cmd-shift-D": "editor::DuplicateLine",
+ "ctrl-cmd-up": "editor::MoveLineUp",
+ "ctrl-cmd-down": "editor::MoveLineDown",
+ "cmd-x": "editor::Cut",
+ "cmd-c": "editor::Copy",
+ "cmd-v": "editor::Paste",
+ "cmd-z": "editor::Undo",
+ "cmd-shift-Z": "editor::Redo",
+ "up": "editor::MoveUp",
+ "down": "editor::MoveDown",
+ "left": "editor::MoveLeft",
+ "right": "editor::MoveRight",
+ "ctrl-p": "editor::MoveUp",
+ "ctrl-n": "editor::MoveDown",
+ "ctrl-b": "editor::MoveLeft",
+ "ctrl-f": "editor::MoveRight",
+ "alt-left": "editor::MoveToPreviousWordStart",
+ "alt-b": "editor::MoveToPreviousWordStart",
+ "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+ "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
+ "alt-right": "editor::MoveToNextWordEnd",
+ "alt-f": "editor::MoveToNextWordEnd",
+ "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+ "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
+ "cmd-left": "editor::MoveToBeginningOfLine",
+ "ctrl-a": "editor::MoveToBeginningOfLine",
+ "cmd-right": "editor::MoveToEndOfLine",
+ "ctrl-e": "editor::MoveToEndOfLine",
+ "cmd-up": "editor::MoveToBeginning",
+ "cmd-down": "editor::MoveToEnd",
+ "shift-up": "editor::SelectUp",
+ "ctrl-shift-P": "editor::SelectUp",
+ "shift-down": "editor::SelectDown",
+ "ctrl-shift-N": "editor::SelectDown",
+ "shift-left": "editor::SelectLeft",
+ "ctrl-shift-B": "editor::SelectLeft",
+ "shift-right": "editor::SelectRight",
+ "ctrl-shift-F": "editor::SelectRight",
+ "alt-shift-left": "editor::SelectToPreviousWordStart",
+ "alt-shift-B": "editor::SelectToPreviousWordStart",
+ "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
+ "ctrl-alt-shift-B": "editor::SelectToPreviousSubwordStart",
+ "alt-shift-right": "editor::SelectToNextWordEnd",
+ "alt-shift-F": "editor::SelectToNextWordEnd",
+ "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
+ "cmd-shift-up": "editor::SelectToBeginning",
+ "cmd-shift-down": "editor::SelectToEnd",
+ "cmd-a": "editor::SelectAll",
+ "cmd-l": "editor::SelectLine",
+ "cmd-shift-L": "editor::SplitSelectionIntoLines",
+ "cmd-alt-up": "editor::AddSelectionAbove",
+ "cmd-ctrl-p": "editor::AddSelectionAbove",
+ "cmd-alt-down": "editor::AddSelectionBelow",
+ "cmd-ctrl-n": "editor::AddSelectionBelow",
+ "ctrl-alt-shift-F": "editor::SelectToNextSubwordEnd",
+ "cmd-shift-left": [
+ "editor::SelectToBeginningOfLine",
+ {
+ "stop_at_soft_wraps": true
+ }
+ ],
+ "ctrl-shift-A": [
+ "editor::SelectToBeginningOfLine",
+ {
+ "stop_at_soft_wraps": true
+ }
+ ],
+ "cmd-shift-right": [
+ "editor::SelectToEndOfLine",
+ {
+ "stop_at_soft_wraps": true
+ }
+ ],
+ "ctrl-shift-E": [
+ "editor::SelectToEndOfLine",
+ {
+ "stop_at_soft_wraps": true
+ }
+ ],
+ "cmd-d": [
+ "editor::SelectNext",
+ {
+ "replace_newest": false
+ }
+ ],
+ "cmd-k cmd-d": [
+ "editor::SelectNext",
+ {
+ "replace_newest": true
+ }
+ ],
+ "cmd-/": "editor::ToggleComments",
+ "alt-up": "editor::SelectLargerSyntaxNode",
+ "ctrl-w": "editor::SelectLargerSyntaxNode",
+ "alt-down": "editor::SelectSmallerSyntaxNode",
+ "ctrl-shift-W": "editor::SelectSmallerSyntaxNode",
+ "cmd-u": "editor::UndoSelection",
+ "cmd-shift-U": "editor::RedoSelection",
+ "f8": "editor::GoToNextDiagnostic",
+ "shift-f8": "editor::GoToPrevDiagnostic",
+ "f2": "editor::Rename",
+ "f12": "editor::GoToDefinition",
+ "alt-shift-f12": "editor::FindAllReferences",
+ "ctrl-m": "editor::MoveToEnclosingBracket",
+ "pageup": "editor::PageUp",
+ "pagedown": "editor::PageDown",
+ "alt-cmd-[": "editor::Fold",
+ "alt-cmd-]": "editor::UnfoldLines",
+ "alt-cmd-f": "editor::FoldSelectedRanges",
+ "ctrl-space": "editor::ShowCompletions",
+ "cmd-.": "editor::ToggleCodeActions",
+ "alt-enter": "editor::OpenExcerpts",
+ "cmd-f10": "editor::RestartLanguageServer"
+ },
+ "Editor && renaming": {
+ "enter": "editor::ConfirmRename"
+ },
+ "Editor && showing_completions": {
+ "enter": "editor::ConfirmCompletion",
+ "tab": "editor::ConfirmCompletion"
+ },
+ "Editor && showing_code_actions": {
+ "enter": "editor::ConfirmCodeAction"
+ },
+ "Editor && mode == full": {
+ "enter": "editor::Newline",
+ "cmd-f": [
+ "buffer_search::Deploy",
+ {
+ "focus": true
+ }
+ ],
+ "cmd-e": [
+ "buffer_search::Deploy",
+ {
+ "focus": false
+ }
+ ],
+ "cmd-shift-O": "outline::Toggle",
+ "ctrl-g": "go_to_line::Toggle"
+ },
+ "Editor && mode == auto_height": {
+ "alt-enter": [
+ "editor::Input",
+ "\n"
+ ]
+ },
+ "GoToLine": {
+ "escape": "go_to_line::Toggle",
+ "enter": "go_to_line::Confirm"
+ },
+ "ChatPanel": {
+ "enter": "chat_panel::Send"
+ },
+ "ProjectPanel": {
+ "left": "project_panel::CollapseSelectedEntry",
+ "right": "project_panel::ExpandSelectedEntry"
+ }
+}
@@ -0,0 +1,93 @@
+{
+ "Editor && VimControl": {
+ "i": [
+ "vim::SwitchMode",
+ "Insert"
+ ],
+ "g": [
+ "vim::PushOperator",
+ {
+ "Namespace": "G"
+ }
+ ],
+ "h": "vim::Left",
+ "j": "vim::Down",
+ "k": "vim::Up",
+ "l": "vim::Right",
+ "0": "vim::StartOfLine",
+ "shift-$": "vim::EndOfLine",
+ "shift-G": "vim::EndOfDocument",
+ "w": "vim::NextWordStart",
+ "shift-W": [
+ "vim::NextWordStart",
+ {
+ "ignorePunctuation": true
+ }
+ ],
+ "e": "vim::NextWordEnd",
+ "shift-E": [
+ "vim::NextWordEnd",
+ {
+ "ignorePunctuation": true
+ }
+ ],
+ "b": "vim::PreviousWordStart",
+ "shift-B": [
+ "vim::PreviousWordStart",
+ {
+ "ignorePunctuation": true
+ }
+ ],
+ "escape": [
+ "vim::SwitchMode",
+ "Normal"
+ ]
+ },
+ "Editor && vim_operator == g": {
+ "g": "vim::StartOfDocument"
+ },
+ "Editor && vim_mode == insert": {
+ "escape": "vim::NormalBefore",
+ "ctrl-c": "vim::NormalBefore"
+ },
+ "Editor && vim_mode == normal": {
+ "c": [
+ "vim::PushOperator",
+ "Change"
+ ],
+ "d": [
+ "vim::PushOperator",
+ "Delete"
+ ]
+ },
+ "Editor && vim_operator == c": {
+ "w": [
+ "vim::NextWordEnd",
+ {
+ "ignorePunctuation": false
+ }
+ ],
+ "shift-W": [
+ "vim::NextWordEnd",
+ {
+ "ignorePunctuation": true
+ }
+ ]
+ },
+ "Editor && vim_operator == d": {
+ "w": [
+ "vim::NextWordStart",
+ {
+ "ignorePunctuation": false,
+ "stopAtNewline": true
+ }
+ ],
+ "shift-W": [
+ "vim::NextWordStart",
+ {
+ "ignorePunctuation": true,
+ "stopAtNewline": true
+ }
+ ]
+ }
+}
@@ -0,0 +1,1338 @@
+{
+ "selector": {
+ "background": "#1c1c1c",
+ "corner_radius": 8,
+ "padding": 8,
+ "item": {
+ "padding": {
+ "bottom": 4,
+ "left": 12,
+ "right": 12,
+ "top": 4
+ },
+ "corner_radius": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#4f8ff7",
+ "weight": "bold",
+ "size": 14
+ }
+ },
+ "active_item": {
+ "padding": {
+ "bottom": 4,
+ "left": 12,
+ "right": 12,
+ "top": 4
+ },
+ "corner_radius": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#4f8ff7",
+ "weight": "bold",
+ "size": 14
+ },
+ "background": "#2b2b2b"
+ },
+ "border": {
+ "color": "#070707",
+ "width": 1
+ },
+ "empty": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 4,
+ "left": 12,
+ "right": 12,
+ "top": 8
+ }
+ },
+ "input_editor": {
+ "background": "#000000",
+ "corner_radius": 8,
+ "placeholder_text": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "border": {
+ "color": "#232323",
+ "width": 1
+ },
+ "padding": {
+ "bottom": 7,
+ "left": 16,
+ "right": 16,
+ "top": 7
+ }
+ },
+ "margin": {
+ "bottom": 52,
+ "top": 52
+ },
+ "shadow": {
+ "blur": 16,
+ "color": "#00000052",
+ "offset": [
+ 0,
+ 2
+ ]
+ }
+ },
+ "workspace": {
+ "background": "#1c1c1c",
+ "leader_border_opacity": 0.7,
+ "leader_border_width": 2,
+ "tab": {
+ "height": 32,
+ "background": "#1c1c1c",
+ "icon_close": "#555555",
+ "icon_close_active": "#ffffff",
+ "icon_conflict": "#f6a724",
+ "icon_dirty": "#135acd",
+ "icon_width": 8,
+ "spacing": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14
+ },
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "left": true,
+ "bottom": true,
+ "overlay": true
+ },
+ "padding": {
+ "left": 8,
+ "right": 8
+ }
+ },
+ "active_tab": {
+ "height": 32,
+ "background": "#000000",
+ "icon_close": "#555555",
+ "icon_close_active": "#ffffff",
+ "icon_conflict": "#f6a724",
+ "icon_dirty": "#135acd",
+ "icon_width": 8,
+ "spacing": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#ffffff",
+ "size": 14
+ },
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "left": true,
+ "bottom": false,
+ "overlay": true
+ },
+ "padding": {
+ "left": 8,
+ "right": 8
+ }
+ },
+ "left_sidebar": {
+ "width": 30,
+ "background": "#1c1c1c",
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "right": true
+ },
+ "item": {
+ "height": 32,
+ "icon_color": "#9c9c9c",
+ "icon_size": 18
+ },
+ "active_item": {
+ "height": 32,
+ "icon_color": "#ffffff",
+ "icon_size": 18
+ },
+ "resize_handle": {
+ "background": "#070707",
+ "padding": {
+ "left": 1
+ }
+ }
+ },
+ "right_sidebar": {
+ "width": 30,
+ "background": "#1c1c1c",
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "left": true
+ },
+ "item": {
+ "height": 32,
+ "icon_color": "#9c9c9c",
+ "icon_size": 18
+ },
+ "active_item": {
+ "height": 32,
+ "icon_color": "#ffffff",
+ "icon_size": 18
+ },
+ "resize_handle": {
+ "background": "#070707",
+ "padding": {
+ "left": 1
+ }
+ }
+ },
+ "pane_divider": {
+ "color": "#232323",
+ "width": 1
+ },
+ "status_bar": {
+ "height": 24,
+ "item_spacing": 8,
+ "padding": {
+ "left": 6,
+ "right": 6
+ },
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true,
+ "overlay": true
+ },
+ "cursor_position": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "diagnostic_message": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "lsp_message": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "auto_update_progress_message": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "auto_update_done_message": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ }
+ },
+ "titlebar": {
+ "avatar_width": 18,
+ "height": 32,
+ "background": "#2b2b2b",
+ "share_icon_color": "#9c9c9c",
+ "share_icon_active_color": "#2472f2",
+ "title": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "avatar": {
+ "corner_radius": 10,
+ "border": {
+ "color": "#00000088",
+ "width": 1
+ }
+ },
+ "avatar_ribbon": {
+ "height": 3,
+ "width": 12
+ },
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "bottom": true
+ },
+ "sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 12,
+ "border": {
+ "color": "#070707",
+ "width": 1
+ },
+ "corner_radius": 6,
+ "margin": {
+ "top": 1,
+ "right": 6
+ },
+ "padding": {
+ "left": 6,
+ "right": 6
+ }
+ },
+ "hovered_sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#ffffff",
+ "size": 12,
+ "border": {
+ "color": "#070707",
+ "width": 1
+ },
+ "corner_radius": 6,
+ "margin": {
+ "top": 1,
+ "right": 6
+ },
+ "padding": {
+ "left": 6,
+ "right": 6
+ }
+ },
+ "offline_icon": {
+ "color": "#9c9c9c",
+ "width": 16,
+ "padding": {
+ "right": 4
+ }
+ },
+ "outdated_warning": {
+ "family": "Zed Sans",
+ "color": "#f7bb57",
+ "size": 13
+ }
+ },
+ "toolbar": {
+ "height": 34,
+ "background": "#000000",
+ "border": {
+ "color": "#232323",
+ "width": 1,
+ "bottom": true
+ },
+ "item_spacing": 8,
+ "padding": {
+ "left": 16,
+ "right": 8,
+ "top": 4,
+ "bottom": 4
+ }
+ },
+ "breadcrumbs": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 14,
+ "padding": {
+ "left": 6
+ }
+ },
+ "disconnected_overlay": {
+ "family": "Zed Sans",
+ "color": "#ffffff",
+ "size": 14,
+ "background": "#000000aa"
+ }
+ },
+ "editor": {
+ "text_color": "#d5d5d5",
+ "background": "#000000",
+ "active_line_background": "#ffffff12",
+ "code_actions_indicator": "#555555",
+ "diff_background_deleted": "#f15656",
+ "diff_background_inserted": "#1b9447",
+ "document_highlight_read_background": "#ffffff1f",
+ "document_highlight_write_background": "#ffffff29",
+ "error_color": "#f15656",
+ "gutter_background": "#000000",
+ "gutter_padding_factor": 3.5,
+ "highlighted_line_background": "#ffffff1f",
+ "line_number": "#474747",
+ "line_number_active": "#ffffff",
+ "rename_fade": 0.6,
+ "unnecessary_code_fade": 0.5,
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "guest_selections": [
+ {
+ "cursor": "#79ba16",
+ "selection": "#79ba163d"
+ },
+ {
+ "cursor": "#d430e0",
+ "selection": "#d430e03d"
+ },
+ {
+ "cursor": "#ee670a",
+ "selection": "#ee670a3d"
+ },
+ {
+ "cursor": "#993bf3",
+ "selection": "#993bf33d"
+ },
+ {
+ "cursor": "#16d6c1",
+ "selection": "#16d6c13d"
+ },
+ {
+ "cursor": "#ef59a3",
+ "selection": "#ef59a33d"
+ },
+ {
+ "cursor": "#f7bf17",
+ "selection": "#f7bf173d"
+ }
+ ],
+ "autocomplete": {
+ "background": "#000000",
+ "corner_radius": 8,
+ "padding": 4,
+ "border": {
+ "color": "#232323",
+ "width": 1
+ },
+ "item": {
+ "corner_radius": 6,
+ "padding": {
+ "bottom": 2,
+ "left": 6,
+ "right": 6,
+ "top": 2
+ }
+ },
+ "hovered_item": {
+ "corner_radius": 6,
+ "padding": {
+ "bottom": 2,
+ "left": 6,
+ "right": 6,
+ "top": 2
+ },
+ "background": "#ffffff14"
+ },
+ "margin": {
+ "left": -14
+ },
+ "match_highlight": {
+ "family": "Zed Mono",
+ "color": "#4f8ff7",
+ "size": 14
+ },
+ "selected_item": {
+ "corner_radius": 6,
+ "padding": {
+ "bottom": 2,
+ "left": 6,
+ "right": 6,
+ "top": 2
+ },
+ "background": "#ffffff1f"
+ }
+ },
+ "diagnostic_header": {
+ "background": "#1c1c1c",
+ "icon_width_factor": 1.5,
+ "text_scale_factor": 0.857,
+ "border": {
+ "color": "#232323",
+ "width": 1,
+ "bottom": true,
+ "top": true
+ },
+ "code": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "left": 10
+ }
+ },
+ "message": {
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 14,
+ "weight": "bold"
+ },
+ "text": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14
+ }
+ }
+ },
+ "diagnostic_path_header": {
+ "background": "#ffffff12",
+ "text_scale_factor": 0.857,
+ "filename": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "path": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "left": 12
+ }
+ }
+ },
+ "error_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#f15656",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#f15656",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "warning_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#f7bb57",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#f7bb57",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "information_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "hint_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_error_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_hint_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_information_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_warning_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#070707",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "syntax": {
+ "keyword": "#4f8ff7",
+ "function": "#f9da82",
+ "string": "#f99d5f",
+ "type": "#3eeeda",
+ "number": "#aeef4b",
+ "comment": "#aaaaaa",
+ "property": "#4f8ff7",
+ "variant": "#53c1f5",
+ "constant": "#d5d5d5",
+ "title": {
+ "color": "#de900c",
+ "weight": "bold"
+ },
+ "emphasis": "#4f8ff7",
+ "emphasis_strong": {
+ "color": "#4f8ff7",
+ "weight": "bold"
+ },
+ "link_uri": {
+ "color": "#79ba16",
+ "underline": true
+ },
+ "link_text": {
+ "color": "#ee670a",
+ "italic": true
+ },
+ "list_marker": "#c6c6c6"
+ }
+ },
+ "project_diagnostics": {
+ "tab_icon_spacing": 4,
+ "tab_icon_width": 13,
+ "tab_summary_spacing": 10,
+ "empty_message": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 18
+ },
+ "status_bar_item": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 10
+ }
+ }
+ },
+ "command_palette": {
+ "keystroke_spacing": 8,
+ "key": {
+ "text": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 12
+ },
+ "corner_radius": 4,
+ "background": "#0e0e0e80",
+ "border": {
+ "color": "#232323",
+ "width": 1
+ },
+ "padding": {
+ "top": 2,
+ "bottom": 2,
+ "left": 8,
+ "right": 8
+ },
+ "margin": {
+ "left": 2
+ }
+ }
+ },
+ "project_panel": {
+ "padding": {
+ "top": 6,
+ "left": 12
+ },
+ "entry": {
+ "height": 22,
+ "icon_color": "#555555",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 14
+ }
+ },
+ "hovered_entry": {
+ "height": 22,
+ "background": "#232323",
+ "icon_color": "#555555",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 14
+ }
+ },
+ "selected_entry": {
+ "height": 22,
+ "icon_color": "#555555",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ }
+ },
+ "hovered_selected_entry": {
+ "height": 22,
+ "background": "#232323",
+ "icon_color": "#555555",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ }
+ }
+ },
+ "chat_panel": {
+ "padding": {
+ "top": 12,
+ "left": 12,
+ "bottom": 12,
+ "right": 12
+ },
+ "channel_name": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "weight": "bold",
+ "size": 14
+ },
+ "channel_name_hash": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "padding": {
+ "right": 8
+ }
+ },
+ "channel_select": {
+ "header": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 4,
+ "left": 0
+ },
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "corner_radius": 0
+ },
+ "item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "corner_radius": 0
+ },
+ "hovered_item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "background": "#232323",
+ "corner_radius": 6
+ },
+ "active_item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "corner_radius": 0
+ },
+ "hovered_active_item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "background": "#232323",
+ "corner_radius": 6
+ },
+ "menu": {
+ "background": "#000000",
+ "corner_radius": 6,
+ "padding": 4,
+ "border": {
+ "color": "#070707",
+ "width": 1
+ },
+ "shadow": {
+ "blur": 16,
+ "color": "#00000052",
+ "offset": [
+ 0,
+ 2
+ ]
+ }
+ }
+ },
+ "sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "underline": true,
+ "size": 14
+ },
+ "hovered_sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "underline": true,
+ "size": 14
+ },
+ "message": {
+ "body": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14
+ },
+ "timestamp": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 6
+ },
+ "sender": {
+ "family": "Zed Sans",
+ "color": "#f1f1f1",
+ "weight": "bold",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ }
+ },
+ "pending_message": {
+ "body": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "timestamp": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 6
+ },
+ "sender": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "weight": "bold",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ }
+ },
+ "input_editor": {
+ "background": "#000000",
+ "corner_radius": 6,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ },
+ "placeholder_text": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "border": {
+ "color": "#232323",
+ "width": 1
+ },
+ "padding": {
+ "bottom": 7,
+ "left": 8,
+ "right": 8,
+ "top": 7
+ }
+ }
+ },
+ "contacts_panel": {
+ "padding": {
+ "top": 12,
+ "left": 12,
+ "bottom": 12,
+ "right": 12
+ },
+ "host_row_height": 28,
+ "tree_branch_color": "#404040",
+ "tree_branch_width": 1,
+ "host_avatar": {
+ "corner_radius": 10,
+ "width": 18
+ },
+ "host_username": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14,
+ "padding": {
+ "left": 8
+ }
+ },
+ "project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ }
+ },
+ "shared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ },
+ "background": "#1c1c1c",
+ "corner_radius": 6
+ },
+ "hovered_shared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ },
+ "background": "#232323",
+ "corner_radius": 6
+ },
+ "unshared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ }
+ },
+ "hovered_unshared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ },
+ "corner_radius": 6
+ }
+ },
+ "search": {
+ "match_background": "#3f15a380",
+ "tab_icon_spacing": 8,
+ "tab_icon_width": 14,
+ "active_hovered_option_button": {
+ "family": "Zed Mono",
+ "color": "#ffffff",
+ "size": 14,
+ "background": "#232323",
+ "corner_radius": 4,
+ "border": {
+ "color": "#404040",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "active_option_button": {
+ "family": "Zed Mono",
+ "color": "#ffffff",
+ "size": 14,
+ "background": "#232323",
+ "corner_radius": 4,
+ "border": {
+ "color": "#404040",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "editor": {
+ "background": "#000000",
+ "corner_radius": 8,
+ "min_width": 200,
+ "max_width": 500,
+ "placeholder_text": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "text": {
+ "family": "Zed Mono",
+ "color": "#ffffff",
+ "size": 14
+ },
+ "border": {
+ "color": "#232323",
+ "width": 1
+ },
+ "margin": {
+ "right": 6
+ },
+ "padding": {
+ "top": 3,
+ "bottom": 3,
+ "left": 12,
+ "right": 8
+ }
+ },
+ "hovered_option_button": {
+ "family": "Zed Mono",
+ "color": "#ffffff",
+ "size": 14,
+ "background": "#0e0e0e",
+ "corner_radius": 4,
+ "border": {
+ "color": "#404040",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "invalid_editor": {
+ "background": "#000000",
+ "corner_radius": 8,
+ "min_width": 200,
+ "max_width": 500,
+ "placeholder_text": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "text": {
+ "family": "Zed Mono",
+ "color": "#ffffff",
+ "size": 14
+ },
+ "border": {
+ "color": "#eb2d2d",
+ "width": 1
+ },
+ "margin": {
+ "right": 6
+ },
+ "padding": {
+ "top": 3,
+ "bottom": 3,
+ "left": 12,
+ "right": 8
+ }
+ },
+ "match_index": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14,
+ "padding": 6
+ },
+ "option_button": {
+ "family": "Zed Mono",
+ "color": "#9c9c9c",
+ "size": 14,
+ "background": "#0e0e0e",
+ "corner_radius": 4,
+ "border": {
+ "color": "#232323",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "option_button_group": {
+ "padding": {
+ "left": 4,
+ "right": 4
+ }
+ },
+ "results_status": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 18
+ }
+ },
+ "breadcrumbs": {
+ "family": "Zed Sans",
+ "color": "#9c9c9c",
+ "size": 14,
+ "padding": {
+ "left": 6
+ }
+ }
+}
@@ -0,0 +1,1338 @@
+{
+ "selector": {
+ "background": "#f8f8f8",
+ "corner_radius": 8,
+ "padding": 8,
+ "item": {
+ "padding": {
+ "bottom": 4,
+ "left": 12,
+ "right": 12,
+ "top": 4
+ },
+ "corner_radius": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#484bed",
+ "weight": "bold",
+ "size": 14
+ }
+ },
+ "active_item": {
+ "padding": {
+ "bottom": 4,
+ "left": 12,
+ "right": 12,
+ "top": 4
+ },
+ "corner_radius": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#484bed",
+ "weight": "bold",
+ "size": 14
+ },
+ "background": "#e3e3e3"
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "empty": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 4,
+ "left": 12,
+ "right": 12,
+ "top": 8
+ }
+ },
+ "input_editor": {
+ "background": "#ffffff",
+ "corner_radius": 8,
+ "placeholder_text": {
+ "family": "Zed Sans",
+ "color": "#808080",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "text": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "padding": {
+ "bottom": 7,
+ "left": 16,
+ "right": 16,
+ "top": 7
+ }
+ },
+ "margin": {
+ "bottom": 52,
+ "top": 52
+ },
+ "shadow": {
+ "blur": 16,
+ "color": "#0000001f",
+ "offset": [
+ 0,
+ 2
+ ]
+ }
+ },
+ "workspace": {
+ "background": "#f8f8f8",
+ "leader_border_opacity": 0.7,
+ "leader_border_width": 2,
+ "tab": {
+ "height": 32,
+ "background": "#f8f8f8",
+ "icon_close": "#9c9c9c",
+ "icon_close_active": "#000000",
+ "icon_conflict": "#f7bf17",
+ "icon_dirty": "#135acd",
+ "icon_width": 8,
+ "spacing": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "left": true,
+ "bottom": true,
+ "overlay": true
+ },
+ "padding": {
+ "left": 8,
+ "right": 8
+ }
+ },
+ "active_tab": {
+ "height": 32,
+ "background": "#ffffff",
+ "icon_close": "#9c9c9c",
+ "icon_close_active": "#000000",
+ "icon_conflict": "#f7bf17",
+ "icon_dirty": "#135acd",
+ "icon_width": 8,
+ "spacing": 8,
+ "text": {
+ "family": "Zed Sans",
+ "color": "#000000",
+ "size": 14
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "left": true,
+ "bottom": false,
+ "overlay": true
+ },
+ "padding": {
+ "left": 8,
+ "right": 8
+ }
+ },
+ "left_sidebar": {
+ "width": 30,
+ "background": "#f8f8f8",
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "right": true
+ },
+ "item": {
+ "height": 32,
+ "icon_color": "#717171",
+ "icon_size": 18
+ },
+ "active_item": {
+ "height": 32,
+ "icon_color": "#000000",
+ "icon_size": 18
+ },
+ "resize_handle": {
+ "background": "#d5d5d5",
+ "padding": {
+ "left": 1
+ }
+ }
+ },
+ "right_sidebar": {
+ "width": 30,
+ "background": "#f8f8f8",
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "left": true
+ },
+ "item": {
+ "height": 32,
+ "icon_color": "#717171",
+ "icon_size": 18
+ },
+ "active_item": {
+ "height": 32,
+ "icon_color": "#000000",
+ "icon_size": 18
+ },
+ "resize_handle": {
+ "background": "#d5d5d5",
+ "padding": {
+ "left": 1
+ }
+ }
+ },
+ "pane_divider": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "status_bar": {
+ "height": 24,
+ "item_spacing": 8,
+ "padding": {
+ "left": 6,
+ "right": 6
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true,
+ "overlay": true
+ },
+ "cursor_position": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "diagnostic_message": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "lsp_message": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "auto_update_progress_message": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "auto_update_done_message": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ }
+ },
+ "titlebar": {
+ "avatar_width": 18,
+ "height": 32,
+ "background": "#eaeaea",
+ "share_icon_color": "#717171",
+ "share_icon_active_color": "#484bed",
+ "title": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "avatar": {
+ "corner_radius": 10,
+ "border": {
+ "color": "#00000088",
+ "width": 1
+ }
+ },
+ "avatar_ribbon": {
+ "height": 3,
+ "width": 12
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "bottom": true
+ },
+ "sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 12,
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "corner_radius": 6,
+ "margin": {
+ "top": 1,
+ "right": 6
+ },
+ "padding": {
+ "left": 6,
+ "right": 6
+ }
+ },
+ "hovered_sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#000000",
+ "size": 12,
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "corner_radius": 6,
+ "margin": {
+ "top": 1,
+ "right": 6
+ },
+ "padding": {
+ "left": 6,
+ "right": 6
+ }
+ },
+ "offline_icon": {
+ "color": "#717171",
+ "width": 16,
+ "padding": {
+ "right": 4
+ }
+ },
+ "outdated_warning": {
+ "family": "Zed Sans",
+ "color": "#d3a20b",
+ "size": 13
+ }
+ },
+ "toolbar": {
+ "height": 34,
+ "background": "#ffffff",
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "bottom": true
+ },
+ "item_spacing": 8,
+ "padding": {
+ "left": 16,
+ "right": 8,
+ "top": 4,
+ "bottom": 4
+ }
+ },
+ "breadcrumbs": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "padding": {
+ "left": 6
+ }
+ },
+ "disconnected_overlay": {
+ "family": "Zed Sans",
+ "color": "#000000",
+ "size": 14,
+ "background": "#000000aa"
+ }
+ },
+ "editor": {
+ "text_color": "#1c1c1c",
+ "background": "#ffffff",
+ "active_line_background": "#0000000f",
+ "code_actions_indicator": "#9c9c9c",
+ "diff_background_deleted": "#fcc6c6",
+ "diff_background_inserted": "#b7f9ce",
+ "document_highlight_read_background": "#0000000f",
+ "document_highlight_write_background": "#00000029",
+ "error_color": "#eb2d2d",
+ "gutter_background": "#ffffff",
+ "gutter_padding_factor": 3.5,
+ "highlighted_line_background": "#0000001f",
+ "line_number": "#aaaaaa",
+ "line_number_active": "#000000",
+ "rename_fade": 0.6,
+ "unnecessary_code_fade": 0.5,
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "guest_selections": [
+ {
+ "cursor": "#12d796",
+ "selection": "#12d7963d"
+ },
+ {
+ "cursor": "#de57e8",
+ "selection": "#de57e83d"
+ },
+ {
+ "cursor": "#f9812e",
+ "selection": "#f9812e3d"
+ },
+ {
+ "cursor": "#b066f8",
+ "selection": "#b066f83d"
+ },
+ {
+ "cursor": "#16d6c1",
+ "selection": "#16d6c13d"
+ },
+ {
+ "cursor": "#ef59a3",
+ "selection": "#ef59a33d"
+ },
+ {
+ "cursor": "#f7bf17",
+ "selection": "#f7bf173d"
+ }
+ ],
+ "autocomplete": {
+ "background": "#ffffff",
+ "corner_radius": 8,
+ "padding": 4,
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "item": {
+ "corner_radius": 6,
+ "padding": {
+ "bottom": 2,
+ "left": 6,
+ "right": 6,
+ "top": 2
+ }
+ },
+ "hovered_item": {
+ "corner_radius": 6,
+ "padding": {
+ "bottom": 2,
+ "left": 6,
+ "right": 6,
+ "top": 2
+ },
+ "background": "#00000008"
+ },
+ "margin": {
+ "left": -14
+ },
+ "match_highlight": {
+ "family": "Zed Mono",
+ "color": "#484bed",
+ "size": 14
+ },
+ "selected_item": {
+ "corner_radius": 6,
+ "padding": {
+ "bottom": 2,
+ "left": 6,
+ "right": 6,
+ "top": 2
+ },
+ "background": "#0000000f"
+ }
+ },
+ "diagnostic_header": {
+ "background": "#f8f8f8",
+ "icon_width_factor": 1.5,
+ "text_scale_factor": 0.857,
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "bottom": true,
+ "top": true
+ },
+ "code": {
+ "family": "Zed Mono",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "left": 10
+ }
+ },
+ "message": {
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 14,
+ "weight": "bold"
+ },
+ "text": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ }
+ }
+ },
+ "diagnostic_path_header": {
+ "background": "#0000000f",
+ "text_scale_factor": 0.857,
+ "filename": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "path": {
+ "family": "Zed Mono",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "left": 12
+ }
+ }
+ },
+ "error_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#eb2d2d",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#eb2d2d",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "warning_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#d3a20b",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#d3a20b",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "information_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "hint_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#2472f2",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_error_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_hint_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_information_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "invalid_warning_diagnostic": {
+ "text_scale_factor": 0.857,
+ "header": {
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1,
+ "top": true
+ }
+ },
+ "message": {
+ "text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "highlight_text": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "weight": "bold"
+ }
+ }
+ },
+ "syntax": {
+ "keyword": "#1819a1",
+ "function": "#bb550e",
+ "string": "#eb2d2d",
+ "type": "#a8820e",
+ "number": "#484bed",
+ "comment": "#717171",
+ "property": "#106c4e",
+ "variant": "#97142a",
+ "constant": "#1c1c1c",
+ "title": {
+ "color": "#1096d3",
+ "weight": "bold"
+ },
+ "emphasis": "#484bed",
+ "emphasis_strong": {
+ "color": "#484bed",
+ "weight": "bold"
+ },
+ "link_uri": {
+ "color": "#79ba16",
+ "underline": true
+ },
+ "link_text": {
+ "color": "#eb2d2d",
+ "italic": true
+ },
+ "list_marker": "#555555"
+ }
+ },
+ "project_diagnostics": {
+ "tab_icon_spacing": 4,
+ "tab_icon_width": 13,
+ "tab_summary_spacing": 10,
+ "empty_message": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 18
+ },
+ "status_bar_item": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "right": 10
+ }
+ }
+ },
+ "command_palette": {
+ "keystroke_spacing": 8,
+ "key": {
+ "text": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 12
+ },
+ "corner_radius": 4,
+ "background": "#f1f1f1",
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "padding": {
+ "top": 2,
+ "bottom": 2,
+ "left": 8,
+ "right": 8
+ },
+ "margin": {
+ "left": 2
+ }
+ }
+ },
+ "project_panel": {
+ "padding": {
+ "top": 6,
+ "left": 12
+ },
+ "entry": {
+ "height": 22,
+ "icon_color": "#9c9c9c",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14
+ }
+ },
+ "hovered_entry": {
+ "height": 22,
+ "background": "#eaeaea",
+ "icon_color": "#9c9c9c",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14
+ }
+ },
+ "selected_entry": {
+ "height": 22,
+ "icon_color": "#9c9c9c",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ }
+ },
+ "hovered_selected_entry": {
+ "height": 22,
+ "background": "#eaeaea",
+ "icon_color": "#9c9c9c",
+ "icon_size": 8,
+ "icon_spacing": 8,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ }
+ }
+ },
+ "chat_panel": {
+ "padding": {
+ "top": 12,
+ "left": 12,
+ "bottom": 12,
+ "right": 12
+ },
+ "channel_name": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "weight": "bold",
+ "size": 14
+ },
+ "channel_name_hash": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "padding": {
+ "right": 8
+ }
+ },
+ "channel_select": {
+ "header": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 4,
+ "left": 0
+ },
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "corner_radius": 0
+ },
+ "item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "corner_radius": 0
+ },
+ "hovered_item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "background": "#eaeaea",
+ "corner_radius": 6
+ },
+ "active_item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "corner_radius": 0
+ },
+ "hovered_active_item": {
+ "name": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "padding": 4,
+ "hash": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ },
+ "background": "#eaeaea",
+ "corner_radius": 6
+ },
+ "menu": {
+ "background": "#ffffff",
+ "corner_radius": 6,
+ "padding": 4,
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "shadow": {
+ "blur": 16,
+ "color": "#0000001f",
+ "offset": [
+ 0,
+ 2
+ ]
+ }
+ }
+ },
+ "sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "underline": true,
+ "size": 14
+ },
+ "hovered_sign_in_prompt": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "underline": true,
+ "size": 14
+ },
+ "message": {
+ "body": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14
+ },
+ "timestamp": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 6
+ },
+ "sender": {
+ "family": "Zed Sans",
+ "color": "#2b2b2b",
+ "weight": "bold",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ }
+ },
+ "pending_message": {
+ "body": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "timestamp": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "size": 14
+ },
+ "padding": {
+ "bottom": 6
+ },
+ "sender": {
+ "family": "Zed Sans",
+ "color": "#636363",
+ "weight": "bold",
+ "size": 14,
+ "margin": {
+ "right": 8
+ }
+ }
+ },
+ "input_editor": {
+ "background": "#ffffff",
+ "corner_radius": 6,
+ "text": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ },
+ "placeholder_text": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "padding": {
+ "bottom": 7,
+ "left": 8,
+ "right": 8,
+ "top": 7
+ }
+ }
+ },
+ "contacts_panel": {
+ "padding": {
+ "top": 12,
+ "left": 12,
+ "bottom": 12,
+ "right": 12
+ },
+ "host_row_height": 28,
+ "tree_branch_color": "#e3e3e3",
+ "tree_branch_width": 1,
+ "host_avatar": {
+ "corner_radius": 10,
+ "width": 18
+ },
+ "host_username": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14,
+ "padding": {
+ "left": 8
+ }
+ },
+ "project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ }
+ },
+ "shared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ },
+ "background": "#f8f8f8",
+ "corner_radius": 6
+ },
+ "hovered_shared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ },
+ "background": "#eaeaea",
+ "corner_radius": 6
+ },
+ "unshared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ }
+ },
+ "hovered_unshared_project": {
+ "guest_avatar_spacing": 4,
+ "height": 24,
+ "guest_avatar": {
+ "corner_radius": 8,
+ "width": 14
+ },
+ "name": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14,
+ "margin": {
+ "right": 6
+ }
+ },
+ "padding": {
+ "left": 8
+ },
+ "corner_radius": 6
+ }
+ },
+ "search": {
+ "match_background": "#fce9b7",
+ "tab_icon_spacing": 8,
+ "tab_icon_width": 14,
+ "active_hovered_option_button": {
+ "family": "Zed Mono",
+ "color": "#000000",
+ "size": 14,
+ "background": "#ffffff",
+ "corner_radius": 4,
+ "border": {
+ "color": "#e3e3e3",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "active_option_button": {
+ "family": "Zed Mono",
+ "color": "#000000",
+ "size": 14,
+ "background": "#ffffff",
+ "corner_radius": 4,
+ "border": {
+ "color": "#e3e3e3",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "editor": {
+ "background": "#ffffff",
+ "corner_radius": 8,
+ "min_width": 200,
+ "max_width": 500,
+ "placeholder_text": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "text": {
+ "family": "Zed Mono",
+ "color": "#000000",
+ "size": 14
+ },
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "margin": {
+ "right": 6
+ },
+ "padding": {
+ "top": 3,
+ "bottom": 3,
+ "left": 12,
+ "right": 8
+ }
+ },
+ "hovered_option_button": {
+ "family": "Zed Mono",
+ "color": "#000000",
+ "size": 14,
+ "background": "#f1f1f1",
+ "corner_radius": 4,
+ "border": {
+ "color": "#e3e3e3",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "invalid_editor": {
+ "background": "#ffffff",
+ "corner_radius": 8,
+ "min_width": 200,
+ "max_width": 500,
+ "placeholder_text": {
+ "family": "Zed Mono",
+ "color": "#808080",
+ "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
+ },
+ "text": {
+ "family": "Zed Mono",
+ "color": "#000000",
+ "size": 14
+ },
+ "border": {
+ "color": "#f9a0a0",
+ "width": 1
+ },
+ "margin": {
+ "right": 6
+ },
+ "padding": {
+ "top": 3,
+ "bottom": 3,
+ "left": 12,
+ "right": 8
+ }
+ },
+ "match_index": {
+ "family": "Zed Mono",
+ "color": "#636363",
+ "size": 14,
+ "padding": 6
+ },
+ "option_button": {
+ "family": "Zed Mono",
+ "color": "#474747",
+ "size": 14,
+ "background": "#f1f1f1",
+ "corner_radius": 4,
+ "border": {
+ "color": "#d5d5d5",
+ "width": 1
+ },
+ "margin": {
+ "left": 2,
+ "right": 2
+ },
+ "padding": {
+ "bottom": 3,
+ "left": 8,
+ "right": 8,
+ "top": 3
+ }
+ },
+ "option_button_group": {
+ "padding": {
+ "left": 4,
+ "right": 4
+ }
+ },
+ "results_status": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 18
+ }
+ },
+ "breadcrumbs": {
+ "family": "Zed Sans",
+ "color": "#474747",
+ "size": 14,
+ "padding": {
+ "left": 6
+ }
+ }
+}
@@ -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"] }
+
@@ -3,7 +3,7 @@ use gpui::AssetSource;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
-#[folder = "assets"]
+#[folder = "../../assets"]
#[exclude = "*.DS_Store"]
pub struct Assets;
@@ -8,9 +8,10 @@ path = "src/auto_update.rs"
doctest = false
[dependencies]
+client = { path = "../client" }
gpui = { path = "../gpui" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
-client = { path = "../client" }
workspace = { path = "../workspace" }
anyhow = "1.0.38"
lazy_static = "1.4"
@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use client::http::{self, HttpClient};
use gpui::{
- action,
+ actions,
elements::{Empty, MouseEventHandler, Text},
platform::AppVersion,
AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View,
@@ -9,10 +9,11 @@ use gpui::{
};
use lazy_static::lazy_static;
use serde::Deserialize;
+use settings::Settings;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
use surf::Request;
-use workspace::{ItemHandle, Settings, StatusItemView};
+use workspace::{ItemHandle, StatusItemView};
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
const ACCESS_TOKEN: &'static str = "618033988749894";
@@ -24,6 +25,8 @@ lazy_static! {
pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
}
+actions!(auto_update, [Check, DismissErrorMessage]);
+
#[derive(Clone, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
@@ -46,8 +49,6 @@ pub struct AutoUpdateIndicator {
updater: Option<ModelHandle<AutoUpdater>>,
}
-action!(DismissErrorMessage);
-
#[derive(Deserialize)]
struct JsonRelease {
version: String,
@@ -66,16 +67,15 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut Mutab
updater
});
cx.set_global(Some(auto_updater));
+ cx.add_global_action(|_: &Check, cx| {
+ if let Some(updater) = AutoUpdater::get(cx) {
+ updater.update(cx, |updater, cx| updater.poll(cx));
+ }
+ });
cx.add_action(AutoUpdateIndicator::dismiss_error_message);
}
}
-pub fn check(cx: &mut MutableAppContext) {
- if let Some(updater) = AutoUpdater::get(cx) {
- updater.update(cx, |updater, cx| updater.poll(cx));
- }
-}
-
impl AutoUpdater {
fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
cx.default_global::<Option<ModelHandle<Self>>>().clone()
@@ -14,6 +14,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
search = { path = "../search" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
@@ -6,8 +6,9 @@ use gpui::{
use language::{Buffer, OutlineItem};
use project::Project;
use search::ProjectSearchView;
+use settings::Settings;
use theme::SyntaxTheme;
-use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub enum Event {
UpdateLocation,
@@ -11,6 +11,7 @@ doctest = false
client = { path = "../client" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
@@ -4,19 +4,18 @@ use client::{
};
use editor::Editor;
use gpui::{
- action,
+ actions,
elements::*,
- keymap::Binding,
platform::CursorStyle,
views::{ItemType, Select, SelectStyle},
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
ViewContext, ViewHandle,
};
use postage::prelude::Stream;
+use settings::{Settings, SoftWrap};
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt};
-use workspace::{settings::SoftWrap, Settings};
const MESSAGE_LOADING_THRESHOLD: usize = 50;
@@ -33,14 +32,11 @@ pub struct ChatPanel {
pub enum Event {}
-action!(Send);
-action!(LoadMoreMessages);
+actions!(chat_panel, [Send, LoadMoreMessages]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ChatPanel::send);
cx.add_action(ChatPanel::load_more_messages);
-
- cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]);
}
impl ChatPanel {
@@ -0,0 +1,24 @@
+[package]
+name = "cli"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/cli.rs"
+doctest = false
+
+[[bin]]
+name = "cli"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1.0"
+clap = { version = "3.1", features = ["derive"] }
+dirs = "3.0"
+ipc-channel = "0.16"
+serde = { version = "1.0", features = ["derive"] }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+core-foundation = "0.9"
+core-services = "0.2"
+plist = "1.3"
@@ -0,0 +1,22 @@
+pub use ipc_channel::ipc;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+#[derive(Serialize, Deserialize)]
+pub struct IpcHandshake {
+ pub requests: ipc::IpcSender<CliRequest>,
+ pub responses: ipc::IpcReceiver<CliResponse>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum CliRequest {
+ Open { paths: Vec<PathBuf>, wait: bool },
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub enum CliResponse {
+ Ping,
+ Stdout { message: String },
+ Stderr { message: String },
+ Exit { status: i32 },
+}
@@ -0,0 +1,124 @@
+use anyhow::{anyhow, Result};
+use clap::Parser;
+use cli::{CliRequest, CliResponse, IpcHandshake};
+use core_foundation::{
+ array::{CFArray, CFIndex},
+ string::kCFStringEncodingUTF8,
+ url::{CFURLCreateWithBytes, CFURL},
+};
+use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
+use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
+use serde::Deserialize;
+use std::{ffi::OsStr, fs, path::PathBuf, ptr};
+
+#[derive(Parser)]
+#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
+struct Args {
+ /// Wait for all of the given paths to be closed before exiting.
+ #[clap(short, long)]
+ wait: bool,
+ /// A sequence of space-separated paths that you want to open.
+ #[clap()]
+ paths: Vec<PathBuf>,
+ /// Print Zed's version and the app path.
+ #[clap(short, long)]
+ version: bool,
+ /// Custom Zed.app path
+ #[clap(short, long)]
+ bundle_path: Option<PathBuf>,
+}
+
+#[derive(Debug, Deserialize)]
+struct InfoPlist {
+ #[serde(rename = "CFBundleShortVersionString")]
+ bundle_short_version_string: String,
+}
+
+fn main() -> Result<()> {
+ let args = Args::parse();
+
+ let bundle_path = if let Some(bundle_path) = args.bundle_path {
+ bundle_path.canonicalize()?
+ } else {
+ locate_bundle()?
+ };
+
+ if args.version {
+ let plist_path = bundle_path.join("Contents/Info.plist");
+ let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
+ println!(
+ "Zed {} – {}",
+ plist.bundle_short_version_string,
+ bundle_path.to_string_lossy()
+ );
+ return Ok(());
+ }
+
+ let (tx, rx) = launch_app(bundle_path)?;
+
+ tx.send(CliRequest::Open {
+ paths: args
+ .paths
+ .into_iter()
+ .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
+ .collect::<Result<Vec<PathBuf>>>()?,
+ wait: args.wait,
+ })?;
+
+ while let Ok(response) = rx.recv() {
+ match response {
+ CliResponse::Ping => {}
+ CliResponse::Stdout { message } => println!("{message}"),
+ CliResponse::Stderr { message } => eprintln!("{message}"),
+ CliResponse::Exit { status } => std::process::exit(status),
+ }
+ }
+
+ Ok(())
+}
+
+fn locate_bundle() -> Result<PathBuf> {
+ let cli_path = std::env::current_exe()?.canonicalize()?;
+ let mut app_path = cli_path.clone();
+ while app_path.extension() != Some(OsStr::new("app")) {
+ if !app_path.pop() {
+ return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
+ }
+ }
+ Ok(app_path)
+}
+
+fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
+ let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
+ let url = format!("zed-cli://{server_name}");
+
+ let status = unsafe {
+ let app_url =
+ CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
+ let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
+ ptr::null(),
+ url.as_ptr(),
+ url.len() as CFIndex,
+ kCFStringEncodingUTF8,
+ ptr::null(),
+ ));
+ let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
+ LSOpenFromURLSpec(
+ &LSLaunchURLSpec {
+ appURL: app_url.as_concrete_TypeRef(),
+ itemURLs: urls_to_open.as_concrete_TypeRef(),
+ passThruParams: ptr::null(),
+ launchFlags: kLSLaunchDefaults,
+ asyncRefCon: ptr::null_mut(),
+ },
+ ptr::null_mut(),
+ )
+ };
+
+ if status == 0 {
+ let (_, handshake) = server.accept()?;
+ Ok((handshake.requests, handshake.responses))
+ } else {
+ Err(anyhow!("cannot start {:?}", app_path))
+ }
+}
@@ -21,7 +21,7 @@ async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures = "0.3"
image = "0.23"
lazy_static = "1.4.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
@@ -13,7 +13,7 @@ use async_tungstenite::tungstenite::{
};
use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
use gpui::{
- action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
+ actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
@@ -50,7 +50,7 @@ lazy_static! {
.and_then(|s| if s.is_empty() { None } else { Some(s) });
}
-action!(Authenticate);
+actions!(client, [Authenticate]);
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action(move |_: &Authenticate, cx| {
@@ -6,7 +6,6 @@ use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
use gpui::{executor, ModelHandle, TestAppContext};
use parking_lot::Mutex;
-use postage::barrier;
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
use std::{fmt, rc::Rc, sync::Arc};
@@ -23,7 +22,6 @@ struct FakeServerState {
connection_id: Option<ConnectionId>,
forbid_connections: bool,
auth_count: usize,
- connection_killer: Option<barrier::Sender>,
access_token: usize,
}
@@ -76,15 +74,13 @@ impl FakeServer {
Err(EstablishConnectionError::Unauthorized)?
}
- let (client_conn, server_conn, kill) =
- Connection::in_memory(cx.background());
+ let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
let (connection_id, io, incoming) =
peer.add_test_connection(server_conn, cx.background()).await;
cx.background().spawn(io).detach();
let mut state = state.lock();
state.connection_id = Some(connection_id);
state.incoming = Some(incoming);
- state.connection_killer = Some(kill);
Ok(client_conn)
})
}
@@ -1,12 +1,12 @@
[package]
authors = ["Nathan Sobo <nathan@warp.dev>"]
-default-run = "zed-server"
+default-run = "collab"
edition = "2021"
-name = "zed-server"
+name = "collab"
version = "0.1.0"
[[bin]]
-name = "zed-server"
+name = "collab"
[[bin]]
name = "seed"
@@ -15,25 +15,27 @@ required-features = ["seed-support"]
[dependencies]
collections = { path = "../collections" }
rpc = { path = "../rpc" }
+util = { path = "../util" }
anyhow = "1.0.40"
async-io = "1.3"
async-std = { version = "1.8.0", features = ["attributes"] }
async-trait = "0.1.50"
async-tungstenite = "0.16"
base64 = "0.13"
-clap = "=3.0.0-beta.2"
+clap = "3.1"
comrak = "0.10"
either = "1.6"
envy = "0.4.2"
futures = "0.3"
handlebars = "3.5"
http-auth-basic = "0.1.3"
+json_env_logger = "0.1"
jwt-simple = "0.10.0"
lipsum = { version = "0.8", optional = true }
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
oauth2 = { version = "4.0.0", default_features = false }
oauth2-surf = "0.1.1"
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8"
rust-embed = { version = "6.3", features = ["include-exclude"] }
scrypt = "0.7"
@@ -64,6 +66,8 @@ editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"
@@ -1,2 +1,2 @@
-web: ./target/release/zed-server
+collab: ./target/release/collab
release: ./target/release/sqlx migrate run
@@ -8,14 +8,14 @@ kind: Service
apiVersion: v1
metadata:
namespace: ${ZED_KUBE_NAMESPACE}
- name: zed
+ name: collab
annotations:
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
- service.beta.kubernetes.io/do-loadbalancer-certificate-id: "2634d353-1ab4-437f-add2-4ffd8f315233"
+ service.beta.kubernetes.io/do-loadbalancer-certificate-id: "40879815-9a6b-4bbb-8207-8f2c7c0218f9"
spec:
type: LoadBalancer
selector:
- app: zed
+ app: collab
ports:
- name: web
protocol: TCP
@@ -26,19 +26,19 @@ apiVersion: apps/v1
kind: Deployment
metadata:
namespace: ${ZED_KUBE_NAMESPACE}
- name: zed
+ name: collab
spec:
replicas: 1
selector:
matchLabels:
- app: zed
+ app: collab
template:
metadata:
labels:
- app: zed
+ app: collab
spec:
containers:
- - name: zed
+ - name: collab
image: "${ZED_IMAGE_ID}"
ports:
- containerPort: 8080
@@ -81,6 +81,10 @@ spec:
secretKeyRef:
name: api
key: token
+ - name: LOG_JSON
+ value: "1"
+ - name: RUST_LOG
+ value: "trace"
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.
@@ -111,7 +111,6 @@ async fn create_access_token(request: Request) -> tide::Result {
.get_user_by_github_login(request.param("github_login")?)
.await?
.ok_or_else(|| surf::Error::from_str(StatusCode::NotFound, "user not found"))?;
- let access_token = auth::create_access_token(request.db().as_ref(), user.id).await?;
#[derive(Deserialize)]
struct QueryParams {
@@ -123,9 +122,6 @@ async fn create_access_token(request: Request) -> tide::Result {
surf::Error::from_str(StatusCode::UnprocessableEntity, "invalid query params")
})?;
- let encrypted_access_token =
- auth::encrypt_access_token(&access_token, query_params.public_key.clone())?;
-
let mut user_id = user.id;
if let Some(impersonate) = query_params.impersonate {
if user.admin {
@@ -151,6 +147,10 @@ async fn create_access_token(request: Request) -> tide::Result {
}
}
+ let access_token = auth::create_access_token(request.db().as_ref(), user_id).await?;
+ let encrypted_access_token =
+ auth::encrypt_access_token(&access_token, query_params.public_key.clone())?;
+
Ok(tide::Response::builder(StatusCode::Ok)
.body(json!({"user_id": user_id, "encrypted_access_token": encrypted_access_token}))
.build())
@@ -1,7 +1,8 @@
use crate::{AppState, Request, RequestExt as _};
-use serde::Deserialize;
+use log::as_serde;
+use serde::{Deserialize, Serialize};
use std::sync::Arc;
-use tide::{http::mime, log, Server};
+use tide::{http::mime, Server};
pub fn add_routes(app: &mut Server<Arc<AppState>>) {
app.at("/").get(get_home);
@@ -18,7 +19,7 @@ async fn get_home(mut request: Request) -> tide::Result {
}
async fn post_signup(mut request: Request) -> tide::Result {
- #[derive(Debug, Deserialize)]
+ #[derive(Debug, Deserialize, Serialize)]
struct Form {
github_login: String,
email_address: String,
@@ -38,7 +39,7 @@ async fn post_signup(mut request: Request) -> tide::Result {
.map(str::to_string)
.unwrap_or(form.github_login);
- log::info!("Signup submitted: {:?}", form);
+ log::info!(form = as_serde!(form); "signup submitted");
// Save signup in the database
request
@@ -27,7 +27,7 @@ use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use surf::http::cookies::SameSite;
-use tide::{log, sessions::SessionMiddleware};
+use tide::sessions::SessionMiddleware;
use tide_compress::CompressMiddleware;
type Request = tide::Request<Arc<AppState>>;
@@ -138,7 +138,11 @@ struct LayoutData {
#[async_std::main]
async fn main() -> tide::Result<()> {
- log::start();
+ if std::env::var("LOG_JSON").is_ok() {
+ json_env_logger::init();
+ } else {
+ tide::log::start();
+ }
if let Err(error) = env::load_dotenv() {
log::error!(
@@ -7,11 +7,14 @@ use super::{
};
use anyhow::anyhow;
use async_io::Timer;
-use async_std::task;
+use async_std::{
+ sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
+ task,
+};
use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
use collections::{HashMap, HashSet};
use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt};
-use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
+use log::{as_debug, as_display};
use rpc::{
proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage},
Connection, ConnectionId, Peer, TypedEnvelope,
@@ -20,17 +23,20 @@ use sha1::{Digest as _, Sha1};
use std::{
any::TypeId,
future::Future,
+ marker::PhantomData,
+ ops::{Deref, DerefMut},
+ rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use store::{Store, Worktree};
use surf::StatusCode;
-use tide::log;
use tide::{
http::headers::{HeaderName, CONNECTION, UPGRADE},
Request, Response,
};
use time::OffsetDateTime;
+use util::ResultExt;
type MessageHandler = Box<
dyn Send
@@ -58,6 +64,16 @@ pub struct RealExecutor;
const MESSAGE_COUNT_PER_PAGE: usize = 100;
const MAX_MESSAGE_LEN: usize = 1024;
+struct StoreReadGuard<'a> {
+ guard: RwLockReadGuard<'a, Store>,
+ _not_send: PhantomData<Rc<()>>,
+}
+
+struct StoreWriteGuard<'a> {
+ guard: RwLockWriteGuard<'a, Store>,
+ _not_send: PhantomData<Rc<()>>,
+}
+
impl Server {
pub fn new(
app_state: Arc<AppState>,
@@ -78,7 +94,7 @@ impl Server {
.add_message_handler(Server::unregister_project)
.add_request_handler(Server::share_project)
.add_message_handler(Server::unshare_project)
- .add_request_handler(Server::join_project)
+ .add_sync_request_handler(Server::join_project)
.add_message_handler(Server::leave_project)
.add_request_handler(Server::register_worktree)
.add_message_handler(Server::unregister_worktree)
@@ -170,6 +186,42 @@ impl Server {
})
}
+ /// Handle a request while holding a lock to the store. This is useful when we're registering
+ /// a connection but we want to respond on the connection before anybody else can send on it.
+ fn add_sync_request_handler<F, M>(&mut self, handler: F) -> &mut Self
+ where
+ F: 'static
+ + Send
+ + Sync
+ + Fn(Arc<Self>, &mut Store, TypedEnvelope<M>) -> tide::Result<M::Response>,
+ M: RequestMessage,
+ {
+ let handler = Arc::new(handler);
+ self.add_message_handler(move |server, envelope| {
+ let receipt = envelope.receipt();
+ let handler = handler.clone();
+ async move {
+ let mut store = server.store.write().await;
+ let response = (handler)(server.clone(), &mut *store, envelope);
+ match response {
+ Ok(response) => {
+ server.peer.respond(receipt, response)?;
+ Ok(())
+ }
+ Err(error) => {
+ server.peer.respond_with_error(
+ receipt,
+ proto::Error {
+ message: error.to_string(),
+ },
+ )?;
+ Err(error)
+ }
+ }
+ }
+ })
+ }
+
pub fn handle_connection<E: Executor>(
self: &Arc<Self>,
connection: Connection,
@@ -197,9 +249,10 @@ impl Server {
let _ = send_connection_id.send(connection_id).await;
}
- this.state_mut().add_connection(connection_id, user_id);
- if let Err(err) = this.update_contacts_for_users(&[user_id]) {
- log::error!("error updating contacts for {:?}: {}", user_id, err);
+ {
+ let mut state = this.state_mut().await;
+ state.add_connection(connection_id, user_id);
+ this.update_contacts_for_users(&*state, &[user_id]);
}
let handle_io = handle_io.fuse();
@@ -218,16 +271,16 @@ impl Server {
if let Some(message) = message {
let start_time = Instant::now();
let type_name = message.payload_type_name();
- log::info!("rpc message received. connection:{}, type:{}", connection_id, type_name);
+ log::info!(connection_id = connection_id.0, type_name = type_name; "rpc message received");
if let Some(handler) = this.handlers.get(&message.payload_type_id()) {
let notifications = this.notifications.clone();
let is_background = message.is_background();
let handle_message = (handler)(this.clone(), message);
let handle_message = async move {
if let Err(err) = handle_message.await {
- log::error!("rpc message error. connection:{}, type:{}, error:{:?}", connection_id, type_name, err);
+ log::error!(connection_id = connection_id.0, type = type_name, error = as_display!(err); "rpc message error");
} else {
- log::info!("rpc message handled. connection:{}, type:{}, duration:{:?}", connection_id, type_name, start_time.elapsed());
+ log::info!(connection_id = connection_id.0, type = type_name, duration = as_debug!(start_time.elapsed()); "rpc message handled");
}
if let Some(mut notifications) = notifications {
let _ = notifications.send(()).await;
@@ -242,7 +295,7 @@ impl Server {
log::warn!("unhandled message: {}", type_name);
}
} else {
- log::info!("rpc connection closed {:?}", addr);
+ log::info!(address = as_debug!(addr); "rpc connection closed");
break;
}
}
@@ -257,7 +310,8 @@ impl Server {
async fn sign_out(self: &mut Arc<Self>, connection_id: ConnectionId) -> tide::Result<()> {
self.peer.disconnect(connection_id);
- let removed_connection = self.state_mut().remove_connection(connection_id)?;
+ let mut state = self.state_mut().await;
+ let removed_connection = state.remove_connection(connection_id)?;
for (project_id, project) in removed_connection.hosted_projects {
if let Some(share) = project.share {
@@ -268,7 +322,7 @@ impl Server {
self.peer
.send(conn_id, proto::UnshareProject { project_id })
},
- )?;
+ );
}
}
@@ -281,10 +335,10 @@ impl Server {
peer_id: connection_id.0,
},
)
- })?;
+ });
}
- self.update_contacts_for_users(removed_connection.contact_ids.iter())?;
+ self.update_contacts_for_users(&*state, removed_connection.contact_ids.iter());
Ok(())
}
@@ -293,11 +347,11 @@ impl Server {
}
async fn register_project(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::RegisterProject>,
) -> tide::Result<proto::RegisterProjectResponse> {
let project_id = {
- let mut state = self.state_mut();
+ let mut state = self.state_mut().await;
let user_id = state.user_id_for_connection(request.sender_id)?;
state.register_project(request.sender_id, user_id)
};
@@ -305,51 +359,49 @@ impl Server {
}
async fn unregister_project(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::UnregisterProject>,
) -> tide::Result<()> {
- let project = self
- .state_mut()
- .unregister_project(request.payload.project_id, request.sender_id)?;
- self.update_contacts_for_users(project.authorized_user_ids().iter())?;
+ let mut state = self.state_mut().await;
+ let project = state.unregister_project(request.payload.project_id, request.sender_id)?;
+ self.update_contacts_for_users(&*state, &project.authorized_user_ids());
Ok(())
}
async fn share_project(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::ShareProject>,
) -> tide::Result<proto::Ack> {
- self.state_mut()
- .share_project(request.payload.project_id, request.sender_id);
+ let mut state = self.state_mut().await;
+ let project = state.share_project(request.payload.project_id, request.sender_id)?;
+ self.update_contacts_for_users(&mut *state, &project.authorized_user_ids);
Ok(proto::Ack {})
}
async fn unshare_project(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::UnshareProject>,
) -> tide::Result<()> {
let project_id = request.payload.project_id;
- let project = self
- .state_mut()
- .unshare_project(project_id, request.sender_id)?;
-
+ let mut state = self.state_mut().await;
+ let project = state.unshare_project(project_id, request.sender_id)?;
broadcast(request.sender_id, project.connection_ids, |conn_id| {
self.peer
.send(conn_id, proto::UnshareProject { project_id })
- })?;
- self.update_contacts_for_users(&project.authorized_user_ids)?;
+ });
+ self.update_contacts_for_users(&mut *state, &project.authorized_user_ids);
Ok(())
}
- async fn join_project(
- mut self: Arc<Server>,
+ fn join_project(
+ self: Arc<Server>,
+ state: &mut Store,
request: TypedEnvelope<proto::JoinProject>,
) -> tide::Result<proto::JoinProjectResponse> {
let project_id = request.payload.project_id;
- let user_id = self.state().user_id_for_connection(request.sender_id)?;
- let (response, connection_ids, contact_user_ids) = self
- .state_mut()
+ let user_id = state.user_id_for_connection(request.sender_id)?;
+ let (response, connection_ids, contact_user_ids) = state
.join_project(request.sender_id, user_id, project_id)
.and_then(|joined| {
let share = joined.project.share()?;
@@ -410,19 +462,19 @@ impl Server {
}),
},
)
- })?;
- self.update_contacts_for_users(&contact_user_ids)?;
+ });
+ self.update_contacts_for_users(state, &contact_user_ids);
Ok(response)
}
async fn leave_project(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::LeaveProject>,
) -> tide::Result<()> {
let sender_id = request.sender_id;
let project_id = request.payload.project_id;
- let worktree = self.state_mut().leave_project(sender_id, project_id)?;
-
+ let mut state = self.state_mut().await;
+ let worktree = state.leave_project(sender_id, project_id)?;
broadcast(sender_id, worktree.connection_ids, |conn_id| {
self.peer.send(
conn_id,
@@ -431,60 +483,57 @@ impl Server {
peer_id: sender_id.0,
},
)
- })?;
- self.update_contacts_for_users(&worktree.authorized_user_ids)?;
-
+ });
+ self.update_contacts_for_users(&*state, &worktree.authorized_user_ids);
Ok(())
}
async fn register_worktree(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::RegisterWorktree>,
) -> tide::Result<proto::Ack> {
- let host_user_id = self.state().user_id_for_connection(request.sender_id)?;
-
let mut contact_user_ids = HashSet::default();
- contact_user_ids.insert(host_user_id);
for github_login in &request.payload.authorized_logins {
let contact_user_id = self.app_state.db.create_user(github_login, false).await?;
contact_user_ids.insert(contact_user_id);
}
+ let mut state = self.state_mut().await;
+ let host_user_id = state.user_id_for_connection(request.sender_id)?;
+ contact_user_ids.insert(host_user_id);
+
let contact_user_ids = contact_user_ids.into_iter().collect::<Vec<_>>();
- let guest_connection_ids;
- {
- let mut state = self.state_mut();
- guest_connection_ids = state
- .read_project(request.payload.project_id, request.sender_id)?
- .guest_connection_ids();
- state.register_worktree(
- request.payload.project_id,
- request.payload.worktree_id,
- request.sender_id,
- Worktree {
- authorized_user_ids: contact_user_ids.clone(),
- root_name: request.payload.root_name.clone(),
- visible: request.payload.visible,
- },
- )?;
- }
+ let guest_connection_ids = state
+ .read_project(request.payload.project_id, request.sender_id)?
+ .guest_connection_ids();
+ state.register_worktree(
+ request.payload.project_id,
+ request.payload.worktree_id,
+ request.sender_id,
+ Worktree {
+ authorized_user_ids: contact_user_ids.clone(),
+ root_name: request.payload.root_name.clone(),
+ visible: request.payload.visible,
+ },
+ )?;
+
broadcast(request.sender_id, guest_connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
- self.update_contacts_for_users(&contact_user_ids)?;
+ });
+ self.update_contacts_for_users(&*state, &contact_user_ids);
Ok(proto::Ack {})
}
async fn unregister_worktree(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::UnregisterWorktree>,
) -> tide::Result<()> {
let project_id = request.payload.project_id;
let worktree_id = request.payload.worktree_id;
+ let mut state = self.state_mut().await;
let (worktree, guest_connection_ids) =
- self.state_mut()
- .unregister_worktree(project_id, worktree_id, request.sender_id)?;
+ state.unregister_worktree(project_id, worktree_id, request.sender_id)?;
broadcast(request.sender_id, guest_connection_ids, |conn_id| {
self.peer.send(
conn_id,
@@ -493,16 +542,16 @@ impl Server {
worktree_id,
},
)
- })?;
- self.update_contacts_for_users(&worktree.authorized_user_ids)?;
+ });
+ self.update_contacts_for_users(&*state, &worktree.authorized_user_ids);
Ok(())
}
async fn update_worktree(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::UpdateWorktree>,
) -> tide::Result<proto::Ack> {
- let connection_ids = self.state_mut().update_worktree(
+ let connection_ids = self.state_mut().await.update_worktree(
request.sender_id,
request.payload.project_id,
request.payload.worktree_id,
@@ -513,13 +562,13 @@ impl Server {
broadcast(request.sender_id, connection_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(proto::Ack {})
}
async fn update_diagnostic_summary(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::UpdateDiagnosticSummary>,
) -> tide::Result<()> {
let summary = request
@@ -527,7 +576,7 @@ impl Server {
.summary
.clone()
.ok_or_else(|| anyhow!("invalid summary"))?;
- let receiver_ids = self.state_mut().update_diagnostic_summary(
+ let receiver_ids = self.state_mut().await.update_diagnostic_summary(
request.payload.project_id,
request.payload.worktree_id,
request.sender_id,
@@ -537,15 +586,15 @@ impl Server {
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(())
}
async fn start_language_server(
- mut self: Arc<Server>,
+ self: Arc<Server>,
request: TypedEnvelope<proto::StartLanguageServer>,
) -> tide::Result<()> {
- let receiver_ids = self.state_mut().start_language_server(
+ let receiver_ids = self.state_mut().await.start_language_server(
request.payload.project_id,
request.sender_id,
request
@@ -557,7 +606,7 @@ impl Server {
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(())
}
@@ -567,11 +616,12 @@ impl Server {
) -> tide::Result<()> {
let receiver_ids = self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(())
}
@@ -584,6 +634,7 @@ impl Server {
{
let host_connection_id = self
.state()
+ .await
.read_project(request.payload.remote_entity_id(), request.sender_id)?
.host_connection_id;
Ok(self
@@ -596,24 +647,25 @@ impl Server {
self: Arc<Server>,
request: TypedEnvelope<proto::SaveBuffer>,
) -> tide::Result<proto::BufferSaved> {
- let host;
- let mut guests;
- {
- let state = self.state();
- let project = state.read_project(request.payload.project_id, request.sender_id)?;
- host = project.host_connection_id;
- guests = project.guest_connection_ids()
- }
-
+ let host = self
+ .state()
+ .await
+ .read_project(request.payload.project_id, request.sender_id)?
+ .host_connection_id;
let response = self
.peer
.forward_request(request.sender_id, host, request.payload.clone())
.await?;
+ let mut guests = self
+ .state()
+ .await
+ .read_project(request.payload.project_id, request.sender_id)?
+ .connection_ids();
guests.retain(|guest_connection_id| *guest_connection_id != request.sender_id);
broadcast(host, guests, |conn_id| {
self.peer.forward_send(host, conn_id, response.clone())
- })?;
+ });
Ok(response)
}
@@ -624,11 +676,12 @@ impl Server {
) -> tide::Result<proto::Ack> {
let receiver_ids = self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(proto::Ack {})
}
@@ -638,11 +691,12 @@ impl Server {
) -> tide::Result<()> {
let receiver_ids = self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(())
}
@@ -652,11 +706,12 @@ impl Server {
) -> tide::Result<()> {
let receiver_ids = self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(())
}
@@ -666,11 +721,12 @@ impl Server {
) -> tide::Result<()> {
let receiver_ids = self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
- })?;
+ });
Ok(())
}
@@ -682,6 +738,7 @@ impl Server {
let follower_id = request.sender_id;
if !self
.state()
+ .await
.project_connection_ids(request.payload.project_id, follower_id)?
.contains(&leader_id)
{
@@ -704,6 +761,7 @@ impl Server {
let leader_id = ConnectionId(request.payload.leader_id);
if !self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?
.contains(&leader_id)
{
@@ -720,6 +778,7 @@ impl Server {
) -> tide::Result<()> {
let connection_ids = self
.state()
+ .await
.project_connection_ids(request.payload.project_id, request.sender_id)?;
let leader_id = request
.payload
@@ -744,7 +803,10 @@ impl Server {
self: Arc<Server>,
request: TypedEnvelope<proto::GetChannels>,
) -> tide::Result<proto::GetChannelsResponse> {
- let user_id = self.state().user_id_for_connection(request.sender_id)?;
+ let user_id = self
+ .state()
+ .await
+ .user_id_for_connection(request.sender_id)?;
let channels = self.app_state.db.get_accessible_channels(user_id).await?;
Ok(proto::GetChannelsResponse {
channels: channels
@@ -783,32 +845,33 @@ impl Server {
}
fn update_contacts_for_users<'a>(
- self: &Arc<Server>,
+ self: &Arc<Self>,
+ state: &Store,
user_ids: impl IntoIterator<Item = &'a UserId>,
- ) -> anyhow::Result<()> {
- let mut result = Ok(());
- let state = self.state();
+ ) {
for user_id in user_ids {
let contacts = state.contacts_for_user(*user_id);
for connection_id in state.connection_ids_for_user(*user_id) {
- if let Err(error) = self.peer.send(
- connection_id,
- proto::UpdateContacts {
- contacts: contacts.clone(),
- },
- ) {
- result = Err(error);
- }
+ self.peer
+ .send(
+ connection_id,
+ proto::UpdateContacts {
+ contacts: contacts.clone(),
+ },
+ )
+ .log_err();
}
}
- result
}
async fn join_channel(
- mut self: Arc<Self>,
+ self: Arc<Self>,
request: TypedEnvelope<proto::JoinChannel>,
) -> tide::Result<proto::JoinChannelResponse> {
- let user_id = self.state().user_id_for_connection(request.sender_id)?;
+ let user_id = self
+ .state()
+ .await
+ .user_id_for_connection(request.sender_id)?;
let channel_id = ChannelId::from_proto(request.payload.channel_id);
if !self
.app_state
@@ -819,7 +882,9 @@ impl Server {
Err(anyhow!("access denied"))?;
}
- self.state_mut().join_channel(request.sender_id, channel_id);
+ self.state_mut()
+ .await
+ .join_channel(request.sender_id, channel_id);
let messages = self
.app_state
.db
@@ -841,10 +906,13 @@ impl Server {
}
async fn leave_channel(
- mut self: Arc<Self>,
+ self: Arc<Self>,
request: TypedEnvelope<proto::LeaveChannel>,
) -> tide::Result<()> {
- let user_id = self.state().user_id_for_connection(request.sender_id)?;
+ let user_id = self
+ .state()
+ .await
+ .user_id_for_connection(request.sender_id)?;
let channel_id = ChannelId::from_proto(request.payload.channel_id);
if !self
.app_state
@@ -856,6 +924,7 @@ impl Server {
}
self.state_mut()
+ .await
.leave_channel(request.sender_id, channel_id);
Ok(())
@@ -869,7 +938,7 @@ impl Server {
let user_id;
let connection_ids;
{
- let state = self.state();
+ let state = self.state().await;
user_id = state.user_id_for_connection(request.sender_id)?;
connection_ids = state.channel_connection_ids(channel_id)?;
}
@@ -910,7 +979,7 @@ impl Server {
message: Some(message.clone()),
},
)
- })?;
+ });
Ok(proto::SendChannelMessageResponse {
message: Some(message),
})
@@ -920,7 +989,10 @@ impl Server {
self: Arc<Self>,
request: TypedEnvelope<proto::GetChannelMessages>,
) -> tide::Result<proto::GetChannelMessagesResponse> {
- let user_id = self.state().user_id_for_connection(request.sender_id)?;
+ let user_id = self
+ .state()
+ .await
+ .user_id_for_connection(request.sender_id)?;
let channel_id = ChannelId::from_proto(request.payload.channel_id);
if !self
.app_state
@@ -956,12 +1028,57 @@ impl Server {
})
}
- fn state<'a>(self: &'a Arc<Self>) -> RwLockReadGuard<'a, Store> {
- self.store.read()
+ async fn state<'a>(self: &'a Arc<Self>) -> StoreReadGuard<'a> {
+ #[cfg(test)]
+ async_std::task::yield_now().await;
+ let guard = self.store.read().await;
+ #[cfg(test)]
+ async_std::task::yield_now().await;
+ StoreReadGuard {
+ guard,
+ _not_send: PhantomData,
+ }
+ }
+
+ async fn state_mut<'a>(self: &'a Arc<Self>) -> StoreWriteGuard<'a> {
+ #[cfg(test)]
+ async_std::task::yield_now().await;
+ let guard = self.store.write().await;
+ #[cfg(test)]
+ async_std::task::yield_now().await;
+ StoreWriteGuard {
+ guard,
+ _not_send: PhantomData,
+ }
}
+}
+
+impl<'a> Deref for StoreReadGuard<'a> {
+ type Target = Store;
+
+ fn deref(&self) -> &Self::Target {
+ &*self.guard
+ }
+}
+
+impl<'a> Deref for StoreWriteGuard<'a> {
+ type Target = Store;
+
+ fn deref(&self) -> &Self::Target {
+ &*self.guard
+ }
+}
+
+impl<'a> DerefMut for StoreWriteGuard<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut *self.guard
+ }
+}
- fn state_mut<'a>(self: &'a mut Arc<Self>) -> RwLockWriteGuard<'a, Store> {
- self.store.write()
+impl<'a> Drop for StoreWriteGuard<'a> {
+ fn drop(&mut self) {
+ #[cfg(test)]
+ self.check_invariants();
}
}
@@ -977,25 +1094,15 @@ impl Executor for RealExecutor {
}
}
-fn broadcast<F>(
- sender_id: ConnectionId,
- receiver_ids: Vec<ConnectionId>,
- mut f: F,
-) -> anyhow::Result<()>
+fn broadcast<F>(sender_id: ConnectionId, receiver_ids: Vec<ConnectionId>, mut f: F)
where
F: FnMut(ConnectionId) -> anyhow::Result<()>,
{
- let mut result = Ok(());
for receiver_id in receiver_ids {
if receiver_id != sender_id {
- if let Err(error) = f(receiver_id) {
- if result.is_ok() {
- result = Err(error);
- }
- }
+ f(receiver_id).log_err();
}
}
- result
}
pub fn add_routes(app: &mut tide::Server<Arc<AppState>>, rpc: &Arc<Peer>) {
@@ -1080,21 +1187,24 @@ mod tests {
use ::rpc::Peer;
use client::{
self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials,
- EstablishConnectionError, UserStore,
+ EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
};
use collections::BTreeMap;
use editor::{
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
ToOffset, ToggleCodeActions, Undo,
};
- use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
+ use gpui::{
+ executor::{self, Deterministic},
+ geometry::vector::vec2f,
+ ModelHandle, TestAppContext, ViewHandle,
+ };
use language::{
range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
};
use lsp::{self, FakeLanguageServer};
use parking_lot::Mutex;
- use postage::barrier;
use project::{
fs::{FakeFs, Fs as _},
search::SearchQuery,
@@ -1104,9 +1214,9 @@ mod tests {
use rand::prelude::*;
use rpc::PeerId;
use serde_json::json;
+ use settings::Settings;
use sqlx::types::time::OffsetDateTime;
use std::{
- cell::Cell,
env,
ops::Deref,
path::{Path, PathBuf},
@@ -1117,7 +1227,8 @@ mod tests {
},
time::Duration,
};
- use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
+ use theme::ThemeRegistry;
+ use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
#[cfg(test)]
#[ctor::ctor]
@@ -2252,6 +2363,25 @@ mod tests {
]
);
});
+
+ // Simulate a language server reporting no errors for a file.
+ fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
+ lsp::PublishDiagnosticsParams {
+ uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
+ version: None,
+ diagnostics: vec![],
+ },
+ );
+ project_a
+ .condition(cx_a, |project, cx| {
+ project.diagnostic_summaries(cx).collect::<Vec<_>>() == &[]
+ })
+ .await;
+ project_b
+ .condition(cx_b, |project, cx| {
+ project.diagnostic_summaries(cx).collect::<Vec<_>>() == &[]
+ })
+ .await;
}
#[gpui::test(iterations = 10)]
@@ -2417,7 +2547,7 @@ mod tests {
.condition(&cx_b, |editor, _| editor.context_menu_visible())
.await;
editor_b.update(cx_b, |editor, cx| {
- editor.confirm_completion(&ConfirmCompletion(Some(0)), cx);
+ editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
});
@@ -3606,7 +3736,12 @@ mod tests {
// Toggle code actions and wait for them to display.
editor_b.update(cx_b, |editor, cx| {
- editor.toggle_code_actions(&ToggleCodeActions(false), cx);
+ editor.toggle_code_actions(
+ &ToggleCodeActions {
+ deployed_from_indicator: false,
+ },
+ cx,
+ );
});
editor_b
.condition(&cx_b, |editor, _| editor.context_menu_visible())
@@ -3617,7 +3752,7 @@ mod tests {
// Confirming the code action will trigger a resolve request.
let confirm_action = workspace_b
.update(cx_b, |workspace, cx| {
- Editor::confirm_code_action(workspace, &ConfirmCodeAction(Some(0)), cx)
+ Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
})
.unwrap();
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
@@ -4349,19 +4484,19 @@ mod tests {
client_a
.user_store
.condition(&cx_a, |user_store, _| {
- contacts(user_store) == vec![("user_a", vec![("a", vec![])])]
+ contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])]
})
.await;
client_b
.user_store
.condition(&cx_b, |user_store, _| {
- contacts(user_store) == vec![("user_a", vec![("a", vec![])])]
+ contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])]
})
.await;
client_c
.user_store
.condition(&cx_c, |user_store, _| {
- contacts(user_store) == vec![("user_a", vec![("a", vec![])])]
+ contacts(user_store) == vec![("user_a", vec![("a", false, vec![])])]
})
.await;
@@ -4372,6 +4507,24 @@ mod tests {
.update(cx_a, |project, cx| project.share(cx))
.await
.unwrap();
+ client_a
+ .user_store
+ .condition(&cx_a, |user_store, _| {
+ contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])]
+ })
+ .await;
+ client_b
+ .user_store
+ .condition(&cx_b, |user_store, _| {
+ contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])]
+ })
+ .await;
+ client_c
+ .user_store
+ .condition(&cx_c, |user_store, _| {
+ contacts(user_store) == vec![("user_a", vec![("a", true, vec![])])]
+ })
+ .await;
let _project_b = Project::remote(
project_id,
@@ -4387,19 +4540,19 @@ mod tests {
client_a
.user_store
.condition(&cx_a, |user_store, _| {
- contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])]
+ contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])]
})
.await;
client_b
.user_store
.condition(&cx_b, |user_store, _| {
- contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])]
+ contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])]
})
.await;
client_c
.user_store
.condition(&cx_c, |user_store, _| {
- contacts(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])]
+ contacts(user_store) == vec![("user_a", vec![("a", true, vec!["user_b"])])]
})
.await;
@@ -4423,7 +4576,7 @@ mod tests {
.condition(&cx_c, |user_store, _| contacts(user_store) == vec![])
.await;
- fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, Vec<&str>)>)> {
+ fn contacts(user_store: &UserStore) -> Vec<(&str, Vec<(&str, bool, Vec<&str>)>)> {
user_store
.contacts()
.iter()
@@ -4434,6 +4587,7 @@ mod tests {
.map(|p| {
(
p.worktree_root_names[0].as_str(),
+ p.is_shared,
p.guests.iter().map(|p| p.github_login.as_str()).collect(),
)
})
@@ -66,6 +66,10 @@ pub struct JoinedProject<'a> {
pub project: &'a Project,
}
+pub struct SharedProject {
+ pub authorized_user_ids: Vec<UserId>,
+}
+
pub struct UnsharedProject {
pub connection_ids: Vec<ConnectionId>,
pub authorized_user_ids: Vec<UserId>,
@@ -130,9 +134,6 @@ impl Store {
}
}
- #[cfg(test)]
- self.check_invariants();
-
Ok(result)
}
@@ -244,6 +245,9 @@ impl Store {
language_servers: Default::default(),
},
);
+ if let Some(connection) = self.connections.get_mut(&host_connection_id) {
+ connection.projects.insert(project_id);
+ }
self.next_project_id += 1;
project_id
}
@@ -266,16 +270,12 @@ impl Store {
.or_default()
.insert(project_id);
}
- if let Some(connection) = self.connections.get_mut(&project.host_connection_id) {
- connection.projects.insert(project_id);
- }
+
project.worktrees.insert(worktree_id, worktree);
if let Ok(share) = project.share_mut() {
share.worktrees.insert(worktree_id, Default::default());
}
- #[cfg(test)]
- self.check_invariants();
Ok(())
} else {
Err(anyhow!("no such project"))?
@@ -312,8 +312,6 @@ impl Store {
}
}
- #[cfg(test)]
- self.check_invariants();
Ok(project)
} else {
Err(anyhow!("no such project"))?
@@ -358,13 +356,14 @@ impl Store {
}
}
- #[cfg(test)]
- self.check_invariants();
-
Ok((worktree, guest_connection_ids))
}
- pub fn share_project(&mut self, project_id: u64, connection_id: ConnectionId) -> bool {
+ pub fn share_project(
+ &mut self,
+ project_id: u64,
+ connection_id: ConnectionId,
+ ) -> tide::Result<SharedProject> {
if let Some(project) = self.projects.get_mut(&project_id) {
if project.host_connection_id == connection_id {
let mut share = ProjectShare::default();
@@ -372,10 +371,12 @@ impl Store {
share.worktrees.insert(*worktree_id, Default::default());
}
project.share = Some(share);
- return true;
+ return Ok(SharedProject {
+ authorized_user_ids: project.authorized_user_ids(),
+ });
}
}
- false
+ Err(anyhow!("no such project"))?
}
pub fn unshare_project(
@@ -402,9 +403,6 @@ impl Store {
}
}
- #[cfg(test)]
- self.check_invariants();
-
Ok(UnsharedProject {
connection_ids,
authorized_user_ids,
@@ -490,9 +488,6 @@ impl Store {
share.active_replica_ids.insert(replica_id);
share.guests.insert(connection_id, (replica_id, user_id));
- #[cfg(test)]
- self.check_invariants();
-
Ok(JoinedProject {
replica_id,
project: &self.projects[&project_id],
@@ -525,9 +520,6 @@ impl Store {
let connection_ids = project.connection_ids();
let authorized_user_ids = project.authorized_user_ids();
- #[cfg(test)]
- self.check_invariants();
-
Ok(LeftProject {
connection_ids,
authorized_user_ids,
@@ -555,10 +547,6 @@ impl Store {
worktree.entries.insert(entry.id, entry.clone());
}
let connection_ids = project.connection_ids();
-
- #[cfg(test)]
- self.check_invariants();
-
Ok(connection_ids)
}
@@ -632,7 +620,7 @@ impl Store {
}
#[cfg(test)]
- fn check_invariants(&self) {
+ pub fn check_invariants(&self) {
for (connection_id, connection) in &self.connections {
for project_id in &connection.projects {
let project = &self.projects.get(&project_id).unwrap();
@@ -0,0 +1,26 @@
+[package]
+name = "command_palette"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/command_palette.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+picker = { path = "../picker" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"
@@ -0,0 +1,362 @@
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+ actions,
+ elements::{ChildView, Flex, Label, ParentElement},
+ keymap::Keystroke,
+ Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle,
+};
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
+use std::cmp;
+use workspace::Workspace;
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(CommandPalette::toggle);
+ Picker::<CommandPalette>::init(cx);
+}
+
+actions!(command_palette, [Toggle]);
+
+pub struct CommandPalette {
+ picker: ViewHandle<Picker<Self>>,
+ actions: Vec<Command>,
+ matches: Vec<StringMatch>,
+ selected_ix: usize,
+ focused_view_id: usize,
+}
+
+pub enum Event {
+ Dismissed,
+ Confirmed {
+ window_id: usize,
+ focused_view_id: usize,
+ action: Box<dyn Action>,
+ },
+}
+
+struct Command {
+ name: String,
+ action: Box<dyn Action>,
+ keystrokes: Vec<Keystroke>,
+}
+
+impl CommandPalette {
+ pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
+ let this = cx.weak_handle();
+ let actions = cx
+ .available_actions(cx.window_id(), focused_view_id)
+ .map(|(name, action, bindings)| Command {
+ name: humanize_action_name(name),
+ action,
+ keystrokes: bindings
+ .last()
+ .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
+ })
+ .collect();
+ let picker = cx.add_view(|cx| Picker::new(this, cx));
+ Self {
+ picker,
+ actions,
+ matches: vec![],
+ selected_ix: 0,
+ focused_view_id,
+ }
+ }
+
+ fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+ let workspace = cx.handle();
+ let window_id = cx.window_id();
+ let focused_view_id = cx.focused_view_id(window_id).unwrap_or(workspace.id());
+
+ cx.as_mut().defer(move |cx| {
+ let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx));
+ workspace.update(cx, |workspace, cx| {
+ workspace.toggle_modal(cx, |cx, _| {
+ cx.subscribe(&this, Self::on_event).detach();
+ this
+ });
+ });
+ });
+ }
+
+ fn on_event(
+ workspace: &mut Workspace,
+ _: ViewHandle<Self>,
+ event: &Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ match event {
+ Event::Dismissed => workspace.dismiss_modal(cx),
+ Event::Confirmed {
+ window_id,
+ focused_view_id,
+ action,
+ } => {
+ let window_id = *window_id;
+ let focused_view_id = *focused_view_id;
+ let action = (*action).boxed_clone();
+ workspace.dismiss_modal(cx);
+ cx.as_mut()
+ .defer(move |cx| cx.dispatch_action_at(window_id, focused_view_id, &*action))
+ }
+ }
+ }
+}
+
+impl Entity for CommandPalette {
+ type Event = Event;
+}
+
+impl View for CommandPalette {
+ fn ui_name() -> &'static str {
+ "CommandPalette"
+ }
+
+ fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+ ChildView::new(self.picker.clone()).boxed()
+ }
+
+ fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+ cx.focus(&self.picker);
+ }
+}
+
+impl PickerDelegate for CommandPalette {
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_ix
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+ self.selected_ix = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut gpui::ViewContext<Self>,
+ ) -> gpui::Task<()> {
+ let candidates = self
+ .actions
+ .iter()
+ .enumerate()
+ .map(|(ix, command)| StringMatchCandidate {
+ id: ix,
+ string: command.name.to_string(),
+ char_bag: command.name.chars().collect(),
+ })
+ .collect::<Vec<_>>();
+ cx.spawn(move |this, mut cx| async move {
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ 10000,
+ &Default::default(),
+ cx.background(),
+ )
+ .await
+ };
+ this.update(&mut cx, |this, _| {
+ this.matches = matches;
+ if this.matches.is_empty() {
+ this.selected_ix = 0;
+ } else {
+ this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
+ }
+ });
+ })
+ }
+
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Dismissed);
+ }
+
+ fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+ if !self.matches.is_empty() {
+ let action_ix = self.matches[self.selected_ix].candidate_id;
+ cx.emit(Event::Confirmed {
+ window_id: cx.window_id(),
+ focused_view_id: self.focused_view_id,
+ action: self.actions.remove(action_ix).action,
+ });
+ } else {
+ cx.emit(Event::Dismissed);
+ }
+ }
+
+ fn render_match(&self, ix: usize, selected: bool, cx: &gpui::AppContext) -> gpui::ElementBox {
+ let mat = &self.matches[ix];
+ let command = &self.actions[mat.candidate_id];
+ let settings = cx.global::<Settings>();
+ let theme = &settings.theme;
+ let style = if selected {
+ &theme.selector.active_item
+ } else {
+ &theme.selector.item
+ };
+ let key_style = &theme.command_palette.key;
+ let keystroke_spacing = theme.command_palette.keystroke_spacing;
+
+ Flex::row()
+ .with_child(
+ Label::new(mat.string.clone(), style.label.clone())
+ .with_highlights(mat.positions.clone())
+ .boxed(),
+ )
+ .with_children(command.keystrokes.iter().map(|keystroke| {
+ Flex::row()
+ .with_children(
+ [
+ (keystroke.ctrl, "^"),
+ (keystroke.alt, "⎇"),
+ (keystroke.cmd, "⌘"),
+ (keystroke.shift, "⇧"),
+ ]
+ .into_iter()
+ .filter_map(|(modifier, label)| {
+ if modifier {
+ Some(
+ Label::new(label.into(), key_style.label.clone())
+ .contained()
+ .with_style(key_style.container)
+ .boxed(),
+ )
+ } else {
+ None
+ }
+ }),
+ )
+ .with_child(
+ Label::new(keystroke.key.clone(), key_style.label.clone())
+ .contained()
+ .with_style(key_style.container)
+ .boxed(),
+ )
+ .contained()
+ .with_margin_left(keystroke_spacing)
+ .flex_float()
+ .boxed()
+ }))
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ }
+}
+
+fn humanize_action_name(name: &str) -> String {
+ let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
+ let mut result = String::with_capacity(capacity);
+ for char in name.chars() {
+ if char == ':' {
+ if result.ends_with(':') {
+ result.push(' ');
+ } else {
+ result.push(':');
+ }
+ } else if char.is_uppercase() {
+ if !result.ends_with(' ') {
+ result.push(' ');
+ }
+ result.extend(char.to_lowercase());
+ } else {
+ result.push(char);
+ }
+ }
+ result
+}
+
+impl std::fmt::Debug for Command {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Command")
+ .field("name", &self.name)
+ .field("keystrokes", &self.keystrokes)
+ .finish()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use editor::Editor;
+ use gpui::TestAppContext;
+ use workspace::{Workspace, WorkspaceParams};
+
+ #[test]
+ fn test_humanize_action_name() {
+ assert_eq!(
+ &humanize_action_name("editor::GoToDefinition"),
+ "editor: go to definition"
+ );
+ assert_eq!(
+ &humanize_action_name("editor::Backspace"),
+ "editor: backspace"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_command_palette(cx: &mut TestAppContext) {
+ let params = cx.update(WorkspaceParams::test);
+
+ cx.update(|cx| {
+ editor::init(cx);
+ workspace::init(¶ms.client, cx);
+ init(cx);
+ });
+
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
+ let editor = cx.add_view(window_id, |cx| {
+ let mut editor = Editor::single_line(None, cx);
+ editor.set_text("abc", cx);
+ editor
+ });
+
+ workspace.update(cx, |workspace, cx| {
+ cx.focus(editor.clone());
+ workspace.add_item(Box::new(editor.clone()), cx)
+ });
+
+ workspace.update(cx, |workspace, cx| {
+ CommandPalette::toggle(workspace, &Toggle, cx)
+ });
+
+ let palette = workspace.read_with(cx, |workspace, _| {
+ workspace
+ .modal()
+ .unwrap()
+ .clone()
+ .downcast::<CommandPalette>()
+ .unwrap()
+ });
+
+ palette
+ .update(cx, |palette, cx| {
+ palette.update_matches("bcksp".to_string(), cx)
+ })
+ .await;
+
+ palette.update(cx, |palette, cx| {
+ assert_eq!(palette.matches[0].string, "editor: backspace");
+ palette.confirm(cx);
+ });
+
+ editor.read_with(cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "ab");
+ });
+ }
+}
@@ -10,6 +10,7 @@ doctest = false
[dependencies]
client = { path = "../client" }
gpui = { path = "../gpui" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] }
@@ -1,5 +1,3 @@
-use std::sync::Arc;
-
use client::{Contact, UserStore};
use gpui::{
elements::*,
@@ -8,7 +6,9 @@ use gpui::{
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
ViewContext,
};
-use workspace::{AppState, JoinProject, JoinProjectParams, Settings};
+use settings::Settings;
+use std::sync::Arc;
+use workspace::{AppState, JoinProject};
pub struct ContactsPanel {
contacts: ListState,
@@ -206,10 +206,10 @@ impl ContactsPanel {
})
.on_click(move |cx| {
if !is_host && !is_guest {
- cx.dispatch_global_action(JoinProject(JoinProjectParams {
+ cx.dispatch_global_action(JoinProject {
project_id,
app_state: app_state.clone(),
- }));
+ });
}
})
.flex(1., true)
@@ -14,6 +14,7 @@ editor = { path = "../editor" }
language = { path = "../language" }
gpui = { path = "../gpui" }
project = { path = "../project" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
@@ -8,7 +8,7 @@ use editor::{
highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
};
use gpui::{
- action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
+ actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
WeakViewHandle,
};
@@ -16,6 +16,8 @@ use language::{
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
};
use project::{DiagnosticSummary, Project, ProjectPath};
+use serde_json::json;
+use settings::Settings;
use std::{
any::{Any, TypeId},
cmp::Ordering,
@@ -25,14 +27,13 @@ use std::{
sync::Arc,
};
use util::TryFutureExt;
-use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
-action!(Deploy);
+actions!(diagnostics, [Deploy]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]);
cx.add_action(ProjectDiagnosticsEditor::deploy);
}
@@ -91,6 +92,31 @@ impl View for ProjectDiagnosticsEditor {
cx.focus(&self.editor);
}
}
+
+ fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+ let project = self.project.read(cx);
+ json!({
+ "project": json!({
+ "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
+ "summary": project.diagnostic_summary(cx),
+ }),
+ "summary": self.summary,
+ "paths_to_update": self.paths_to_update.iter().map(|path|
+ path.path.to_string_lossy()
+ ).collect::<Vec<_>>(),
+ "paths_states": self.path_states.iter().map(|state|
+ json!({
+ "path": state.path.path.to_string_lossy(),
+ "groups": state.diagnostic_groups.iter().map(|group|
+ json!({
+ "block_count": group.blocks.len(),
+ "excerpt_count": group.excerpts.len(),
+ })
+ ).collect::<Vec<_>>(),
+ })
+ ).collect::<Vec<_>>(),
+ })
+ }
}
impl ProjectDiagnosticsEditor {
@@ -1,9 +1,11 @@
use crate::render_summary;
use gpui::{
- elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
+ elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, RenderContext, View,
+ ViewContext,
};
use project::Project;
-use workspace::{Settings, StatusItemView};
+use settings::Settings;
+use workspace::StatusItemView;
pub struct DiagnosticSummary {
summary: project::DiagnosticSummary,
@@ -66,6 +68,10 @@ impl View for DiagnosticSummary {
.on_click(|cx| cx.dispatch_action(crate::Deploy))
.boxed()
}
+
+ fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
+ serde_json::json!({ "summary": self.summary })
+ }
}
impl StatusItemView for DiagnosticSummary {
@@ -28,6 +28,7 @@ language = { path = "../language" }
lsp = { path = "../lsp" }
project = { path = "../project" }
rpc = { path = "../rpc" }
+settings = { path = "../settings" }
snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" }
@@ -36,9 +37,10 @@ workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
futures = "0.3"
+indoc = "1.0.4"
itertools = "0.10"
lazy_static = "1.4"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11"
postage = { version = "0.4", features = ["futures-traits"] }
@@ -54,6 +56,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"
@@ -0,0 +1,272 @@
+pub enum ContextMenu {
+ Completions(CompletionsMenu),
+ CodeActions(CodeActionsMenu),
+}
+
+impl ContextMenu {
+ pub fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ if self.visible() {
+ match self {
+ ContextMenu::Completions(menu) => menu.select_prev(cx),
+ ContextMenu::CodeActions(menu) => menu.select_prev(cx),
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+ if self.visible() {
+ match self {
+ ContextMenu::Completions(menu) => menu.select_next(cx),
+ ContextMenu::CodeActions(menu) => menu.select_next(cx),
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn visible(&self) -> bool {
+ match self {
+ ContextMenu::Completions(menu) => menu.visible(),
+ ContextMenu::CodeActions(menu) => menu.visible(),
+ }
+ }
+
+ pub fn render(
+ &self,
+ cursor_position: DisplayPoint,
+ style: EditorStyle,
+ cx: &AppContext,
+ ) -> (DisplayPoint, ElementBox) {
+ match self {
+ ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
+ ContextMenu::CodeActions(menu) => menu.render(cursor_position, style),
+ }
+ }
+}
+
+struct CompletionsMenu {
+ id: CompletionId,
+ initial_position: Anchor,
+ buffer: ModelHandle<Buffer>,
+ completions: Arc<[Completion]>,
+ match_candidates: Vec<StringMatchCandidate>,
+ matches: Arc<[StringMatch]>,
+ selected_item: usize,
+ list: UniformListState,
+}
+
+impl CompletionsMenu {
+ fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+ if self.selected_item > 0 {
+ self.selected_item -= 1;
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ }
+ cx.notify();
+ }
+
+ fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+ if self.selected_item + 1 < self.matches.len() {
+ self.selected_item += 1;
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ }
+ cx.notify();
+ }
+
+ fn visible(&self) -> bool {
+ !self.matches.is_empty()
+ }
+
+ fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox {
+ enum CompletionTag {}
+
+ let completions = self.completions.clone();
+ let matches = self.matches.clone();
+ let selected_item = self.selected_item;
+ let container_style = style.autocomplete.container;
+ UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
+ let start_ix = range.start;
+ for (ix, mat) in matches[range].iter().enumerate() {
+ let completion = &completions[mat.candidate_id];
+ let item_ix = start_ix + ix;
+ items.push(
+ MouseEventHandler::new::<CompletionTag, _, _>(
+ mat.candidate_id,
+ cx,
+ |state, _| {
+ let item_style = if item_ix == selected_item {
+ style.autocomplete.selected_item
+ } else if state.hovered {
+ style.autocomplete.hovered_item
+ } else {
+ style.autocomplete.item
+ };
+
+ Text::new(completion.label.text.clone(), style.text.clone())
+ .with_soft_wrap(false)
+ .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+ &completion.label.text,
+ style.text.color.into(),
+ styled_runs_for_code_label(&completion.label, &style.syntax),
+ &mat.positions,
+ ))
+ .contained()
+ .with_style(item_style)
+ .boxed()
+ },
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_mouse_down(move |cx| {
+ cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
+ })
+ .boxed(),
+ );
+ }
+ })
+ .with_width_from_item(
+ self.matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, mat)| {
+ self.completions[mat.candidate_id]
+ .label
+ .text
+ .chars()
+ .count()
+ })
+ .map(|(ix, _)| ix),
+ )
+ .contained()
+ .with_style(container_style)
+ .boxed()
+ }
+
+ pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
+ let mut matches = if let Some(query) = query {
+ fuzzy::match_strings(
+ &self.match_candidates,
+ query,
+ false,
+ 100,
+ &Default::default(),
+ executor,
+ )
+ .await
+ } else {
+ self.match_candidates
+ .iter()
+ .enumerate()
+ .map(|(candidate_id, candidate)| StringMatch {
+ candidate_id,
+ score: Default::default(),
+ positions: Default::default(),
+ string: candidate.string.clone(),
+ })
+ .collect()
+ };
+ matches.sort_unstable_by_key(|mat| {
+ (
+ Reverse(OrderedFloat(mat.score)),
+ self.completions[mat.candidate_id].sort_key(),
+ )
+ });
+
+ for mat in &mut matches {
+ let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
+ for position in &mut mat.positions {
+ *position += filter_start;
+ }
+ }
+
+ self.matches = matches.into();
+ }
+}
+
+#[derive(Clone)]
+struct CodeActionsMenu {
+ actions: Arc<[CodeAction]>,
+ buffer: ModelHandle<Buffer>,
+ selected_item: usize,
+ list: UniformListState,
+ deployed_from_indicator: bool,
+}
+
+impl CodeActionsMenu {
+ fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+ if self.selected_item > 0 {
+ self.selected_item -= 1;
+ cx.notify()
+ }
+ }
+
+ fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+ if self.selected_item + 1 < self.actions.len() {
+ self.selected_item += 1;
+ cx.notify()
+ }
+ }
+
+ fn visible(&self) -> bool {
+ !self.actions.is_empty()
+ }
+
+ fn render(
+ &self,
+ mut cursor_position: DisplayPoint,
+ style: EditorStyle,
+ ) -> (DisplayPoint, ElementBox) {
+ enum ActionTag {}
+
+ let container_style = style.autocomplete.container;
+ let actions = self.actions.clone();
+ let selected_item = self.selected_item;
+ let element =
+ UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
+ let start_ix = range.start;
+ for (ix, action) in actions[range].iter().enumerate() {
+ let item_ix = start_ix + ix;
+ items.push(
+ MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
+ let item_style = if item_ix == selected_item {
+ style.autocomplete.selected_item
+ } else if state.hovered {
+ style.autocomplete.hovered_item
+ } else {
+ style.autocomplete.item
+ };
+
+ Text::new(action.lsp_action.title.clone(), style.text.clone())
+ .with_soft_wrap(false)
+ .contained()
+ .with_style(item_style)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_mouse_down(move |cx| {
+ cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
+ })
+ .boxed(),
+ );
+ }
+ })
+ .with_width_from_item(
+ self.actions
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+ .map(|(ix, _)| ix),
+ )
+ .contained()
+ .with_style(container_style)
+ .boxed();
+
+ if self.deployed_from_indicator {
+ *cursor_position.column_mut() = 0;
+ }
+
+ (cursor_position, element)
+ }
+}
@@ -12,6 +12,7 @@ use gpui::{
Entity, ModelContext, ModelHandle,
};
use language::{Point, Subscription as BufferSubscription};
+use settings::Settings;
use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
@@ -46,7 +47,6 @@ impl Entity for DisplayMap {
impl DisplayMap {
pub fn new(
buffer: ModelHandle<MultiBuffer>,
- tab_size: usize,
font_id: FontId,
font_size: f32,
wrap_width: Option<f32>,
@@ -55,6 +55,8 @@ impl DisplayMap {
cx: &mut ModelContext<Self>,
) -> Self {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+ let tab_size = Self::tab_size(&buffer, cx);
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
@@ -76,7 +78,9 @@ impl DisplayMap {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
- let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
+
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits, tab_size);
let (wraps_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
@@ -100,14 +104,15 @@ impl DisplayMap {
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
+ let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -122,14 +127,15 @@ impl DisplayMap {
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
+ let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -143,8 +149,9 @@ impl DisplayMap {
) -> Vec<BlockId> {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
+ let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -159,8 +166,9 @@ impl DisplayMap {
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
+ let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
- let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
+ let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -195,6 +203,16 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
+ fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> u32 {
+ let language_name = buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|buffer| buffer.read(cx).language())
+ .map(|language| language.name());
+
+ cx.global::<Settings>().tab_size(language_name.as_deref())
+ }
+
#[cfg(test)]
pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
self.wrap_map.read(cx).is_rewrapping()
@@ -536,6 +554,8 @@ pub mod tests {
log::info!("tab size: {}", tab_size);
log::info!("wrap width: {:?}", wrap_width);
+ cx.update(|cx| cx.set_global(Settings::test(cx)));
+
let buffer = cx.update(|cx| {
if rng.gen() {
let len = rng.gen_range(0..10);
@@ -549,7 +569,6 @@ pub mod tests {
let map = cx.add_model(|cx| {
DisplayMap::new(
buffer.clone(),
- tab_size,
font_id,
font_size,
wrap_width,
@@ -759,27 +778,18 @@ pub mod tests {
let font_cache = cx.font_cache();
- let tab_size = 4;
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 12.0;
let wrap_width = Some(64.);
+ cx.set_global(Settings::test(cx));
let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx);
let map = cx.add_model(|cx| {
- DisplayMap::new(
- buffer.clone(),
- tab_size,
- font_id,
- font_size,
- wrap_width,
- 1,
- 1,
- cx,
- )
+ DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@@ -847,18 +857,17 @@ pub mod tests {
#[gpui::test]
fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx);
- let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
- let map = cx.add_model(|cx| {
- DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
- });
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
@@ -923,12 +932,17 @@ pub mod tests {
.unwrap(),
);
language.set_theme(&theme);
+ cx.update(|cx| {
+ cx.set_global(Settings {
+ tab_size: 2,
+ ..Settings::test(cx)
+ })
+ });
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
- let tab_size = 2;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
@@ -936,8 +950,7 @@ pub mod tests {
.unwrap();
let font_size = 14.0;
- let map = cx
- .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+ let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
assert_eq!(
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
vec![
@@ -1011,22 +1024,22 @@ pub mod tests {
);
language.set_theme(&theme);
+ cx.update(|cx| cx.set_global(Settings::test(cx)));
+
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let font_cache = cx.font_cache();
- let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 16.0;
- let map = cx.add_model(|cx| {
- DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx)
- });
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
assert_eq!(
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
[
@@ -1058,6 +1071,7 @@ pub mod tests {
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+ cx.update(|cx| cx.set_global(Settings::test(cx)));
let theme = SyntaxTheme::new(vec![
("operator".to_string(), Color::red().into()),
("string".to_string(), Color::green().into()),
@@ -1090,14 +1104,12 @@ pub mod tests {
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_cache = cx.font_cache();
- let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 16.0;
- let map = cx
- .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+ let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
enum MyType {}
@@ -1136,6 +1148,7 @@ pub mod tests {
#[gpui::test]
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
@@ -1152,10 +1165,7 @@ pub mod tests {
*markers[0].column_mut() += 1;
}
- assert_eq!(
- unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
- markers[1]
- )
+ assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
}
};
}
@@ -1187,6 +1197,8 @@ pub mod tests {
#[gpui::test]
fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
+
fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
unmarked_snapshot.clip_at_line_ends = true;
@@ -1204,9 +1216,9 @@ pub mod tests {
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = MultiBuffer::build_simple(text, cx);
- let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
@@ -1214,9 +1226,8 @@ pub mod tests {
.unwrap();
let font_size = 14.0;
- let map = cx.add_model(|cx| {
- DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
- });
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
assert_eq!(
@@ -1264,17 +1275,16 @@ pub mod tests {
#[gpui::test]
fn test_max_point(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
- let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
- let map = cx.add_model(|cx| {
- DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
- });
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
DisplayPoint::new(1, 11)
@@ -969,6 +969,7 @@ mod tests {
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
use rand::prelude::*;
+ use settings::Settings;
use std::env;
use text::RandomCharIter;
@@ -988,6 +989,8 @@ mod tests {
#[gpui::test]
fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
+
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
@@ -1157,7 +1160,7 @@ mod tests {
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot, subscription.consume().into_inner());
- let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+ let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, 4);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
@@ -1167,6 +1170,8 @@ mod tests {
#[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
+
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
@@ -1209,6 +1214,8 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+ cx.set_global(Settings::test(cx));
+
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
@@ -1296,7 +1303,8 @@ mod tests {
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]);
- let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
@@ -1318,7 +1326,8 @@ mod tests {
let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]);
- let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
@@ -1338,7 +1347,7 @@ mod tests {
}
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
- let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+ let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
});
@@ -1210,6 +1210,7 @@ mod tests {
use super::*;
use crate::{MultiBuffer, ToPoint};
use rand::prelude::*;
+ use settings::Settings;
use std::{cmp::Reverse, env, mem, sync::Arc};
use sum_tree::TreeMap;
use text::RandomCharIter;
@@ -1218,6 +1219,7 @@ mod tests {
#[gpui::test]
fn test_basic_folds(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1291,6 +1293,7 @@ mod tests {
#[gpui::test]
fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1354,6 +1357,7 @@ mod tests {
#[gpui::test]
fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1404,6 +1408,7 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_folds(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+ cx.set_global(Settings::test(cx));
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
@@ -12,7 +12,7 @@ use text::Point;
pub struct TabMap(Mutex<TabSnapshot>);
impl TabMap {
- pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, TabSnapshot) {
+ pub fn new(input: FoldSnapshot, tab_size: u32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot: input,
tab_size,
@@ -24,12 +24,13 @@ impl TabMap {
&self,
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
+ tab_size: u32,
) -> (TabSnapshot, Vec<TabEdit>) {
let mut old_snapshot = self.0.lock();
let max_offset = old_snapshot.fold_snapshot.len();
let new_snapshot = TabSnapshot {
fold_snapshot,
- tab_size: old_snapshot.tab_size,
+ tab_size,
};
let mut tab_edits = Vec::with_capacity(fold_edits.len());
@@ -87,7 +88,7 @@ impl TabMap {
#[derive(Clone)]
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
- pub tab_size: usize,
+ pub tab_size: u32,
}
impl TabSnapshot {
@@ -95,6 +96,22 @@ impl TabSnapshot {
self.fold_snapshot.buffer_snapshot()
}
+ pub fn line_len(&self, row: u32) -> u32 {
+ let max_point = self.max_point();
+ if row < max_point.row() {
+ self.chunks(
+ TabPoint::new(row, 0)..TabPoint::new(row + 1, 0),
+ false,
+ None,
+ )
+ .map(|chunk| chunk.text.len() as u32)
+ .sum::<u32>()
+ - 1
+ } else {
+ max_point.column()
+ }
+ }
+
pub fn text_summary(&self) -> TextSummary {
self.text_summary_for_range(TabPoint::zero()..self.max_point())
}
@@ -234,7 +251,7 @@ impl TabSnapshot {
.to_buffer_point(&self.fold_snapshot)
}
- fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
+ fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: u32) -> usize {
let mut expanded_chars = 0;
let mut expanded_bytes = 0;
let mut collapsed_bytes = 0;
@@ -243,7 +260,7 @@ impl TabSnapshot {
break;
}
if c == '\t' {
- let tab_len = tab_size - expanded_chars % tab_size;
+ let tab_len = tab_size as usize - expanded_chars % tab_size as usize;
expanded_bytes += tab_len;
expanded_chars += tab_len;
} else {
@@ -259,7 +276,7 @@ impl TabSnapshot {
mut chars: impl Iterator<Item = char>,
column: usize,
bias: Bias,
- tab_size: usize,
+ tab_size: u32,
) -> (usize, usize, usize) {
let mut expanded_bytes = 0;
let mut expanded_chars = 0;
@@ -270,7 +287,7 @@ impl TabSnapshot {
}
if c == '\t' {
- let tab_len = tab_size - (expanded_chars % tab_size);
+ let tab_len = tab_size as usize - (expanded_chars % tab_size as usize);
expanded_chars += tab_len;
expanded_bytes += tab_len;
if expanded_bytes > column {
@@ -383,7 +400,7 @@ pub struct TabChunks<'a> {
column: usize,
output_position: Point,
max_output_position: Point,
- tab_size: usize,
+ tab_size: u32,
skip_leading_tab: bool,
}
@@ -415,16 +432,16 @@ impl<'a> Iterator for TabChunks<'a> {
});
} else {
self.chunk.text = &self.chunk.text[1..];
- let mut len = self.tab_size - self.column % self.tab_size;
+ let mut len = self.tab_size - self.column as u32 % self.tab_size;
let next_output_position = cmp::min(
- self.output_position + Point::new(0, len as u32),
+ self.output_position + Point::new(0, len),
self.max_output_position,
);
- len = (next_output_position.column - self.output_position.column) as usize;
- self.column += len;
+ len = next_output_position.column - self.output_position.column;
+ self.column += len as usize;
self.output_position = next_output_position;
return Some(Chunk {
- text: &SPACES[0..len],
+ text: &SPACES[0..len as usize],
..self.chunk
});
}
@@ -516,8 +533,11 @@ mod tests {
actual_summary.longest_row = expected_summary.longest_row;
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
}
+ assert_eq!(actual_summary, expected_summary);
+ }
- assert_eq!(actual_summary, expected_summary,);
+ for row in 0..=text.max_point().row {
+ assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
}
}
}
@@ -559,11 +559,6 @@ impl WrapSnapshot {
Patch::new(wrap_edits)
}
- pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
- self.chunks(wrap_row..self.max_point().row() + 1, false, None)
- .map(|h| h.text)
- }
-
pub fn chunks<'a>(
&'a self,
rows: Range<u32>,
@@ -599,16 +594,23 @@ impl WrapSnapshot {
}
pub fn line_len(&self, row: u32) -> u32 {
- let mut len = 0;
- for chunk in self.text_chunks(row) {
- if let Some(newline_ix) = chunk.find('\n') {
- len += newline_ix;
- break;
+ let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
+ if cursor
+ .item()
+ .map_or(false, |transform| transform.is_isomorphic())
+ {
+ let overshoot = row - cursor.start().0.row();
+ let tab_row = cursor.start().1.row() + overshoot;
+ let tab_line_len = self.tab_snapshot.line_len(tab_row);
+ if overshoot == 0 {
+ cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
} else {
- len += chunk.len();
+ tab_line_len
}
+ } else {
+ cursor.start().0.column()
}
- len as u32
}
pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
@@ -741,6 +743,7 @@ impl WrapSnapshot {
}
}
+ let text = language::Rope::from(self.text().as_str());
let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
@@ -754,6 +757,8 @@ impl WrapSnapshot {
expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
prev_tab_row = tab_point.row();
}
+
+ assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
for start_display_row in 0..expected_buffer_rows.len() {
@@ -957,6 +962,10 @@ impl WrapPoint {
&mut self.0.row
}
+ pub fn column(self) -> u32 {
+ self.0.column
+ }
+
pub fn column_mut(&mut self) -> &mut u32 {
&mut self.0.column
}
@@ -1014,12 +1023,14 @@ mod tests {
use gpui::test::observe;
use language::RandomCharIter;
use rand::prelude::*;
+ use settings::Settings;
use smol::stream::StreamExt;
use std::{cmp, env};
use text::Rope;
#[gpui::test(iterations = 100)]
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+ cx.update(|cx| cx.set_global(Settings::test(cx)));
cx.foreground().set_block_on_ticks(0..=50);
cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS")
@@ -1104,7 +1115,8 @@ mod tests {
}
20..=39 => {
for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
- let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1129,7 +1141,7 @@ mod tests {
"Unwrapped text (unexpanded tabs): {:?}",
folds_snapshot.text()
);
- let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+ let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
@@ -1269,6 +1281,11 @@ mod tests {
self.text_chunks(0).collect()
}
+ pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+ self.chunks(wrap_row..self.max_point().row() + 1, false, None)
+ .map(|h| h.text)
+ }
+
fn verify_chunks(&mut self, rng: &mut impl Rng) {
for _ in 0..5 {
let mut end_row = rng.gen_range(0..=self.max_point().row());
@@ -16,13 +16,13 @@ use display_map::*;
pub use element::*;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- action,
+ actions,
color::Color,
elements::*,
executor,
fonts::{self, HighlightStyle, TextStyle},
geometry::vector::{vec2f, Vector2F},
- keymap::Binding,
+ impl_actions, impl_internal_actions,
platform::CursorStyle,
text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
@@ -41,6 +41,7 @@ pub use multi_buffer::{
use ordered_float::OrderedFloat;
use project::{Project, ProjectTransaction};
use serde::{Deserialize, Serialize};
+use settings::Settings;
use smallvec::SmallVec;
use smol::Timer;
use snippet::Snippet;
@@ -55,93 +56,154 @@ use std::{
};
pub use sum_tree::Bias;
use text::rope::TextDimension;
-use theme::DiagnosticStyle;
+use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{settings, ItemNavHistory, Settings, Workspace};
+use workspace::{ItemNavHistory, Workspace};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
-action!(Cancel);
-action!(Backspace);
-action!(Delete);
-action!(Input, String);
-action!(Newline);
-action!(Tab, Direction);
-action!(Indent);
-action!(Outdent);
-action!(DeleteLine);
-action!(DeleteToPreviousWordStart);
-action!(DeleteToPreviousSubwordStart);
-action!(DeleteToNextWordEnd);
-action!(DeleteToNextSubwordEnd);
-action!(DeleteToBeginningOfLine);
-action!(DeleteToEndOfLine);
-action!(CutToEndOfLine);
-action!(DuplicateLine);
-action!(MoveLineUp);
-action!(MoveLineDown);
-action!(Cut);
-action!(Copy);
-action!(Paste);
-action!(Undo);
-action!(Redo);
-action!(MoveUp);
-action!(MoveDown);
-action!(MoveLeft);
-action!(MoveRight);
-action!(MoveToPreviousWordStart);
-action!(MoveToPreviousSubwordStart);
-action!(MoveToNextWordEnd);
-action!(MoveToNextSubwordEnd);
-action!(MoveToBeginningOfLine);
-action!(MoveToEndOfLine);
-action!(MoveToBeginning);
-action!(MoveToEnd);
-action!(SelectUp);
-action!(SelectDown);
-action!(SelectLeft);
-action!(SelectRight);
-action!(SelectToPreviousWordStart);
-action!(SelectToPreviousSubwordStart);
-action!(SelectToNextWordEnd);
-action!(SelectToNextSubwordEnd);
-action!(SelectToBeginningOfLine, bool);
-action!(SelectToEndOfLine, bool);
-action!(SelectToBeginning);
-action!(SelectToEnd);
-action!(SelectAll);
-action!(SelectLine);
-action!(SplitSelectionIntoLines);
-action!(AddSelectionAbove);
-action!(AddSelectionBelow);
-action!(SelectNext, bool);
-action!(ToggleComments);
-action!(SelectLargerSyntaxNode);
-action!(SelectSmallerSyntaxNode);
-action!(MoveToEnclosingBracket);
-action!(UndoSelection);
-action!(RedoSelection);
-action!(GoToDiagnostic, Direction);
-action!(GoToDefinition);
-action!(FindAllReferences);
-action!(Rename);
-action!(ConfirmRename);
-action!(PageUp);
-action!(PageDown);
-action!(Fold);
-action!(UnfoldLines);
-action!(FoldSelectedRanges);
-action!(Scroll, Vector2F);
-action!(Select, SelectPhase);
-action!(ShowCompletions);
-action!(ToggleCodeActions, bool);
-action!(ConfirmCompletion, Option<usize>);
-action!(ConfirmCodeAction, Option<usize>);
-action!(OpenExcerpts);
-action!(RestartLanguageServer);
+#[derive(Clone, Deserialize)]
+pub struct SelectNext {
+ #[serde(default)]
+ pub replace_newest: bool,
+}
+
+#[derive(Clone)]
+pub struct GoToDiagnostic(pub Direction);
+
+#[derive(Clone)]
+pub struct Scroll(pub Vector2F);
+
+#[derive(Clone)]
+pub struct Select(pub SelectPhase);
+
+#[derive(Clone, Deserialize)]
+pub struct Input(pub String);
+
+#[derive(Clone, Deserialize)]
+pub struct SelectToBeginningOfLine {
+ #[serde(default)]
+ stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct SelectToEndOfLine {
+ #[serde(default)]
+ stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct ToggleCodeActions {
+ #[serde(default)]
+ pub deployed_from_indicator: bool,
+}
+
+#[derive(Clone, Default, Deserialize)]
+pub struct ConfirmCompletion {
+ #[serde(default)]
+ pub item_ix: Option<usize>,
+}
+
+#[derive(Clone, Default, Deserialize)]
+pub struct ConfirmCodeAction {
+ #[serde(default)]
+ pub item_ix: Option<usize>,
+}
+
+actions!(
+ editor,
+ [
+ Cancel,
+ Backspace,
+ Delete,
+ Newline,
+ GoToNextDiagnostic,
+ GoToPrevDiagnostic,
+ Indent,
+ Outdent,
+ DeleteLine,
+ DeleteToPreviousWordStart,
+ DeleteToPreviousSubwordStart,
+ DeleteToNextWordEnd,
+ DeleteToNextSubwordEnd,
+ DeleteToBeginningOfLine,
+ DeleteToEndOfLine,
+ CutToEndOfLine,
+ DuplicateLine,
+ MoveLineUp,
+ MoveLineDown,
+ Cut,
+ Copy,
+ Paste,
+ Undo,
+ Redo,
+ MoveUp,
+ MoveDown,
+ MoveLeft,
+ MoveRight,
+ MoveToPreviousWordStart,
+ MoveToPreviousSubwordStart,
+ MoveToNextWordEnd,
+ MoveToNextSubwordEnd,
+ MoveToBeginningOfLine,
+ MoveToEndOfLine,
+ MoveToBeginning,
+ MoveToEnd,
+ SelectUp,
+ SelectDown,
+ SelectLeft,
+ SelectRight,
+ SelectToPreviousWordStart,
+ SelectToPreviousSubwordStart,
+ SelectToNextWordEnd,
+ SelectToNextSubwordEnd,
+ SelectToBeginning,
+ SelectToEnd,
+ SelectAll,
+ SelectLine,
+ SplitSelectionIntoLines,
+ AddSelectionAbove,
+ AddSelectionBelow,
+ Tab,
+ TabPrev,
+ ToggleComments,
+ SelectLargerSyntaxNode,
+ SelectSmallerSyntaxNode,
+ MoveToEnclosingBracket,
+ UndoSelection,
+ RedoSelection,
+ GoToDefinition,
+ FindAllReferences,
+ Rename,
+ ConfirmRename,
+ PageUp,
+ PageDown,
+ Fold,
+ UnfoldLines,
+ FoldSelectedRanges,
+ ShowCompletions,
+ OpenExcerpts,
+ RestartLanguageServer,
+ ]
+);
+
+impl_actions!(
+ editor,
+ [
+ Input,
+ SelectNext,
+ SelectToBeginningOfLine,
+ SelectToEndOfLine,
+ ToggleCodeActions,
+ ConfirmCompletion,
+ ConfirmCodeAction,
+ ]
+);
+
+impl_internal_actions!(editor, [Scroll, Select]);
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
@@ -153,159 +215,6 @@ pub enum Direction {
}
pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings(vec![
- Binding::new("escape", Cancel, Some("Editor")),
- Binding::new("backspace", Backspace, Some("Editor")),
- Binding::new("ctrl-h", Backspace, Some("Editor")),
- Binding::new("delete", Delete, Some("Editor")),
- Binding::new("ctrl-d", Delete, Some("Editor")),
- Binding::new("enter", Newline, Some("Editor && mode == full")),
- Binding::new(
- "alt-enter",
- Input("\n".into()),
- Some("Editor && mode == auto_height"),
- ),
- Binding::new(
- "enter",
- ConfirmCompletion(None),
- Some("Editor && showing_completions"),
- ),
- Binding::new(
- "enter",
- ConfirmCodeAction(None),
- Some("Editor && showing_code_actions"),
- ),
- Binding::new("enter", ConfirmRename, Some("Editor && renaming")),
- Binding::new("tab", Tab(Direction::Next), Some("Editor")),
- Binding::new("shift-tab", Tab(Direction::Prev), Some("Editor")),
- Binding::new(
- "tab",
- ConfirmCompletion(None),
- Some("Editor && showing_completions"),
- ),
- Binding::new("cmd-[", Outdent, Some("Editor")),
- Binding::new("cmd-]", Indent, Some("Editor")),
- Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")),
- Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")),
- Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")),
- Binding::new(
- "ctrl-alt-backspace",
- DeleteToPreviousSubwordStart,
- Some("Editor"),
- ),
- Binding::new("ctrl-alt-h", DeleteToPreviousSubwordStart, Some("Editor")),
- Binding::new("alt-delete", DeleteToNextWordEnd, Some("Editor")),
- Binding::new("alt-d", DeleteToNextWordEnd, Some("Editor")),
- Binding::new("ctrl-alt-delete", DeleteToNextSubwordEnd, Some("Editor")),
- Binding::new("ctrl-alt-d", DeleteToNextSubwordEnd, Some("Editor")),
- Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")),
- Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")),
- Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")),
- Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")),
- Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")),
- Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")),
- Binding::new("cmd-x", Cut, Some("Editor")),
- Binding::new("cmd-c", Copy, Some("Editor")),
- Binding::new("cmd-v", Paste, Some("Editor")),
- Binding::new("cmd-z", Undo, Some("Editor")),
- Binding::new("cmd-shift-Z", Redo, Some("Editor")),
- Binding::new("up", MoveUp, Some("Editor")),
- Binding::new("down", MoveDown, Some("Editor")),
- Binding::new("left", MoveLeft, Some("Editor")),
- Binding::new("right", MoveRight, Some("Editor")),
- Binding::new("ctrl-p", MoveUp, Some("Editor")),
- Binding::new("ctrl-n", MoveDown, Some("Editor")),
- Binding::new("ctrl-b", MoveLeft, Some("Editor")),
- Binding::new("ctrl-f", MoveRight, Some("Editor")),
- Binding::new("alt-left", MoveToPreviousWordStart, Some("Editor")),
- Binding::new("alt-b", MoveToPreviousWordStart, Some("Editor")),
- Binding::new("ctrl-alt-left", MoveToPreviousSubwordStart, Some("Editor")),
- Binding::new("ctrl-alt-b", MoveToPreviousSubwordStart, Some("Editor")),
- Binding::new("alt-right", MoveToNextWordEnd, Some("Editor")),
- Binding::new("alt-f", MoveToNextWordEnd, Some("Editor")),
- Binding::new("ctrl-alt-right", MoveToNextSubwordEnd, Some("Editor")),
- Binding::new("ctrl-alt-f", MoveToNextSubwordEnd, Some("Editor")),
- Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")),
- Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")),
- Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")),
- Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")),
- Binding::new("cmd-up", MoveToBeginning, Some("Editor")),
- Binding::new("cmd-down", MoveToEnd, Some("Editor")),
- Binding::new("shift-up", SelectUp, Some("Editor")),
- Binding::new("ctrl-shift-P", SelectUp, Some("Editor")),
- Binding::new("shift-down", SelectDown, Some("Editor")),
- Binding::new("ctrl-shift-N", SelectDown, Some("Editor")),
- Binding::new("shift-left", SelectLeft, Some("Editor")),
- Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")),
- Binding::new("shift-right", SelectRight, Some("Editor")),
- Binding::new("ctrl-shift-F", SelectRight, Some("Editor")),
- Binding::new("alt-shift-left", SelectToPreviousWordStart, Some("Editor")),
- Binding::new("alt-shift-B", SelectToPreviousWordStart, Some("Editor")),
- Binding::new(
- "ctrl-alt-shift-left",
- SelectToPreviousSubwordStart,
- Some("Editor"),
- ),
- Binding::new(
- "ctrl-alt-shift-B",
- SelectToPreviousSubwordStart,
- Some("Editor"),
- ),
- Binding::new("alt-shift-right", SelectToNextWordEnd, Some("Editor")),
- Binding::new("alt-shift-F", SelectToNextWordEnd, Some("Editor")),
- Binding::new(
- "cmd-shift-left",
- SelectToBeginningOfLine(true),
- Some("Editor"),
- ),
- Binding::new(
- "ctrl-alt-shift-right",
- SelectToNextSubwordEnd,
- Some("Editor"),
- ),
- Binding::new("ctrl-alt-shift-F", SelectToNextSubwordEnd, Some("Editor")),
- Binding::new(
- "ctrl-shift-A",
- SelectToBeginningOfLine(true),
- Some("Editor"),
- ),
- Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")),
- Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")),
- Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")),
- Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")),
- Binding::new("cmd-a", SelectAll, Some("Editor")),
- Binding::new("cmd-l", SelectLine, Some("Editor")),
- Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")),
- Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")),
- Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")),
- Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")),
- Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")),
- Binding::new("cmd-d", SelectNext(false), Some("Editor")),
- Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")),
- Binding::new("cmd-/", ToggleComments, Some("Editor")),
- Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")),
- Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")),
- Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
- Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
- Binding::new("cmd-u", UndoSelection, Some("Editor")),
- Binding::new("cmd-shift-U", RedoSelection, Some("Editor")),
- Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")),
- Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")),
- Binding::new("f2", Rename, Some("Editor")),
- Binding::new("f12", GoToDefinition, Some("Editor")),
- Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")),
- Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
- Binding::new("pageup", PageUp, Some("Editor")),
- Binding::new("pagedown", PageDown, Some("Editor")),
- Binding::new("alt-cmd-[", Fold, Some("Editor")),
- Binding::new("alt-cmd-]", UnfoldLines, Some("Editor")),
- Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")),
- Binding::new("ctrl-space", ShowCompletions, Some("Editor")),
- Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")),
- Binding::new("alt-enter", OpenExcerpts, Some("Editor")),
- Binding::new("cmd-f10", RestartLanguageServer, Some("Editor")),
- ]);
-
cx.add_action(Editor::open_new);
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
cx.add_action(Editor::select);
@@ -315,6 +224,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::backspace);
cx.add_action(Editor::delete);
cx.add_action(Editor::tab);
+ cx.add_action(Editor::tab_prev);
cx.add_action(Editor::indent);
cx.add_action(Editor::outdent);
cx.add_action(Editor::delete_line);
@@ -369,7 +279,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::move_to_enclosing_bracket);
cx.add_action(Editor::undo_selection);
cx.add_action(Editor::redo_selection);
- cx.add_action(Editor::go_to_diagnostic);
+ cx.add_action(Editor::go_to_next_diagnostic);
+ cx.add_action(Editor::go_to_prev_diagnostic);
cx.add_action(Editor::go_to_definition);
cx.add_action(Editor::page_up);
cx.add_action(Editor::page_down);
@@ -490,7 +401,7 @@ pub struct Editor {
vertical_scroll_margin: f32,
placeholder_text: Option<Arc<str>>,
highlighted_rows: Option<Range<u32>>,
- background_highlights: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
+ background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
nav_history: Option<ItemNavHistory>,
context_menu: Option<ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
@@ -767,7 +678,9 @@ impl CompletionsMenu {
)
.with_cursor_style(CursorStyle::PointingHand)
.on_mouse_down(move |cx| {
- cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
+ cx.dispatch_action(ConfirmCompletion {
+ item_ix: Some(item_ix),
+ });
})
.boxed(),
);
@@ -893,7 +806,9 @@ impl CodeActionsMenu {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_mouse_down(move |cx| {
- cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
+ cx.dispatch_action(ConfirmCodeAction {
+ item_ix: Some(item_ix),
+ });
})
.boxed(),
);
@@ -933,8 +848,13 @@ struct ClipboardSelection {
}
pub struct NavigationData {
- anchor: Anchor,
- offset: usize,
+ // Matching offsets for anchor and scroll_top_anchor allows us to recreate the anchor if the buffer
+ // has since been closed
+ cursor_anchor: Anchor,
+ cursor_offset: usize,
+ scroll_position: Vector2F,
+ scroll_top_anchor: Anchor,
+ scroll_top_offset: usize,
}
pub struct EditorCreated(pub ViewHandle<Editor>);
@@ -1008,7 +928,6 @@ impl Editor {
let style = build_style(&*settings, get_field_editor_theme, None, cx);
DisplayMap::new(
buffer.clone(),
- settings.tab_size,
style.text.font_id,
style.text.font_size,
None,
@@ -1094,7 +1013,7 @@ impl Editor {
if project.read(cx).is_remote() {
cx.propagate_action();
} else if let Some(buffer) = project
- .update(cx, |project, cx| project.create_buffer(cx))
+ .update(cx, |project, cx| project.create_buffer("", None, cx))
.log_err()
{
workspace.add_item(
@@ -1130,8 +1049,12 @@ impl Editor {
}
}
- pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
- self.buffer.read(cx).language(cx)
+ pub fn language_at<'a, T: ToOffset>(
+ &self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> Option<&'a Arc<Language>> {
+ self.buffer.read(cx).language_at(point, cx)
}
fn style(&self, cx: &AppContext) -> EditorStyle {
@@ -1782,33 +1705,37 @@ impl Editor {
return;
}
- if self.mode != EditorMode::Full {
- cx.propagate_action();
- return;
- }
+ if self.mode == EditorMode::Full {
+ if self.active_diagnostics.is_some() {
+ self.dismiss_diagnostics(cx);
+ return;
+ }
- if self.active_diagnostics.is_some() {
- self.dismiss_diagnostics(cx);
- } else if let Some(pending) = self.pending_selection.clone() {
- let mut selections = self.selections.clone();
- if selections.is_empty() {
- selections = Arc::from([pending.selection]);
+ if let Some(pending) = self.pending_selection.clone() {
+ let mut selections = self.selections.clone();
+ if selections.is_empty() {
+ selections = Arc::from([pending.selection]);
+ }
+ self.set_selections(selections, None, true, cx);
+ self.request_autoscroll(Autoscroll::Fit, cx);
+ return;
}
- self.set_selections(selections, None, true, cx);
- self.request_autoscroll(Autoscroll::Fit, cx);
- } else {
+
let mut oldest_selection = self.oldest_selection::<usize>(&cx);
- if self.selection_count() == 1 {
- if oldest_selection.is_empty() {
- cx.propagate_action();
- return;
- }
+ if self.selection_count() > 1 {
+ self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
+ return;
+ }
+ if !oldest_selection.is_empty() {
oldest_selection.start = oldest_selection.head().clone();
oldest_selection.end = oldest_selection.head().clone();
+ self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
+ return;
}
- self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx);
}
+
+ cx.propagate_action();
}
#[cfg(any(test, feature = "test-support"))]
@@ -2388,7 +2315,7 @@ impl Editor {
pub fn confirm_completion(
&mut self,
- ConfirmCompletion(completion_ix): &ConfirmCompletion,
+ action: &ConfirmCompletion,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
@@ -2401,7 +2328,7 @@ impl Editor {
let mat = completions_menu
.matches
- .get(completion_ix.unwrap_or(completions_menu.selected_item))?;
+ .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
let buffer_handle = completions_menu.buffer;
let completion = completions_menu.completions.get(mat.candidate_id)?;
@@ -2491,11 +2418,7 @@ impl Editor {
}))
}
- pub fn toggle_code_actions(
- &mut self,
- &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions,
- cx: &mut ViewContext<Self>,
- ) {
+ pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
if matches!(
self.context_menu.as_ref(),
Some(ContextMenu::CodeActions(_))
@@ -2505,6 +2428,7 @@ impl Editor {
return;
}
+ let deployed_from_indicator = action.deployed_from_indicator;
let mut task = self.code_actions_task.take();
cx.spawn_weak(|this, mut cx| async move {
while let Some(prev_task) = task {
@@ -2539,7 +2463,7 @@ impl Editor {
pub fn confirm_code_action(
workspace: &mut Workspace,
- ConfirmCodeAction(action_ix): &ConfirmCodeAction,
+ action: &ConfirmCodeAction,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
@@ -2550,7 +2474,7 @@ impl Editor {
} else {
return None;
};
- let action_ix = action_ix.unwrap_or(actions_menu.selected_item);
+ let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
let action = actions_menu.actions.get(action_ix)?.clone();
let title = action.lsp_action.title.clone();
let buffer = actions_menu.buffer;
@@ -2629,8 +2553,11 @@ impl Editor {
cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
workspace.add_item(Box::new(editor.clone()), cx);
editor.update(cx, |editor, cx| {
- let color = editor.style(cx).highlighted_line_background;
- editor.highlight_background::<Self>(ranges_to_highlight, color, cx);
+ editor.highlight_background::<Self>(
+ ranges_to_highlight,
+ |theme| theme.editor.highlighted_line_background,
+ cx,
+ );
});
});
@@ -2697,9 +2624,6 @@ impl Editor {
}
let buffer_id = cursor_position.buffer_id;
- let style = this.style(cx);
- let read_background = style.document_highlight_read_background;
- let write_background = style.document_highlight_write_background;
let buffer = this.buffer.read(cx);
if !buffer
.text_anchor_for_position(cursor_position, cx)
@@ -2746,12 +2670,12 @@ impl Editor {
this.highlight_background::<DocumentHighlightRead>(
read_ranges,
- read_background,
+ |theme| theme.editor.document_highlight_read_background,
cx,
);
this.highlight_background::<DocumentHighlightWrite>(
write_ranges,
- write_background,
+ |theme| theme.editor.document_highlight_write_background,
cx,
);
cx.notify();
@@ -2777,7 +2701,9 @@ impl Editor {
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.))
.on_mouse_down(|cx| {
- cx.dispatch_action(ToggleCodeActions(true));
+ cx.dispatch_action(ToggleCodeActions {
+ deployed_from_indicator: true,
+ });
})
.boxed(),
)
@@ -2871,8 +2797,8 @@ impl Editor {
self.move_to_snippet_tabstop(Bias::Right, cx)
}
- pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) {
- self.move_to_snippet_tabstop(Bias::Left, cx);
+ pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ self.move_to_snippet_tabstop(Bias::Left, cx)
}
pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> bool {
@@ -2945,8 +2871,9 @@ impl Editor {
.buffer_line_for_row(old_head.row)
{
let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
+ let language_name = buffer.language().map(|language| language.name());
+ let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
if old_head.column <= indent_column && old_head.column > 0 {
- let indent = buffer.indent_size();
new_head = cmp::min(
new_head,
Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
@@ -2976,60 +2903,58 @@ impl Editor {
});
}
- pub fn tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext<Self>) {
- match direction {
- Direction::Prev => {
- if !self.snippet_stack.is_empty() {
- self.move_to_prev_snippet_tabstop(cx);
- return;
- }
+ pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
+ if self.move_to_prev_snippet_tabstop(cx) {
+ return;
+ }
- self.outdent(&Outdent, cx);
- }
- Direction::Next => {
- if self.move_to_next_snippet_tabstop(cx) {
- return;
- }
+ self.outdent(&Outdent, cx);
+ }
- let tab_size = cx.global::<Settings>().tab_size;
- let mut selections = self.local_selections::<Point>(cx);
- if selections.iter().all(|s| s.is_empty()) {
- self.transact(cx, |this, cx| {
- this.buffer.update(cx, |buffer, cx| {
- for selection in &mut selections {
- let char_column = buffer
- .read(cx)
- .text_for_range(
- Point::new(selection.start.row, 0)..selection.start,
- )
- .flat_map(str::chars)
- .count();
- let chars_to_next_tab_stop = tab_size - (char_column % tab_size);
- buffer.edit(
- [selection.start..selection.start],
- " ".repeat(chars_to_next_tab_stop),
- cx,
- );
- selection.start.column += chars_to_next_tab_stop as u32;
- selection.end = selection.start;
- }
- });
- this.update_selections(selections, Some(Autoscroll::Fit), cx);
- });
- } else {
- self.indent(&Indent, cx);
- }
- }
+ pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+ if self.move_to_next_snippet_tabstop(cx) {
+ return;
+ }
+
+ let mut selections = self.local_selections::<Point>(cx);
+ if selections.iter().all(|s| s.is_empty()) {
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ for selection in &mut selections {
+ let language_name =
+ buffer.language_at(selection.start, cx).map(|l| l.name());
+ let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
+ let char_column = buffer
+ .read(cx)
+ .text_for_range(Point::new(selection.start.row, 0)..selection.start)
+ .flat_map(str::chars)
+ .count();
+ let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
+ buffer.edit(
+ [selection.start..selection.start],
+ " ".repeat(chars_to_next_tab_stop as usize),
+ cx,
+ );
+ selection.start.column += chars_to_next_tab_stop;
+ selection.end = selection.start;
+ }
+ });
+ this.update_selections(selections, Some(Autoscroll::Fit), cx);
+ });
+ } else {
+ self.indent(&Indent, cx);
}
}
pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
- let tab_size = cx.global::<Settings>().tab_size;
let mut selections = self.local_selections::<Point>(cx);
self.transact(cx, |this, cx| {
let mut last_indent = None;
this.buffer.update(cx, |buffer, cx| {
+ let snapshot = buffer.snapshot(cx);
for selection in &mut selections {
+ let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
+ let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
let mut start_row = selection.start.row;
let mut end_row = selection.end.row + 1;
@@ -3053,12 +2978,12 @@ impl Editor {
}
for row in start_row..end_row {
- let indent_column = buffer.read(cx).indent_column_for_line(row) as usize;
+ let indent_column = snapshot.indent_column_for_line(row);
let columns_to_next_tab_stop = tab_size - (indent_column % tab_size);
let row_start = Point::new(row, 0);
buffer.edit(
[row_start..row_start],
- " ".repeat(columns_to_next_tab_stop),
+ " ".repeat(columns_to_next_tab_stop as usize),
cx,
);
@@ -3080,14 +3005,16 @@ impl Editor {
}
pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) {
- let tab_size = cx.global::<Settings>().tab_size;
let selections = self.local_selections::<Point>(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut deletion_ranges = Vec::new();
let mut last_outdent = None;
{
- let buffer = self.buffer.read(cx).read(cx);
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
for selection in &selections {
+ let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
+ let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
let mut rows = selection.spanned_rows(false, &display_map);
// Avoid re-outdenting a row that has already been outdented by a
@@ -3099,11 +3026,11 @@ impl Editor {
}
for row in rows {
- let column = buffer.indent_column_for_line(row) as usize;
+ let column = snapshot.indent_column_for_line(row);
if column > 0 {
- let mut deletion_len = (column % tab_size) as u32;
+ let mut deletion_len = column % tab_size;
if deletion_len == 0 {
- deletion_len = tab_size as u32;
+ deletion_len = tab_size;
}
deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len));
last_outdent = Some(row);
@@ -3847,12 +3774,12 @@ impl Editor {
pub fn select_to_beginning_of_line(
&mut self,
- SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine,
+ action: &SelectToBeginningOfLine,
cx: &mut ViewContext<Self>,
) {
self.move_selection_heads(cx, |map, head, _| {
(
- movement::line_beginning(map, head, *stop_at_soft_boundaries),
+ movement::line_beginning(map, head, action.stop_at_soft_wraps),
SelectionGoal::None,
)
});
@@ -3864,7 +3791,12 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
self.transact(cx, |this, cx| {
- this.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx);
+ this.select_to_beginning_of_line(
+ &SelectToBeginningOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
this.backspace(&Backspace, cx);
});
}
@@ -3877,12 +3809,12 @@ impl Editor {
pub fn select_to_end_of_line(
&mut self,
- SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine,
+ action: &SelectToEndOfLine,
cx: &mut ViewContext<Self>,
) {
self.move_selection_heads(cx, |map, head, _| {
(
- movement::line_end(map, head, *stop_at_soft_boundaries),
+ movement::line_end(map, head, action.stop_at_soft_wraps),
SelectionGoal::None,
)
});
@@ -3890,14 +3822,24 @@ impl Editor {
pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
- this.select_to_end_of_line(&SelectToEndOfLine(false), cx);
+ this.select_to_end_of_line(
+ &SelectToEndOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
this.delete(&Delete, cx);
});
}
pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
- this.select_to_end_of_line(&SelectToEndOfLine(false), cx);
+ this.select_to_end_of_line(
+ &SelectToEndOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
this.cut(&Cut, cx);
});
}
@@ -3959,6 +3901,7 @@ impl Editor {
let buffer = self.buffer.read(cx).read(cx);
let offset = position.to_offset(&buffer);
let point = position.to_point(&buffer);
+ let scroll_top_offset = self.scroll_top_anchor.to_offset(&buffer);
drop(buffer);
if let Some(new_position) = new_position {
@@ -3969,8 +3912,11 @@ impl Editor {
}
nav_history.push(Some(NavigationData {
- anchor: position,
- offset,
+ cursor_anchor: position,
+ cursor_offset: offset,
+ scroll_position: self.scroll_position,
+ scroll_top_anchor: self.scroll_top_anchor.clone(),
+ scroll_top_offset,
}));
}
}
@@ -4144,7 +4090,6 @@ impl Editor {
pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) {
self.push_to_selection_history();
- let replace_newest = action.0;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let mut selections = self.local_selections::<usize>(cx);
@@ -4183,7 +4128,7 @@ impl Editor {
}
if let Some(next_selected_range) = next_selected_range {
- if replace_newest {
+ if action.replace_newest {
if let Some(newest_id) =
selections.iter().max_by_key(|s| s.id).map(|s| s.id)
{
@@ -4243,24 +4188,26 @@ impl Editor {
}
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
- // Get the line comment prefix. Split its trailing whitespace into a separate string,
- // as that portion won't be used for detecting if a line is a comment.
- let full_comment_prefix =
- if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) {
- prefix.to_string()
- } else {
- return;
- };
- let comment_prefix = full_comment_prefix.trim_end_matches(' ');
- let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
-
self.transact(cx, |this, cx| {
let mut selections = this.local_selections::<Point>(cx);
let mut all_selection_lines_are_comments = true;
let mut edit_ranges = Vec::new();
let mut last_toggled_row = None;
this.buffer.update(cx, |buffer, cx| {
+ // TODO: Handle selections that cross excerpts
for selection in &mut selections {
+ // Get the line comment prefix. Split its trailing whitespace into a separate string,
+ // as that portion won't be used for detecting if a line is a comment.
+ let full_comment_prefix = if let Some(prefix) = buffer
+ .language_at(selection.start, cx)
+ .and_then(|l| l.line_comment_prefix())
+ {
+ prefix.to_string()
+ } else {
+ return;
+ };
+ let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+ let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
edit_ranges.clear();
let snapshot = buffer.snapshot(cx);
@@ -22,6 +22,7 @@ use gpui::{
};
use json::json;
use language::{Bias, DiagnosticSeverity};
+use settings::Settings;
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
@@ -917,9 +918,11 @@ impl Element for EditorElement {
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
highlighted_rows = view.highlighted_rows();
+ let theme = cx.global::<Settings>().theme.as_ref();
highlighted_ranges = view.background_highlights_in_range(
start_anchor.clone()..end_anchor.clone(),
&display_map,
+ theme,
);
let mut remote_selections = HashMap::default();
@@ -1147,6 +1150,7 @@ impl Element for EditorElement {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
layout: &mut LayoutState,
paint: &mut PaintState,
cx: &mut EventContext,
@@ -1494,8 +1498,8 @@ mod tests {
display_map::{BlockDisposition, BlockProperties},
Editor, MultiBuffer,
};
+ use settings::Settings;
use util::test::sample_text;
- use workspace::Settings;
#[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
@@ -8,12 +8,11 @@ use gpui::{
use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
use project::{File, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view};
+use settings::Settings;
use std::{fmt::Write, path::PathBuf, time::Duration};
use text::{Point, Selection};
use util::TryFutureExt;
-use workspace::{
- FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
-};
+use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@@ -248,18 +247,27 @@ impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Some(data) = data.downcast_ref::<NavigationData>() {
let buffer = self.buffer.read(cx).read(cx);
- let offset = if buffer.can_resolve(&data.anchor) {
- data.anchor.to_offset(&buffer)
+ let offset = if buffer.can_resolve(&data.cursor_anchor) {
+ data.cursor_anchor.to_offset(&buffer)
} else {
- buffer.clip_offset(data.offset, Bias::Left)
+ buffer.clip_offset(data.cursor_offset, Bias::Left)
};
let newest_selection = self.newest_selection_with_snapshot::<usize>(&buffer);
+
+ let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
+ data.scroll_top_anchor.clone()
+ } else {
+ buffer.anchor_at(data.scroll_top_offset, Bias::Left)
+ };
+
drop(buffer);
if newest_selection.head() == offset {
false
} else {
let nav_history = self.nav_history.take();
+ self.scroll_position = data.scroll_position;
+ self.scroll_top_anchor = scroll_top_anchor;
self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
self.nav_history = nav_history;
true
@@ -268,9 +268,11 @@ mod tests {
use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
use language::Point;
+ use settings::Settings;
#[gpui::test]
fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -297,6 +299,7 @@ mod tests {
#[gpui::test]
fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -330,6 +333,7 @@ mod tests {
#[gpui::test]
fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(
marked_text: &str,
cx: &mut gpui::MutableAppContext,
@@ -361,6 +365,7 @@ mod tests {
#[gpui::test]
fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -384,6 +389,7 @@ mod tests {
#[gpui::test]
fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -416,6 +422,7 @@ mod tests {
#[gpui::test]
fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(
marked_text: &str,
cx: &mut gpui::MutableAppContext,
@@ -447,6 +454,7 @@ mod tests {
#[gpui::test]
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
@@ -467,6 +475,7 @@ mod tests {
#[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
@@ -487,7 +496,7 @@ mod tests {
multibuffer
});
let display_map =
- cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx));
+ cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
@@ -11,6 +11,7 @@ use language::{
Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _,
ToPointUtf16 as _, TransactionId,
};
+use settings::Settings;
use std::{
cell::{Ref, RefCell},
cmp, fmt, io,
@@ -297,8 +298,10 @@ impl MultiBuffer {
.into_iter()
.map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot));
return buffer.update(cx, |buffer, cx| {
+ let language_name = buffer.language().map(|language| language.name());
+ let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
if autoindent {
- buffer.edit_with_autoindent(ranges, new_text, cx);
+ buffer.edit_with_autoindent(ranges, new_text, indent_size, cx);
} else {
buffer.edit(ranges, new_text, cx);
}
@@ -392,10 +395,12 @@ impl MultiBuffer {
);
}
}
+ let language_name = buffer.language().map(|l| l.name());
+ let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
if autoindent {
- buffer.edit_with_autoindent(deletions, "", cx);
- buffer.edit_with_autoindent(insertions, new_text.clone(), cx);
+ buffer.edit_with_autoindent(deletions, "", indent_size, cx);
+ buffer.edit_with_autoindent(insertions, new_text.clone(), indent_size, cx);
} else {
buffer.edit(deletions, "", cx);
buffer.edit(insertions, new_text.clone(), cx);
@@ -783,7 +788,7 @@ impl MultiBuffer {
old: edit_start..edit_start,
new: edit_start..edit_end,
}]);
-
+ cx.emit(Event::Edited);
cx.notify();
ids
}
@@ -797,10 +802,12 @@ impl MultiBuffer {
snapshot.trailing_excerpt_update_count += 1;
snapshot.is_dirty = false;
snapshot.has_conflict = false;
+
self.subscriptions.publish_mut([Edit {
old: 0..prev_len,
new: 0..0,
}]);
+ cx.emit(Event::Edited);
cx.notify();
}
@@ -861,6 +868,29 @@ impl MultiBuffer {
})
}
+ // If point is at the end of the buffer, the last excerpt is returned
+ pub fn point_to_buffer_offset<'a, T: ToOffset>(
+ &'a self,
+ point: T,
+ cx: &AppContext,
+ ) -> Option<(ModelHandle<Buffer>, usize)> {
+ let snapshot = self.read(cx);
+ let offset = point.to_offset(&snapshot);
+ let mut cursor = snapshot.excerpts.cursor::<usize>();
+ cursor.seek(&offset, Bias::Right, &());
+ if cursor.item().is_none() {
+ cursor.prev(&());
+ }
+
+ cursor.item().map(|excerpt| {
+ let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
+ let buffer_point = excerpt_start + offset - *cursor.start();
+ let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
+
+ (buffer, buffer_point)
+ })
+ }
+
pub fn range_to_buffer_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
@@ -965,6 +995,7 @@ impl MultiBuffer {
}
self.subscriptions.publish_mut(edits);
+ cx.emit(Event::Edited);
cx.notify();
}
@@ -1057,12 +1088,13 @@ impl MultiBuffer {
.unwrap_or(false)
}
- pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
- self.buffers
- .borrow()
- .values()
- .next()
- .and_then(|state| state.buffer.read(cx).language())
+ pub fn language_at<'a, T: ToOffset>(
+ &self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> Option<&'a Arc<Language>> {
+ self.point_to_buffer_offset(point, cx)
+ .and_then(|(buffer, _)| buffer.read(cx).language())
}
pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> {
@@ -2899,7 +2931,7 @@ mod tests {
use gpui::MutableAppContext;
use language::{Buffer, Rope};
use rand::prelude::*;
- use std::env;
+ use std::{env, rc::Rc};
use text::{Point, RandomCharIter};
use util::test::sample_text;
@@ -2956,6 +2988,15 @@ mod tests {
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+ let events = Rc::new(RefCell::new(Vec::<Event>::new()));
+ multibuffer.update(cx, |_, cx| {
+ let events = events.clone();
+ cx.subscribe(&multibuffer, move |_, _, event, _| {
+ events.borrow_mut().push(event.clone())
+ })
+ .detach();
+ });
+
let subscription = multibuffer.update(cx, |multibuffer, cx| {
let subscription = multibuffer.subscribe();
multibuffer.push_excerpts(buffer_1.clone(), [Point::new(1, 2)..Point::new(2, 5)], cx);
@@ -2980,6 +3021,12 @@ mod tests {
subscription
});
+ // Adding excerpts emits an edited event.
+ assert_eq!(
+ events.borrow().as_slice(),
+ &[Event::Edited, Event::Edited, Event::Edited]
+ );
+
let snapshot = multibuffer.read(cx).snapshot(cx);
assert_eq!(
snapshot.text(),
@@ -3760,6 +3807,7 @@ mod tests {
#[gpui::test]
fn test_history(cx: &mut MutableAppContext) {
+ cx.set_global(Settings::test(cx));
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -1,8 +1,9 @@
-use util::test::marked_text;
+use gpui::ViewContext;
+use util::test::{marked_text, marked_text_ranges};
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
- DisplayPoint, MultiBuffer,
+ DisplayPoint, Editor, MultiBuffer,
};
#[cfg(test)]
@@ -20,7 +21,6 @@ pub fn marked_display_snapshot(
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
let (unmarked_text, markers) = marked_text(text);
- let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
@@ -30,7 +30,7 @@ pub fn marked_display_snapshot(
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
let display_map =
- cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
+ cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
let markers = markers
.into_iter()
@@ -39,3 +39,20 @@ pub fn marked_display_snapshot(
(snapshot, markers)
}
+
+pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
+ let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
+ assert_eq!(editor.text(cx), umarked_text);
+ editor.select_ranges(text_ranges, None, cx);
+}
+
+pub fn assert_text_with_selections(
+ editor: &mut Editor,
+ marked_text: &str,
+ cx: &mut ViewContext<Editor>,
+) {
+ let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
+
+ assert_eq!(editor.text(cx), unmarked_text);
+ assert_eq!(editor.selected_ranges(cx), text_ranges);
+}
@@ -11,7 +11,9 @@ doctest = false
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
+picker = { path = "../picker" }
project = { path = "../project" }
+settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
@@ -1,15 +1,12 @@
-use editor::Editor;
use fuzzy::PathMatch;
use gpui::{
- action,
- elements::*,
- keymap::{self, Binding},
- AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
+ actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
+ View, ViewContext, ViewHandle,
};
+use picker::{Picker, PickerDelegate};
use project::{Project, ProjectPath, WorktreeId};
+use settings::Settings;
use std::{
- cmp,
path::Path,
sync::{
atomic::{self, AtomicBool},
@@ -17,15 +14,11 @@ use std::{
},
};
use util::post_inc;
-use workspace::{
- menu::{Confirm, SelectNext, SelectPrev},
- Settings, Workspace,
-};
+use workspace::Workspace;
pub struct FileFinder {
- handle: WeakViewHandle<Self>,
project: ModelHandle<Project>,
- query_editor: ViewHandle<Editor>,
+ picker: ViewHandle<Picker<Self>>,
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
@@ -33,23 +26,13 @@ pub struct FileFinder {
matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>,
cancel_flag: Arc<AtomicBool>,
- list_state: UniformListState,
}
-action!(Toggle);
-action!(Select, ProjectPath);
+actions!(file_finder, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(FileFinder::toggle);
- cx.add_action(FileFinder::confirm);
- cx.add_action(FileFinder::select);
- cx.add_action(FileFinder::select_prev);
- cx.add_action(FileFinder::select_next);
-
- cx.add_bindings(vec![
- Binding::new("cmd-p", Toggle, None),
- Binding::new("escape", Toggle, Some("FileFinder")),
- ]);
+ Picker::<FileFinder>::init(cx);
}
pub enum Event {
@@ -66,140 +49,16 @@ impl View for FileFinder {
"FileFinder"
}
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = cx.global::<Settings>();
- Align::new(
- ConstrainedBox::new(
- Container::new(
- Flex::new(Axis::Vertical)
- .with_child(
- ChildView::new(&self.query_editor)
- .contained()
- .with_style(settings.theme.selector.input_editor.container)
- .boxed(),
- )
- .with_child(
- FlexItem::new(self.render_matches(cx))
- .flex(1., false)
- .boxed(),
- )
- .boxed(),
- )
- .with_style(settings.theme.selector.container)
- .boxed(),
- )
- .with_max_width(500.0)
- .with_max_height(420.0)
- .boxed(),
- )
- .top()
- .named("file finder")
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ ChildView::new(self.picker.clone()).boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.query_editor);
- }
-
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
- let mut cx = Self::default_keymap_context();
- cx.set.insert("menu".into());
- cx
+ cx.focus(&self.picker);
}
}
impl FileFinder {
- fn render_matches(&self, cx: &AppContext) -> ElementBox {
- if self.matches.is_empty() {
- let settings = cx.global::<Settings>();
- return Container::new(
- Label::new(
- "No matches".into(),
- settings.theme.selector.empty.label.clone(),
- )
- .boxed(),
- )
- .with_style(settings.theme.selector.empty.container)
- .named("empty matches");
- }
-
- let handle = self.handle.clone();
- let list =
- UniformList::new(
- self.list_state.clone(),
- self.matches.len(),
- move |mut range, items, cx| {
- let cx = cx.as_ref();
- let finder = handle.upgrade(cx).unwrap();
- let finder = finder.read(cx);
- let start = range.start;
- range.end = cmp::min(range.end, finder.matches.len());
- items.extend(finder.matches[range].iter().enumerate().map(
- move |(i, path_match)| finder.render_match(path_match, start + i, cx),
- ));
- },
- );
-
- Container::new(list.boxed())
- .with_margin_top(6.0)
- .named("matches")
- }
-
- fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
- let selected_index = self.selected_index();
- let settings = cx.global::<Settings>();
- let style = if index == selected_index {
- &settings.theme.selector.active_item
- } else {
- &settings.theme.selector.item
- };
- let (file_name, file_name_positions, full_path, full_path_positions) =
- self.labels_for_match(path_match);
- let container = Container::new(
- Flex::row()
- // .with_child(
- // Container::new(
- // LineBox::new(
- // Svg::new("icons/file-16.svg")
- // .with_color(style.label.text.color)
- // .boxed(),
- // style.label.text.clone(),
- // )
- // .boxed(),
- // )
- // .with_padding_right(6.0)
- // .boxed(),
- // )
- .with_child(
- Flex::column()
- .with_child(
- Label::new(file_name.to_string(), style.label.clone())
- .with_highlights(file_name_positions)
- .boxed(),
- )
- .with_child(
- Label::new(full_path, style.label.clone())
- .with_highlights(full_path_positions)
- .boxed(),
- )
- .flex(1., false)
- .boxed(),
- )
- .boxed(),
- )
- .with_style(style.container);
-
- let action = Select(ProjectPath {
- worktree_id: WorktreeId::from_usize(path_match.worktree_id),
- path: path_match.path.clone(),
- });
- EventHandler::new(container.boxed())
- .on_mouse_down(move |cx| {
- cx.dispatch_action(action.clone());
- true
- })
- .named("match")
- }
-
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path_string = path_match.path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
@@ -254,18 +113,11 @@ impl FileFinder {
}
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+ let handle = cx.weak_handle();
cx.observe(&project, Self::project_updated).detach();
-
- let query_editor = cx.add_view(|cx| {
- Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
- });
- cx.subscribe(&query_editor, Self::on_query_editor_event)
- .detach();
-
Self {
- handle: cx.weak_handle(),
project,
- query_editor,
+ picker: cx.add_view(|cx| Picker::new(handle, cx)),
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
@@ -273,40 +125,60 @@ impl FileFinder {
matches: Vec::new(),
selected: None,
cancel_flag: Arc::new(AtomicBool::new(false)),
- list_state: Default::default(),
}
}
fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
- let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
- if let Some(task) = self.spawn_search(query, cx) {
- task.detach();
- }
+ self.spawn_search(self.picker.read(cx).query(cx), cx)
+ .detach();
+ }
+
+ fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+ let search_id = util::post_inc(&mut self.search_count);
+ self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+ self.cancel_flag = Arc::new(AtomicBool::new(false));
+ let cancel_flag = self.cancel_flag.clone();
+ let project = self.project.clone();
+ cx.spawn(|this, mut cx| async move {
+ let matches = project
+ .read_with(&cx, |project, cx| {
+ project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
+ })
+ .await;
+ let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+ this.update(&mut cx, |this, cx| {
+ this.set_matches(search_id, did_cancel, query, matches, cx)
+ });
+ })
}
- fn on_query_editor_event(
+ fn set_matches(
&mut self,
- _: ViewHandle<Editor>,
- event: &editor::Event,
+ search_id: usize,
+ did_cancel: bool,
+ query: String,
+ matches: Vec<PathMatch>,
cx: &mut ViewContext<Self>,
) {
- match event {
- editor::Event::BufferEdited { .. } => {
- let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
- if query.is_empty() {
- self.latest_search_id = post_inc(&mut self.search_count);
- self.matches.clear();
- cx.notify();
- } else {
- if let Some(task) = self.spawn_search(query, cx) {
- task.detach();
- }
- }
+ if search_id >= self.latest_search_id {
+ self.latest_search_id = search_id;
+ if self.latest_search_did_cancel && query == self.latest_search_query {
+ util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
+ } else {
+ self.matches = matches;
}
- editor::Event::Blurred => cx.emit(Event::Dismissed),
- _ => {}
+ self.latest_search_query = query;
+ self.latest_search_did_cancel = did_cancel;
+ cx.notify();
+ self.picker.update(cx, |_, cx| cx.notify());
}
}
+}
+
+impl PickerDelegate for FileFinder {
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
fn selected_index(&self) -> usize {
if let Some(selected) = self.selected.as_ref() {
@@ -321,31 +193,24 @@ impl FileFinder {
0
}
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- let mut selected_index = self.selected_index();
- if selected_index > 0 {
- selected_index -= 1;
- let mat = &self.matches[selected_index];
- self.selected = Some((mat.worktree_id, mat.path.clone()));
- }
- self.list_state
- .scroll_to(ScrollTarget::Show(selected_index));
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+ let mat = &self.matches[ix];
+ self.selected = Some((mat.worktree_id, mat.path.clone()));
cx.notify();
}
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- let mut selected_index = self.selected_index();
- if selected_index + 1 < self.matches.len() {
- selected_index += 1;
- let mat = &self.matches[selected_index];
- self.selected = Some((mat.worktree_id, mat.path.clone()));
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+ if query.is_empty() {
+ self.latest_search_id = post_inc(&mut self.search_count);
+ self.matches.clear();
+ cx.notify();
+ Task::ready(())
+ } else {
+ self.spawn_search(query, cx)
}
- self.list_state
- .scroll_to(ScrollTarget::Show(selected_index));
- cx.notify();
}
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ fn confirm(&mut self, cx: &mut ViewContext<Self>) {
if let Some(m) = self.matches.get(self.selected_index()) {
cx.emit(Event::Selected(ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
@@ -354,57 +219,45 @@ impl FileFinder {
}
}
- fn select(&mut self, Select(project_path): &Select, cx: &mut ViewContext<Self>) {
- cx.emit(Event::Selected(project_path.clone()));
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Dismissed);
}
- #[must_use]
- fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Option<Task<()>> {
- let search_id = util::post_inc(&mut self.search_count);
- self.cancel_flag.store(true, atomic::Ordering::Relaxed);
- self.cancel_flag = Arc::new(AtomicBool::new(false));
- let cancel_flag = self.cancel_flag.clone();
- let project = self.project.clone();
- Some(cx.spawn(|this, mut cx| async move {
- let matches = project
- .read_with(&cx, |project, cx| {
- project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
- })
- .await;
- let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
- this.update(&mut cx, |this, cx| {
- this.update_matches((search_id, did_cancel, query, matches), cx)
- });
- }))
- }
-
- fn update_matches(
- &mut self,
- (search_id, did_cancel, query, matches): (usize, bool, String, Vec<PathMatch>),
- cx: &mut ViewContext<Self>,
- ) {
- if search_id >= self.latest_search_id {
- self.latest_search_id = search_id;
- if self.latest_search_did_cancel && query == self.latest_search_query {
- util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
- } else {
- self.matches = matches;
- }
- self.latest_search_query = query;
- self.latest_search_did_cancel = did_cancel;
- self.list_state
- .scroll_to(ScrollTarget::Show(self.selected_index()));
- cx.notify();
- }
+ fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+ let path_match = &self.matches[ix];
+ let settings = cx.global::<Settings>();
+ let style = if selected {
+ &settings.theme.selector.active_item
+ } else {
+ &settings.theme.selector.item
+ };
+ let (file_name, file_name_positions, full_path, full_path_positions) =
+ self.labels_for_match(path_match);
+ Flex::column()
+ .with_child(
+ Label::new(file_name.to_string(), style.label.clone())
+ .with_highlights(file_name_positions)
+ .boxed(),
+ )
+ .with_child(
+ Label::new(full_path, style.label.clone())
+ .with_highlights(full_path_positions)
+ .boxed(),
+ )
+ .flex(1., false)
+ .contained()
+ .with_style(style.container)
+ .named("match")
}
}
#[cfg(test)]
mod tests {
use super::*;
- use editor::Input;
+ use editor::{Editor, Input};
use serde_json::json;
use std::path::PathBuf;
+ use workspace::menu::{Confirm, SelectNext};
use workspace::{Workspace, WorkspaceParams};
#[ctor::ctor]
@@ -446,7 +299,7 @@ mod tests {
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
- cx.dispatch_action(window_id, vec![workspace.id()], Toggle);
+ cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| {
workspace
@@ -457,19 +310,16 @@ mod tests {
.downcast::<FileFinder>()
.unwrap()
});
- let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone());
-
- let chain = vec![finder.id(), query_buffer.id()];
- cx.dispatch_action(window_id, chain.clone(), Input("b".into()));
- cx.dispatch_action(window_id, chain.clone(), Input("n".into()));
- cx.dispatch_action(window_id, chain.clone(), Input("a".into()));
+ cx.dispatch_action(window_id, Input("b".into()));
+ cx.dispatch_action(window_id, Input("n".into()));
+ cx.dispatch_action(window_id, Input("a".into()));
finder
.condition(&cx, |finder, _| finder.matches.len() == 2)
.await;
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
- cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], SelectNext);
- cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], Confirm);
+ cx.dispatch_action(window_id, SelectNext);
+ cx.dispatch_action(window_id, Confirm);
active_pane
.condition(&cx, |pane, _| pane.active_item().is_some())
.await;
@@ -521,7 +371,6 @@ mod tests {
let query = "hi".to_string();
finder
.update(cx, |f, cx| f.spawn_search(query.clone(), cx))
- .unwrap()
.await;
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5));
@@ -530,26 +379,22 @@ mod tests {
// Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found.
- finder.spawn_search(query.clone(), cx).unwrap().detach();
- finder.update_matches(
- (
- finder.latest_search_id,
- true, // did-cancel
- query.clone(),
- vec![matches[1].clone(), matches[3].clone()],
- ),
+ drop(finder.spawn_search(query.clone(), cx));
+ finder.set_matches(
+ finder.latest_search_id,
+ true, // did-cancel
+ query.clone(),
+ vec![matches[1].clone(), matches[3].clone()],
cx,
);
// Simulate another cancellation.
- finder.spawn_search(query.clone(), cx).unwrap().detach();
- finder.update_matches(
- (
- finder.latest_search_id,
- true, // did-cancel
- query.clone(),
- vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
- ),
+ drop(finder.spawn_search(query.clone(), cx));
+ finder.set_matches(
+ finder.latest_search_id,
+ true, // did-cancel
+ query.clone(),
+ vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
cx,
);
@@ -583,7 +428,6 @@ mod tests {
// is included in the matching, because the worktree is a single file.
finder
.update(cx, |f, cx| f.spawn_search("thf".into(), cx))
- .unwrap()
.await;
cx.read(|cx| {
let finder = finder.read(cx);
@@ -601,7 +445,6 @@ mod tests {
// not match anything.
finder
.update(cx, |f, cx| f.spawn_search("thf/".into(), cx))
- .unwrap()
.await;
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
}
@@ -640,16 +483,15 @@ mod tests {
// Run a search that matches two files with the same relative path.
finder
.update(cx, |f, cx| f.spawn_search("a.t".into(), cx))
- .unwrap()
.await;
// Can switch between different matches with the same relative path.
finder.update(cx, |f, cx| {
assert_eq!(f.matches.len(), 2);
assert_eq!(f.selected_index(), 0);
- f.select_next(&SelectNext, cx);
+ f.set_selected_index(1, cx);
assert_eq!(f.selected_index(), 1);
- f.select_prev(&SelectPrev, cx);
+ f.set_selected_index(0, cx);
assert_eq!(f.selected_index(), 0);
});
}
@@ -11,5 +11,6 @@ doctest = false
text = { path = "../text" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
+settings = { path = "../settings" }
workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] }
@@ -1,20 +1,15 @@
use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
use gpui::{
- action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
- MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+ actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext,
+ RenderContext, View, ViewContext, ViewHandle,
};
+use settings::Settings;
use text::{Bias, Point};
-use workspace::{Settings, Workspace};
+use workspace::Workspace;
-action!(Toggle);
-action!(Confirm);
+actions!(go_to_line, [Toggle, Confirm]);
pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings([
- Binding::new("ctrl-g", Toggle, Some("Editor")),
- Binding::new("escape", Toggle, Some("GoToLine")),
- Binding::new("enter", Confirm, Some("GoToLine")),
- ]);
cx.add_action(GoToLine::toggle);
cx.add_action(GoToLine::confirm);
}
@@ -25,7 +25,7 @@ etagere = "0.2"
futures = "0.3"
image = "0.23"
lazy_static = "1.4.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
num_cpus = "1.13"
ordered-float = "2.1.1"
parking = "2.0.0"
@@ -67,6 +67,6 @@ core-graphics = "0.22.2"
core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }
foreign-types = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
metal = "0.21.0"
objc = "0.2"
@@ -104,6 +104,7 @@ impl gpui::Element for TextElement {
&mut self,
_: &gpui::Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
@@ -1,19 +1,23 @@
+pub mod action;
+
use crate::{
elements::ElementBox,
executor::{self, Task},
- keymap::{self, Keystroke},
+ keymap::{self, Binding, Keystroke},
platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
};
-use anyhow::{anyhow, Result};
+pub use action::*;
+use anyhow::{anyhow, Context, Result};
use collections::btree_map;
use keymap::MatchResult;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use platform::Event;
use postage::oneshot;
+use smallvec::SmallVec;
use smol::prelude::*;
use std::{
any::{type_name, Any, TypeId},
@@ -59,6 +63,9 @@ pub trait View: Entity + Sized {
cx.set.insert(Self::ui_name().into());
cx
}
+ fn debug_json(&self, _: &AppContext) -> serde_json::Value {
+ serde_json::Value::Null
+ }
}
pub trait ReadModel {
@@ -142,89 +149,6 @@ pub trait ElementStateContext: DerefMut<Target = MutableAppContext> {
}
}
-pub trait Action: 'static + AnyAction {
- type Argument: 'static + Clone;
-}
-
-pub trait AnyAction {
- fn id(&self) -> TypeId;
- fn name(&self) -> &'static str;
- fn as_any(&self) -> &dyn Any;
- fn boxed_clone(&self) -> Box<dyn AnyAction>;
- fn boxed_clone_as_any(&self) -> Box<dyn Any>;
-}
-
-#[macro_export]
-macro_rules! action {
- ($name:ident, $arg:ty) => {
- #[derive(Clone)]
- pub struct $name(pub $arg);
-
- impl $crate::Action for $name {
- type Argument = $arg;
- }
-
- impl $crate::AnyAction for $name {
- fn id(&self) -> std::any::TypeId {
- std::any::TypeId::of::<$name>()
- }
-
- fn name(&self) -> &'static str {
- stringify!($name)
- }
-
- fn as_any(&self) -> &dyn std::any::Any {
- self
- }
-
- fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
- Box::new(self.clone())
- }
-
- fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
- Box::new(self.clone())
- }
- }
-
- impl From<$arg> for $name {
- fn from(arg: $arg) -> Self {
- Self(arg)
- }
- }
- };
-
- ($name:ident) => {
- #[derive(Clone, Debug, Eq, PartialEq)]
- pub struct $name;
-
- impl $crate::Action for $name {
- type Argument = ();
- }
-
- impl $crate::AnyAction for $name {
- fn id(&self) -> std::any::TypeId {
- std::any::TypeId::of::<$name>()
- }
-
- fn name(&self) -> &'static str {
- stringify!($name)
- }
-
- fn as_any(&self) -> &dyn std::any::Any {
- self
- }
-
- fn boxed_clone(&self) -> Box<dyn $crate::AnyAction> {
- Box::new(self.clone())
- }
-
- fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
- Box::new(self.clone())
- }
- }
- };
-}
-
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
@@ -234,7 +158,7 @@ pub enum MenuItem<'a> {
Action {
name: &'a str,
keystroke: Option<&'a str>,
- action: Box<dyn AnyAction>,
+ action: Box<dyn Action>,
},
Separator,
}
@@ -324,7 +248,7 @@ impl App {
self
}
- pub fn on_quit<F>(self, mut callback: F) -> Self
+ pub fn on_quit<F>(&mut self, mut callback: F) -> &mut Self
where
F: 'static + FnMut(&mut MutableAppContext),
{
@@ -336,7 +260,7 @@ impl App {
self
}
- pub fn on_event<F>(self, mut callback: F) -> Self
+ pub fn on_event<F>(&mut self, mut callback: F) -> &mut Self
where
F: 'static + FnMut(Event, &mut MutableAppContext) -> bool,
{
@@ -350,15 +274,15 @@ impl App {
self
}
- pub fn on_open_files<F>(self, mut callback: F) -> Self
+ pub fn on_open_urls<F>(&mut self, mut callback: F) -> &mut Self
where
- F: 'static + FnMut(Vec<PathBuf>, &mut MutableAppContext),
+ F: 'static + FnMut(Vec<String>, &mut MutableAppContext),
{
let cx = self.0.clone();
self.0
.borrow_mut()
.foreground_platform
- .on_open_files(Box::new(move |paths| {
+ .on_open_urls(Box::new(move |paths| {
callback(paths, &mut *cx.borrow_mut())
}));
self
@@ -426,15 +350,17 @@ impl TestAppContext {
cx
}
- pub fn dispatch_action<A: Action>(
- &self,
- window_id: usize,
- responder_chain: Vec<usize>,
- action: A,
- ) {
- self.cx
- .borrow_mut()
- .dispatch_action_any(window_id, &responder_chain, &action);
+ pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
+ let mut cx = self.cx.borrow_mut();
+ let dispatch_path = cx
+ .presenters_and_platform_windows
+ .get(&window_id)
+ .unwrap()
+ .0
+ .borrow()
+ .dispatch_path(cx.as_ref());
+
+ cx.dispatch_action_any(window_id, &dispatch_path, &action);
}
pub fn dispatch_global_action<A: Action>(&self, action: A) {
@@ -455,9 +381,9 @@ impl TestAppContext {
.unwrap()
.0
.clone();
- let responder_chain = presenter.borrow().dispatch_path(cx.as_ref());
+ let dispatch_path = presenter.borrow().dispatch_path(cx.as_ref());
- if !cx.dispatch_keystroke(window_id, responder_chain, &keystroke) {
+ if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
presenter.borrow_mut().dispatch_event(
Event::KeyDown {
keystroke,
@@ -595,6 +521,15 @@ impl TestAppContext {
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector()
}
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn assert_dropped(&self, handle: impl WeakHandle) {
+ self.cx
+ .borrow()
+ .leak_detector()
+ .lock()
+ .assert_dropped(handle.id())
+ }
}
impl AsyncAppContext {
@@ -776,20 +711,23 @@ impl ReadViewWith for TestAppContext {
}
type ActionCallback =
- dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize);
-type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
+ dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
+type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
+type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
-type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
+type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
+type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
pub struct MutableAppContext {
weak_self: Option<rc::Weak<RefCell<Self>>>,
foreground_platform: Rc<dyn platform::ForegroundPlatform>,
assets: Arc<AssetCache>,
cx: AppContext,
+ action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
@@ -802,6 +740,8 @@ pub struct MutableAppContext {
global_subscriptions:
Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>,
+ focus_observations:
+ Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<FocusObservationCallback>>>>>,
global_observations:
Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
@@ -809,6 +749,7 @@ pub struct MutableAppContext {
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
foreground: Rc<executor::Foreground>,
pending_effects: VecDeque<Effect>,
+ pending_focus_index: Option<usize>,
pending_notifications: HashSet<usize>,
pending_global_notifications: HashSet<TypeId>,
pending_flushes: usize,
@@ -842,6 +783,7 @@ impl MutableAppContext {
font_cache,
platform,
},
+ action_deserializers: HashMap::new(),
capture_actions: HashMap::new(),
actions: HashMap::new(),
global_actions: HashMap::new(),
@@ -853,11 +795,13 @@ impl MutableAppContext {
subscriptions: Default::default(),
global_subscriptions: Default::default(),
observations: Default::default(),
+ focus_observations: Default::default(),
release_observations: Default::default(),
global_observations: Default::default(),
presenters_and_platform_windows: HashMap::new(),
foreground,
pending_effects: VecDeque::new(),
+ pending_focus_index: None,
pending_notifications: HashSet::new(),
pending_global_notifications: HashSet::new(),
pending_flushes: 0,
@@ -926,6 +870,20 @@ impl MutableAppContext {
.and_then(|(presenter, _)| presenter.borrow().debug_elements(self))
}
+ pub fn deserialize_action(
+ &self,
+ name: &str,
+ argument: Option<&str>,
+ ) -> Result<Box<dyn Action>> {
+ let callback = self
+ .action_deserializers
+ .get(name)
+ .ok_or_else(|| anyhow!("unknown action {}", name))?
+ .1;
+ callback(argument.unwrap_or("{}"))
+ .with_context(|| format!("invalid data for action {}", name))
+ }
+
pub fn add_action<A, V, F>(&mut self, handler: F)
where
A: Action,
@@ -952,7 +910,7 @@ impl MutableAppContext {
{
let handler = Box::new(
move |view: &mut dyn AnyView,
- action: &dyn AnyAction,
+ action: &dyn Action,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize| {
@@ -968,6 +926,10 @@ impl MutableAppContext {
},
);
+ self.action_deserializers
+ .entry(A::qualified_name())
+ .or_insert((TypeId::of::<A>(), A::from_json_str));
+
let actions = if capture {
&mut self.capture_actions
} else {
@@ -998,17 +960,24 @@ impl MutableAppContext {
A: Action,
F: 'static + FnMut(&A, &mut MutableAppContext),
{
- let handler = Box::new(move |action: &dyn AnyAction, cx: &mut MutableAppContext| {
+ let handler = Box::new(move |action: &dyn Action, cx: &mut MutableAppContext| {
let action = action.as_any().downcast_ref().unwrap();
handler(action, cx);
});
+ self.action_deserializers
+ .entry(A::qualified_name())
+ .or_insert((TypeId::of::<A>(), A::from_json_str));
+
if self
.global_actions
.insert(TypeId::of::<A>(), handler)
.is_some()
{
- panic!("registered multiple global handlers for the same action type");
+ panic!(
+ "registered multiple global handlers for {}",
+ type_name::<A>()
+ );
}
}
@@ -1222,6 +1191,32 @@ impl MutableAppContext {
}
}
+ fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
+ where
+ F: 'static + FnMut(ViewHandle<V>, &mut MutableAppContext) -> bool,
+ V: View,
+ {
+ let subscription_id = post_inc(&mut self.next_subscription_id);
+ let observed = handle.downgrade();
+ let view_id = handle.id();
+ self.pending_effects.push_back(Effect::FocusObservation {
+ view_id,
+ subscription_id,
+ callback: Box::new(move |cx| {
+ if let Some(observed) = observed.upgrade(cx) {
+ callback(observed, cx)
+ } else {
+ false
+ }
+ }),
+ });
+ Subscription::FocusObservation {
+ id: subscription_id,
+ view_id,
+ observations: Some(Arc::downgrade(&self.focus_observations)),
+ }
+ }
+
pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
where
G: Any,
@@ -1250,12 +1245,12 @@ impl MutableAppContext {
}
}
- pub fn observe_release<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+ pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
where
E: Entity,
E::Event: 'static,
H: Handle<E>,
- F: 'static + FnMut(&E, &mut Self),
+ F: 'static + FnOnce(&E, &mut Self),
{
let id = post_inc(&mut self.next_subscription_id);
self.release_observations
@@ -1311,20 +1306,71 @@ impl MutableAppContext {
}
}
+ pub fn available_actions(
+ &self,
+ window_id: usize,
+ view_id: usize,
+ ) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
+ let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect();
+
+ let presenter = self
+ .presenters_and_platform_windows
+ .get(&window_id)
+ .unwrap()
+ .0
+ .clone();
+ let dispatch_path = presenter.borrow().dispatch_path_from(view_id);
+ for view_id in dispatch_path {
+ if let Some(view) = self.views.get(&(window_id, view_id)) {
+ let view_type = view.as_any().type_id();
+ if let Some(actions) = self.actions.get(&view_type) {
+ action_types.extend(actions.keys().copied());
+ }
+ }
+ }
+
+ self.action_deserializers
+ .iter()
+ .filter_map(move |(name, (type_id, deserialize))| {
+ if action_types.contains(type_id) {
+ Some((
+ *name,
+ deserialize("{}").ok()?,
+ self.keystroke_matcher
+ .bindings_for_action_type(*type_id)
+ .collect(),
+ ))
+ } else {
+ None
+ }
+ })
+ }
+
+ pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
+ let presenter = self
+ .presenters_and_platform_windows
+ .get(&window_id)
+ .unwrap()
+ .0
+ .clone();
+ let dispatch_path = presenter.borrow().dispatch_path_from(view_id);
+ self.dispatch_action_any(window_id, &dispatch_path, action);
+ }
+
pub fn dispatch_action<A: Action>(
&mut self,
window_id: usize,
- responder_chain: Vec<usize>,
+ dispatch_path: Vec<usize>,
action: &A,
) {
- self.dispatch_action_any(window_id, &responder_chain, action);
+ self.dispatch_action_any(window_id, &dispatch_path, action);
}
pub(crate) fn dispatch_action_any(
&mut self,
window_id: usize,
path: &[usize],
- action: &dyn AnyAction,
+ action: &dyn Action,
) -> bool {
self.update(|this| {
this.halt_action_dispatch = false;
@@ -1384,7 +1430,7 @@ impl MutableAppContext {
self.dispatch_global_action_any(&action);
}
- fn dispatch_global_action_any(&mut self, action: &dyn AnyAction) -> bool {
+ fn dispatch_global_action_any(&mut self, action: &dyn Action) -> bool {
self.update(|this| {
if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
handler(action, this);
@@ -1400,14 +1446,18 @@ impl MutableAppContext {
self.keystroke_matcher.add_bindings(bindings);
}
+ pub fn clear_bindings(&mut self) {
+ self.keystroke_matcher.clear_bindings();
+ }
+
pub fn dispatch_keystroke(
&mut self,
window_id: usize,
- responder_chain: Vec<usize>,
+ dispatch_path: Vec<usize>,
keystroke: &Keystroke,
) -> bool {
let mut context_chain = Vec::new();
- for view_id in &responder_chain {
+ for view_id in &dispatch_path {
let view = self
.cx
.views
@@ -1420,13 +1470,12 @@ impl MutableAppContext {
for (i, cx) in context_chain.iter().enumerate().rev() {
match self
.keystroke_matcher
- .push_keystroke(keystroke.clone(), responder_chain[i], cx)
+ .push_keystroke(keystroke.clone(), dispatch_path[i], cx)
{
MatchResult::None => {}
MatchResult::Pending => pending = true,
MatchResult::Action(action) => {
- if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
- {
+ if self.dispatch_action_any(window_id, &dispatch_path[0..=i], action.as_ref()) {
self.keystroke_matcher.clear_pending();
return true;
}
@@ -1706,7 +1755,7 @@ impl MutableAppContext {
});
if let Some(view_id) = change_focus_to {
- self.focus(window_id, Some(view_id));
+ self.handle_focus_effect(window_id, Some(view_id));
}
self.pending_effects
@@ -1729,6 +1778,9 @@ impl MutableAppContext {
let mut refreshing = false;
loop {
if let Some(effect) = self.pending_effects.pop_front() {
+ if let Some(pending_focus_index) = self.pending_focus_index.as_mut() {
+ *pending_focus_index = pending_focus_index.saturating_sub(1);
+ }
match effect {
Effect::Subscription {
entity_id,
@@ -1752,13 +1804,13 @@ impl MutableAppContext {
callback,
} => self.handle_observation_effect(entity_id, subscription_id, callback),
Effect::ModelNotification { model_id } => {
- self.notify_model_observers(model_id)
+ self.handle_model_notification_effect(model_id)
}
Effect::ViewNotification { window_id, view_id } => {
- self.notify_view_observers(window_id, view_id)
+ self.handle_view_notification_effect(window_id, view_id)
}
Effect::GlobalNotification { type_id } => {
- self.notify_global_observers(type_id)
+ self.handle_global_notification_effect(type_id)
}
Effect::Deferred {
callback,
@@ -1771,13 +1823,20 @@ impl MutableAppContext {
}
}
Effect::ModelRelease { model_id, model } => {
- self.notify_release_observers(model_id, model.as_any())
+ self.handle_entity_release_effect(model_id, model.as_any())
}
Effect::ViewRelease { view_id, view } => {
- self.notify_release_observers(view_id, view.as_any())
+ self.handle_entity_release_effect(view_id, view.as_any())
}
Effect::Focus { window_id, view_id } => {
- self.focus(window_id, view_id);
+ self.handle_focus_effect(window_id, view_id);
+ }
+ Effect::FocusObservation {
+ view_id,
+ subscription_id,
+ callback,
+ } => {
+ self.handle_focus_observation_effect(view_id, subscription_id, callback)
}
Effect::ResizeWindow { window_id } => {
if let Some(window) = self.cx.windows.get_mut(&window_id) {
@@ -2009,7 +2068,31 @@ impl MutableAppContext {
}
}
- fn notify_model_observers(&mut self, observed_id: usize) {
+ fn handle_focus_observation_effect(
+ &mut self,
+ view_id: usize,
+ subscription_id: usize,
+ callback: FocusObservationCallback,
+ ) {
+ match self
+ .focus_observations
+ .lock()
+ .entry(view_id)
+ .or_default()
+ .entry(subscription_id)
+ {
+ btree_map::Entry::Vacant(entry) => {
+ entry.insert(Some(callback));
+ }
+ // Observation was dropped before effect was processed
+ btree_map::Entry::Occupied(entry) => {
+ debug_assert!(entry.get().is_none());
+ entry.remove();
+ }
+ }
+ }
+
+ fn handle_model_notification_effect(&mut self, observed_id: usize) {
let callbacks = self.observations.lock().remove(&observed_id);
if let Some(callbacks) = callbacks {
if self.cx.models.contains_key(&observed_id) {
@@ -2038,7 +2121,11 @@ impl MutableAppContext {
}
}
- fn notify_view_observers(&mut self, observed_window_id: usize, observed_view_id: usize) {
+ fn handle_view_notification_effect(
+ &mut self,
+ observed_window_id: usize,
+ observed_view_id: usize,
+ ) {
if let Some(window) = self.cx.windows.get_mut(&observed_window_id) {
window
.invalidation
@@ -2079,7 +2166,7 @@ impl MutableAppContext {
}
}
- fn notify_global_observers(&mut self, observed_type_id: TypeId) {
+ fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
let callbacks = self.global_observations.lock().remove(&observed_type_id);
if let Some(callbacks) = callbacks {
if let Some(global) = self.cx.globals.remove(&observed_type_id) {
@@ -2107,16 +2194,18 @@ impl MutableAppContext {
}
}
- fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) {
+ fn handle_entity_release_effect(&mut self, entity_id: usize, entity: &dyn Any) {
let callbacks = self.release_observations.lock().remove(&entity_id);
if let Some(callbacks) = callbacks {
- for (_, mut callback) in callbacks {
+ for (_, callback) in callbacks {
callback(entity, self);
}
}
}
- fn focus(&mut self, window_id: usize, focused_id: Option<usize>) {
+ fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
+ self.pending_focus_index.take();
+
if self
.cx
.windows
@@ -2145,11 +2234,45 @@ impl MutableAppContext {
if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) {
focused_view.on_focus(this, window_id, focused_id);
this.cx.views.insert((window_id, focused_id), focused_view);
+
+ let callbacks = this.focus_observations.lock().remove(&focused_id);
+ if let Some(callbacks) = callbacks {
+ for (id, callback) in callbacks {
+ if let Some(mut callback) = callback {
+ let alive = callback(this);
+ if alive {
+ match this
+ .focus_observations
+ .lock()
+ .entry(focused_id)
+ .or_default()
+ .entry(id)
+ {
+ btree_map::Entry::Vacant(entry) => {
+ entry.insert(Some(callback));
+ }
+ btree_map::Entry::Occupied(entry) => {
+ entry.remove();
+ }
+ }
+ }
+ }
+ }
+ }
}
}
})
}
+ fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
+ if let Some(pending_focus_index) = self.pending_focus_index {
+ self.pending_effects.remove(pending_focus_index);
+ }
+ self.pending_focus_index = Some(self.pending_effects.len());
+ self.pending_effects
+ .push_back(Effect::Focus { window_id, view_id });
+ }
+
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
where
F: FnOnce(AsyncAppContext) -> Fut,
@@ -2316,6 +2439,12 @@ pub struct AppContext {
}
impl AppContext {
+ pub(crate) fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
+ self.windows
+ .get(&window_id)
+ .map(|window| window.root_view.clone())
+ }
+
pub fn root_view_id(&self, window_id: usize) -> Option<usize> {
self.windows
.get(&window_id)
@@ -2345,11 +2474,11 @@ impl AppContext {
}
pub fn global<T: 'static>(&self) -> &T {
- self.globals
- .get(&TypeId::of::<T>())
- .expect("no app state has been added for this type")
- .downcast_ref()
- .unwrap()
+ if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
+ global.downcast_ref().unwrap()
+ } else {
+ panic!("no global has been added for {}", type_name::<T>());
+ }
}
}
@@ -2495,6 +2624,11 @@ pub enum Effect {
window_id: usize,
view_id: Option<usize>,
},
+ FocusObservation {
+ view_id: usize,
+ subscription_id: usize,
+ callback: FocusObservationCallback,
+ },
ResizeWindow {
window_id: usize,
},
@@ -2566,6 +2700,15 @@ impl Debug for Effect {
.field("window_id", window_id)
.field("view_id", view_id)
.finish(),
+ Effect::FocusObservation {
+ view_id,
+ subscription_id,
+ ..
+ } => f
+ .debug_struct("Effect::FocusObservation")
+ .field("view_id", view_id)
+ .field("subscription_id", subscription_id)
+ .finish(),
Effect::ResizeWindow { window_id } => f
.debug_struct("Effect::RefreshWindow")
.field("window_id", window_id)
@@ -2629,6 +2772,7 @@ pub trait AnyView {
fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
+ fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
}
impl<T> AnyView for T
@@ -2692,6 +2836,10 @@ where
fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
View::keymap_context(self, cx)
}
+
+ fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+ View::debug_json(self, cx)
+ }
}
pub struct ModelContext<'a, T: ?Sized> {
@@ -2973,24 +3121,15 @@ impl<'a, T: View> ViewContext<'a, T> {
S: Into<AnyViewHandle>,
{
let handle = handle.into();
- self.app.pending_effects.push_back(Effect::Focus {
- window_id: handle.window_id,
- view_id: Some(handle.view_id),
- });
+ self.app.focus(handle.window_id, Some(handle.view_id));
}
pub fn focus_self(&mut self) {
- self.app.pending_effects.push_back(Effect::Focus {
- window_id: self.window_id,
- view_id: Some(self.view_id),
- });
+ self.app.focus(self.window_id, Some(self.view_id));
}
pub fn blur(&mut self) {
- self.app.pending_effects.push_back(Effect::Focus {
- window_id: self.window_id,
- view_id: None,
- });
+ self.app.focus(self.window_id, None);
}
pub fn add_model<S, F>(&mut self, build_model: F) -> ModelHandle<S>
@@ -3057,6 +3196,24 @@ impl<'a, T: View> ViewContext<'a, T> {
})
}
+ pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
+ where
+ F: 'static + FnMut(&mut T, ViewHandle<V>, &mut ViewContext<T>),
+ V: View,
+ {
+ let observer = self.weak_handle();
+ self.app.observe_focus(handle, move |observed, cx| {
+ if let Some(observer) = observer.upgrade(cx) {
+ observer.update(cx, |observer, cx| {
+ callback(observer, observed, cx);
+ });
+ true
+ } else {
+ false
+ }
+ })
+ }
+
pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
where
E: Entity,
@@ -3301,6 +3458,10 @@ pub trait Handle<T> {
Self: Sized;
}
+pub trait WeakHandle {
+ fn id(&self) -> usize;
+}
+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum EntityLocation {
Model(usize),
@@ -3375,12 +3536,10 @@ impl<T: Entity> ModelHandle<T> {
#[cfg(any(test, feature = "test-support"))]
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
- use postage::prelude::{Sink as _, Stream as _};
-
- let (mut tx, mut rx) = postage::mpsc::channel(1);
+ let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.observe(self, move |_, _| {
- tx.try_send(()).ok();
+ tx.unbounded_send(()).ok();
});
let duration = if std::env::var("CI").is_ok() {
@@ -3390,7 +3549,7 @@ impl<T: Entity> ModelHandle<T> {
};
async move {
- let notification = crate::util::timeout(duration, rx.recv())
+ let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");
drop(subscription);
@@ -3403,12 +3562,10 @@ impl<T: Entity> ModelHandle<T> {
where
T::Event: Clone,
{
- use postage::prelude::{Sink as _, Stream as _};
-
- let (mut tx, mut rx) = postage::mpsc::channel(1);
+ let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscription = cx.subscribe(self, move |_, event, _| {
- tx.blocking_send(event.clone()).ok();
+ tx.unbounded_send(event.clone()).ok();
});
let duration = if std::env::var("CI").is_ok() {
@@ -3417,8 +3574,9 @@ impl<T: Entity> ModelHandle<T> {
Duration::from_secs(1)
};
+ cx.foreground.start_waiting();
async move {
- let event = crate::util::timeout(duration, rx.recv())
+ let event = crate::util::timeout(duration, rx.next())
.await
.expect("next event timed out");
drop(subscription);
@@ -3432,22 +3590,20 @@ impl<T: Entity> ModelHandle<T> {
cx: &TestAppContext,
mut predicate: impl FnMut(&T, &AppContext) -> bool,
) -> impl Future<Output = ()> {
- use postage::prelude::{Sink as _, Stream as _};
-
- let (tx, mut rx) = postage::mpsc::channel(1024);
+ let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut cx = cx.cx.borrow_mut();
let subscriptions = (
cx.observe(self, {
- let mut tx = tx.clone();
+ let tx = tx.clone();
move |_, _| {
- tx.blocking_send(()).ok();
+ tx.unbounded_send(()).ok();
}
}),
cx.subscribe(self, {
- let mut tx = tx.clone();
+ let tx = tx.clone();
move |_, _, _| {
- tx.blocking_send(()).ok();
+ tx.unbounded_send(()).ok();
}
}),
);
@@ -3478,7 +3634,7 @@ impl<T: Entity> ModelHandle<T> {
}
cx.borrow().foreground().start_waiting();
- rx.recv()
+ rx.next()
.await
.expect("model dropped with pending condition");
cx.borrow().foreground().finish_waiting();
@@ -3575,6 +3731,12 @@ pub struct WeakModelHandle<T> {
model_type: PhantomData<T>,
}
+impl<T> WeakHandle for WeakModelHandle<T> {
+ fn id(&self) -> usize {
+ self.model_id
+ }
+}
+
unsafe impl<T> Send for WeakModelHandle<T> {}
unsafe impl<T> Sync for WeakModelHandle<T> {}
@@ -3961,6 +4123,12 @@ impl AnyViewHandle {
pub fn view_type(&self) -> TypeId {
self.view_type
}
+
+ pub fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
+ cx.views
+ .get(&(self.window_id, self.view_id))
+ .map_or_else(|| serde_json::Value::Null, |view| view.debug_json(cx))
+ }
}
impl Clone for AnyViewHandle {
@@ -4144,6 +4312,12 @@ pub struct WeakViewHandle<T> {
view_type: PhantomData<T>,
}
+impl<T> WeakHandle for WeakViewHandle<T> {
+ fn id(&self) -> usize {
+ self.view_id
+ }
+}
+
impl<T: View> WeakViewHandle<T> {
fn new(window_id: usize, view_id: usize) -> Self {
Self {
@@ -4304,6 +4478,12 @@ pub enum Subscription {
Weak<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
>,
},
+ FocusObservation {
+ id: usize,
+ view_id: usize,
+ observations:
+ Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<FocusObservationCallback>>>>>>,
+ },
ReleaseObservation {
id: usize,
entity_id: usize,
@@ -4330,6 +4510,9 @@ impl Subscription {
Subscription::ReleaseObservation { observations, .. } => {
observations.take();
}
+ Subscription::FocusObservation { observations, .. } => {
+ observations.take();
+ }
}
}
}
@@ -0,0 +1,109 @@
+use std::any::{Any, TypeId};
+
+pub trait Action: 'static {
+ fn id(&self) -> TypeId;
+ fn name(&self) -> &'static str;
+ fn as_any(&self) -> &dyn Any;
+ fn boxed_clone(&self) -> Box<dyn Action>;
+
+ fn qualified_name() -> &'static str
+ where
+ Self: Sized;
+ fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
+ where
+ Self: Sized;
+}
+
+/// Define a set of unit struct types that all implement the `Action` trait.
+///
+/// The first argument is a namespace that will be associated with each of
+/// the given action types, to ensure that they have globally unique
+/// qualified names for use in keymap files.
+#[macro_export]
+macro_rules! actions {
+ ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+ $(
+ #[derive(Clone, Debug, Default, PartialEq, Eq)]
+ pub struct $name;
+ $crate::__impl_action! {
+ $namespace,
+ $name,
+ fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+ Ok(Box::new(Self))
+ }
+ }
+ )*
+ };
+}
+
+/// Implement the `Action` trait for a set of existing types.
+///
+/// The first argument is a namespace that will be associated with each of
+/// the given action types, to ensure that they have globally unique
+/// qualified names for use in keymap files.
+#[macro_export]
+macro_rules! impl_actions {
+ ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+ $(
+ $crate::__impl_action! {
+ $namespace,
+ $name,
+ fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+ Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
+ }
+ }
+ )*
+ };
+}
+
+/// Implement the `Action` trait for a set of existing types that are
+/// not intended to be constructed via a keymap file, but only dispatched
+/// internally.
+#[macro_export]
+macro_rules! impl_internal_actions {
+ ($namespace:path, [ $($name:ident),* $(,)? ]) => {
+ $(
+ $crate::__impl_action! {
+ $namespace,
+ $name,
+ fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+ Err($crate::anyhow::anyhow!("internal action"))
+ }
+ }
+ )*
+ };
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __impl_action {
+ ($namespace:path, $name:ident, $from_json_fn:item) => {
+ impl $crate::action::Action for $name {
+ fn name(&self) -> &'static str {
+ stringify!($name)
+ }
+
+ fn qualified_name() -> &'static str {
+ concat!(
+ stringify!($namespace),
+ "::",
+ stringify!($name),
+ )
+ }
+
+ fn id(&self) -> std::any::TypeId {
+ std::any::TypeId::of::<$name>()
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ fn boxed_clone(&self) -> Box<dyn $crate::Action> {
+ Box::new(self.clone())
+ }
+
+ $from_json_fn
+ }
+ };
+}
@@ -74,6 +74,7 @@ pub trait Element {
&mut self,
event: &Event,
bounds: RectF,
+ visible_bounds: RectF,
layout: &mut Self::LayoutState,
paint: &mut Self::PaintState,
cx: &mut EventContext,
@@ -169,6 +170,7 @@ pub enum Lifecycle<T: Element> {
element: T,
constraint: SizeConstraint,
bounds: RectF,
+ visible_bounds: RectF,
layout: T::LayoutState,
paint: T::PaintState,
},
@@ -222,6 +224,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
element,
constraint,
bounds,
+ visible_bounds,
layout,
paint,
}
@@ -242,6 +245,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
element,
constraint,
bounds,
+ visible_bounds,
layout,
paint,
}
@@ -254,12 +258,13 @@ impl<T: Element> AnyElement for Lifecycle<T> {
if let Lifecycle::PostPaint {
element,
bounds,
+ visible_bounds,
layout,
paint,
..
} = self
{
- element.dispatch_event(event, *bounds, layout, paint, cx)
+ element.dispatch_event(event, *bounds, *visible_bounds, layout, paint, cx)
} else {
panic!("invalid element lifecycle state");
}
@@ -288,6 +293,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
element,
constraint,
bounds,
+ visible_bounds,
layout,
paint,
} => {
@@ -299,6 +305,8 @@ impl<T: Element> AnyElement for Lifecycle<T> {
new_map.insert("type".into(), typ);
}
new_map.insert("constraint".into(), constraint.to_json());
+ new_map.insert("bounds".into(), bounds.to_json());
+ new_map.insert("visible_bounds".into(), visible_bounds.to_json());
new_map.append(map);
json::Value::Object(new_map)
} else {
@@ -85,7 +85,8 @@ impl Element for Align {
fn dispatch_event(
&mut self,
event: &Event,
- _: pathfinder_geometry::rect::RectF,
+ _: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -59,6 +59,7 @@ where
&mut self,
_: &crate::Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut crate::EventContext,
@@ -81,6 +81,7 @@ impl Element for ConstrainedBox {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -247,6 +247,7 @@ impl Element for Container {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -52,6 +52,7 @@ impl Element for Empty {
&mut self,
_: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
@@ -84,13 +84,14 @@ impl Element for EventHandler {
fn dispatch_event(
&mut self,
event: &Event,
- bounds: RectF,
+ _: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
if let Some(capture) = self.capture.as_mut() {
- if capture(event, bounds, cx) {
+ if capture(event, visible_bounds, cx) {
return true;
}
}
@@ -101,7 +102,7 @@ impl Element for EventHandler {
match event {
Event::LeftMouseDown { position, .. } => {
if let Some(callback) = self.mouse_down.as_mut() {
- if bounds.contains_point(*position) {
+ if visible_bounds.contains_point(*position) {
return callback(cx);
}
}
@@ -109,7 +110,7 @@ impl Element for EventHandler {
}
Event::RightMouseDown { position, .. } => {
if let Some(callback) = self.right_mouse_down.as_mut() {
- if bounds.contains_point(*position) {
+ if visible_bounds.contains_point(*position) {
return callback(cx);
}
}
@@ -121,7 +122,7 @@ impl Element for EventHandler {
..
} => {
if let Some(callback) = self.navigate_mouse_down.as_mut() {
- if bounds.contains_point(*position) {
+ if visible_bounds.contains_point(*position) {
return callback(*direction, cx);
}
}
@@ -66,6 +66,7 @@ impl Element for Expanded {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY};
use crate::{
json::{self, ToJson, Value},
- Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
- SizeConstraint, Vector2FExt,
+ Axis, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event,
+ EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
};
use pathfinder_geometry::{
rect::RectF,
@@ -11,9 +11,16 @@ use pathfinder_geometry::{
};
use serde_json::json;
+#[derive(Default)]
+struct ScrollState {
+ scroll_to: Option<usize>,
+ scroll_position: f32,
+}
+
pub struct Flex {
axis: Axis,
children: Vec<ElementBox>,
+ scroll_state: Option<ElementStateHandle<ScrollState>>,
}
impl Flex {
@@ -21,6 +28,7 @@ impl Flex {
Self {
axis,
children: Default::default(),
+ scroll_state: None,
}
}
@@ -32,6 +40,22 @@ impl Flex {
Self::new(Axis::Vertical)
}
+ pub fn scrollable<Tag, C>(
+ mut self,
+ element_id: usize,
+ scroll_to: Option<usize>,
+ cx: &mut C,
+ ) -> Self
+ where
+ Tag: 'static,
+ C: ElementStateContext,
+ {
+ let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
+ scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
+ self.scroll_state = Some(scroll_state);
+ self
+ }
+
fn layout_flex_children(
&mut self,
layout_expanded: bool,
@@ -167,6 +191,30 @@ impl Element for Flex {
size.set_y(constraint.max.y());
}
+ if let Some(scroll_state) = self.scroll_state.as_ref() {
+ scroll_state.update(cx, |scroll_state, _| {
+ if let Some(scroll_to) = scroll_state.scroll_to.take() {
+ let visible_start = scroll_state.scroll_position;
+ let visible_end = visible_start + size.along(self.axis);
+ if let Some(child) = self.children.get(scroll_to) {
+ let child_start: f32 = self.children[..scroll_to]
+ .iter()
+ .map(|c| c.size().along(self.axis))
+ .sum();
+ let child_end = child_start + child.size().along(self.axis);
+ if child_start < visible_start {
+ scroll_state.scroll_position = child_start;
+ } else if child_end > visible_end {
+ scroll_state.scroll_position = child_end - size.along(self.axis);
+ }
+ }
+ }
+
+ scroll_state.scroll_position =
+ scroll_state.scroll_position.min(-remaining_space).max(0.);
+ });
+ }
+
(size, remaining_space)
}
@@ -181,7 +229,16 @@ impl Element for Flex {
if overflowing {
cx.scene.push_layer(Some(bounds));
}
+
let mut child_origin = bounds.origin();
+ if let Some(scroll_state) = self.scroll_state.as_ref() {
+ let scroll_position = scroll_state.read(cx).scroll_position;
+ match self.axis {
+ Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
+ Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
+ }
+ }
+
for child in &mut self.children {
if *remaining_space > 0. {
if let Some(metadata) = child.metadata::<FlexParentData>() {
@@ -208,8 +265,9 @@ impl Element for Flex {
fn dispatch_event(
&mut self,
event: &Event,
+ bounds: RectF,
_: RectF,
- _: &mut Self::LayoutState,
+ remaining_space: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
@@ -217,6 +275,39 @@ impl Element for Flex {
for child in &mut self.children {
handled = child.dispatch_event(event, cx) || handled;
}
+ if !handled {
+ if let &Event::ScrollWheel {
+ position,
+ delta,
+ precise,
+ } = event
+ {
+ if *remaining_space < 0. && bounds.contains_point(position) {
+ if let Some(scroll_state) = self.scroll_state.as_ref() {
+ scroll_state.update(cx, |scroll_state, cx| {
+ let mut delta = match self.axis {
+ Axis::Horizontal => {
+ if delta.x() != 0. {
+ delta.x()
+ } else {
+ delta.y()
+ }
+ }
+ Axis::Vertical => delta.y(),
+ };
+ if !precise {
+ delta *= 20.;
+ }
+
+ scroll_state.scroll_position -= delta;
+
+ handled = true;
+ cx.notify();
+ });
+ }
+ }
+ }
+ }
handled
}
@@ -295,6 +386,7 @@ impl Element for FlexItem {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -57,6 +57,7 @@ impl Element for Hook {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -81,6 +81,7 @@ impl Element for Image {
&mut self,
_: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
@@ -166,6 +166,7 @@ impl Element for Label {
&mut self,
_: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
@@ -253,6 +253,7 @@ impl Element for List {
&mut self,
event: &Event,
bounds: RectF,
+ _: RectF,
scroll_top: &mut ListOffset,
_: &mut (),
cx: &mut EventContext,
@@ -872,6 +873,7 @@ mod tests {
&mut self,
_: &Event,
_: RectF,
+ _: RectF,
_: &mut (),
_: &mut (),
_: &mut EventContext,
@@ -99,7 +99,8 @@ impl Element for MouseEventHandler {
fn dispatch_event(
&mut self,
event: &Event,
- bounds: RectF,
+ _: RectF,
+ visible_bounds: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -112,8 +113,8 @@ impl Element for MouseEventHandler {
let handled_in_child = self.child.dispatch_event(event, cx);
let hit_bounds = RectF::from_points(
- bounds.origin() - vec2f(self.padding.left, self.padding.top),
- bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
+ visible_bounds.origin() - vec2f(self.padding.left, self.padding.top),
+ visible_bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
)
.round_out();
@@ -44,6 +44,7 @@ impl Element for Overlay {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -51,6 +51,7 @@ impl Element for Stack {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -76,6 +76,7 @@ impl Element for Svg {
&mut self,
_: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
@@ -172,6 +172,7 @@ impl Element for Text {
&mut self,
_: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
@@ -8,11 +8,10 @@ use crate::{
ElementBox,
};
use json::ToJson;
-use parking_lot::Mutex;
-use std::{cmp, ops::Range, sync::Arc};
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
#[derive(Clone, Default)]
-pub struct UniformListState(Arc<Mutex<StateInner>>);
+pub struct UniformListState(Rc<RefCell<StateInner>>);
#[derive(Debug)]
pub enum ScrollTarget {
@@ -22,11 +21,11 @@ pub enum ScrollTarget {
impl UniformListState {
pub fn scroll_to(&self, scroll_to: ScrollTarget) {
- self.0.lock().scroll_to = Some(scroll_to);
+ self.0.borrow_mut().scroll_to = Some(scroll_to);
}
pub fn scroll_top(&self) -> f32 {
- self.0.lock().scroll_top
+ self.0.borrow().scroll_top
}
}
@@ -96,7 +95,7 @@ where
delta *= 20.;
}
- let mut state = self.state.0.lock();
+ let mut state = self.state.0.borrow_mut();
state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
cx.notify();
@@ -104,7 +103,7 @@ where
}
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
- let mut state = self.state.0.lock();
+ let mut state = self.state.0.borrow_mut();
if let Some(scroll_to) = state.scroll_to.take() {
let item_ix;
@@ -141,7 +140,7 @@ where
}
fn scroll_top(&self) -> f32 {
- self.state.0.lock().scroll_top
+ self.state.0.borrow().scroll_top
}
}
@@ -282,6 +281,7 @@ where
&mut self,
event: &Event,
bounds: RectF,
+ _: RectF,
layout: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -1,5 +1,6 @@
use anyhow::{anyhow, Result};
use async_task::Runnable;
+use futures::channel::mpsc;
use smol::{channel, prelude::*, Executor};
use std::{
any::Any,
@@ -621,17 +622,13 @@ impl Background {
Err(async { *future.await.downcast().unwrap() })
}
- pub async fn scoped<'scope, F>(&self, scheduler: F)
+ pub async fn scoped<'scope, F>(self: &Arc<Self>, scheduler: F)
where
F: FnOnce(&mut Scope<'scope>),
{
- let mut scope = Scope {
- futures: Default::default(),
- _phantom: PhantomData,
- };
+ let mut scope = Scope::new(self.clone());
(scheduler)(&mut scope);
- let spawned = scope
- .futures
+ let spawned = mem::take(&mut scope.futures)
.into_iter()
.map(|f| self.spawn(f))
.collect::<Vec<_>>();
@@ -668,25 +665,56 @@ impl Background {
}
pub struct Scope<'a> {
+ executor: Arc<Background>,
futures: Vec<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
+ tx: Option<mpsc::Sender<()>>,
+ rx: mpsc::Receiver<()>,
_phantom: PhantomData<&'a ()>,
}
impl<'a> Scope<'a> {
+ fn new(executor: Arc<Background>) -> Self {
+ let (tx, rx) = mpsc::channel(1);
+ Self {
+ executor,
+ tx: Some(tx),
+ rx,
+ futures: Default::default(),
+ _phantom: PhantomData,
+ }
+ }
+
pub fn spawn<F>(&mut self, f: F)
where
F: Future<Output = ()> + Send + 'a,
{
+ let tx = self.tx.clone().unwrap();
+
+ // Safety: The 'a lifetime is guaranteed to outlive any of these futures because
+ // dropping this `Scope` blocks until all of the futures have resolved.
let f = unsafe {
mem::transmute::<
Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
- >(Box::pin(f))
+ >(Box::pin(async move {
+ f.await;
+ drop(tx);
+ }))
};
self.futures.push(f);
}
}
+impl<'a> Drop for Scope<'a> {
+ fn drop(&mut self) {
+ self.tx.take().unwrap();
+
+ // Wait until the channel is closed, which means that all of the spawned
+ // futures have resolved.
+ self.executor.block(self.rx.next());
+ }
+}
+
impl<T> Task<T> {
pub fn ready(value: T) -> Self {
Self::Ready(Some(value))
@@ -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;
@@ -1,13 +1,13 @@
-use anyhow::anyhow;
+use crate::Action;
+use anyhow::{anyhow, Result};
+use smallvec::SmallVec;
use std::{
- any::Any,
+ any::{Any, TypeId},
collections::{HashMap, HashSet},
fmt::Debug,
};
use tree_sitter::{Language, Node, Parser};
-use crate::{Action, AnyAction};
-
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
@@ -24,11 +24,14 @@ struct Pending {
}
#[derive(Default)]
-pub struct Keymap(Vec<Binding>);
+pub struct Keymap {
+ bindings: Vec<Binding>,
+ binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
+}
pub struct Binding {
keystrokes: Vec<Keystroke>,
- action: Box<dyn AnyAction>,
+ action: Box<dyn Action>,
context: Option<ContextPredicate>,
}
@@ -73,7 +76,7 @@ where
pub enum MatchResult {
None,
Pending,
- Action(Box<dyn AnyAction>),
+ Action(Box<dyn Action>),
}
impl Debug for MatchResult {
@@ -107,6 +110,15 @@ impl Matcher {
self.keymap.add_bindings(bindings);
}
+ pub fn clear_bindings(&mut self) {
+ self.pending.clear();
+ self.keymap.clear();
+ }
+
+ pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
+ self.keymap.bindings_for_action_type(action_type)
+ }
+
pub fn clear_pending(&mut self) {
self.pending.clear();
}
@@ -128,7 +140,7 @@ impl Matcher {
pending.keystrokes.push(keystroke);
let mut retain_pending = false;
- for binding in self.keymap.0.iter().rev() {
+ for binding in self.keymap.bindings.iter().rev() {
if binding.keystrokes.starts_with(&pending.keystrokes)
&& binding.context.as_ref().map(|c| c.eval(cx)).unwrap_or(true)
{
@@ -159,30 +171,73 @@ impl Default for Matcher {
impl Keymap {
pub fn new(bindings: Vec<Binding>) -> Self {
- Self(bindings)
+ let mut binding_indices_by_action_type = HashMap::new();
+ for (ix, binding) in bindings.iter().enumerate() {
+ binding_indices_by_action_type
+ .entry(binding.action.as_any().type_id())
+ .or_insert_with(|| SmallVec::new())
+ .push(ix);
+ }
+ Self {
+ binding_indices_by_action_type,
+ bindings,
+ }
+ }
+
+ fn bindings_for_action_type<'a>(
+ &'a self,
+ action_type: TypeId,
+ ) -> impl Iterator<Item = &'a Binding> {
+ self.binding_indices_by_action_type
+ .get(&action_type)
+ .map(SmallVec::as_slice)
+ .unwrap_or(&[])
+ .iter()
+ .map(|ix| &self.bindings[*ix])
}
fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
- self.0.extend(bindings.into_iter());
+ for binding in bindings {
+ self.binding_indices_by_action_type
+ .entry(binding.action.as_any().type_id())
+ .or_default()
+ .push(self.bindings.len());
+ self.bindings.push(binding);
+ }
+ }
+
+ fn clear(&mut self) {
+ self.bindings.clear();
+ self.binding_indices_by_action_type.clear();
}
}
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
+ Self::load(keystrokes, Box::new(action), context).unwrap()
+ }
+
+ pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context {
- Some(ContextPredicate::parse(context).unwrap())
+ Some(ContextPredicate::parse(context)?)
} else {
None
};
- Self {
- keystrokes: keystrokes
- .split_whitespace()
- .map(|key| Keystroke::parse(key).unwrap())
- .collect(),
- action: Box::new(action),
+ let keystrokes = keystrokes
+ .split_whitespace()
+ .map(|key| Keystroke::parse(key))
+ .collect::<Result<_>>()?;
+
+ Ok(Self {
+ keystrokes,
+ action,
context,
- }
+ })
+ }
+
+ pub fn keystrokes(&self) -> &[Keystroke] {
+ &self.keystrokes
}
}
@@ -329,7 +384,9 @@ impl ContextPredicate {
#[cfg(test)]
mod tests {
- use crate::action;
+ use serde::Deserialize;
+
+ use crate::{actions, impl_actions};
use super::*;
@@ -420,29 +477,18 @@ mod tests {
#[test]
fn test_matcher() -> anyhow::Result<()> {
- action!(A, &'static str);
- action!(B);
- action!(Ab);
-
- impl PartialEq for A {
- fn eq(&self, other: &Self) -> bool {
- self.0 == other.0
- }
- }
- impl Eq for A {}
- impl Debug for A {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "A({:?})", &self.0)
- }
- }
+ #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+ pub struct A(pub String);
+ impl_actions!(test, [A]);
+ actions!(test, [B, Ab]);
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
- let keymap = Keymap(vec![
- Binding::new("a", A("x"), Some("a")),
+ let keymap = Keymap::new(vec![
+ Binding::new("a", A("x".to_string()), Some("a")),
Binding::new("b", B, Some("a")),
Binding::new("a b", Ab, Some("a || b")),
]);
@@ -456,40 +502,54 @@ mod tests {
let mut matcher = Matcher::new(keymap);
// Basic match
- assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
+ assert_eq!(
+ downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
+ Some(&A("x".to_string()))
+ );
// Multi-keystroke match
- assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
- assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
+ assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
+ assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
// Failed matches don't interfere with matching subsequent keys
- assert_eq!(matcher.test_keystroke::<A>("x", 1, &ctx_a), None);
- assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
+ assert!(matcher.test_keystroke("x", 1, &ctx_a).is_none());
+ assert_eq!(
+ downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
+ Some(&A("x".to_string()))
+ );
// Pending keystrokes are cleared when the context changes
- assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
- assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B));
+ assert!(&matcher.test_keystroke("a", 1, &ctx_b).is_none());
+ assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_a)), Some(&B));
let mut ctx_c = Context::default();
ctx_c.set.insert("c".into());
// Pending keystrokes are maintained per-view
- assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
- assert_eq!(matcher.test_keystroke::<A>("a", 2, &ctx_c), None);
- assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
+ assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
+ assert!(matcher.test_keystroke("a", 2, &ctx_c).is_none());
+ assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
Ok(())
}
+ fn downcast<'a, A: Action>(action: &'a Option<Box<dyn Action>>) -> Option<&'a A> {
+ action
+ .as_ref()
+ .and_then(|action| action.as_any().downcast_ref())
+ }
+
impl Matcher {
- fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
- where
- A: Action + Debug + Eq,
- {
+ fn test_keystroke(
+ &mut self,
+ keystroke: &str,
+ view_id: usize,
+ cx: &Context,
+ ) -> Option<Box<dyn Action>> {
if let MatchResult::Action(action) =
self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
{
- Some(*action.boxed_clone_as_any().downcast().unwrap())
+ Some(action.boxed_clone())
} else {
None
}
@@ -15,7 +15,7 @@ use crate::{
vector::Vector2F,
},
text_layout::{LineLayout, RunStyle},
- AnyAction, ClipboardItem, Menu, Scene,
+ Action, ClipboardItem, Menu, Scene,
};
use anyhow::{anyhow, Result};
use async_task::Runnable;
@@ -56,7 +56,7 @@ pub trait Platform: Send + Sync {
fn local_timezone(&self) -> UtcOffset;
- fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf>;
+ fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
fn app_path(&self) -> Result<PathBuf>;
fn app_version(&self) -> Result<AppVersion>;
}
@@ -66,10 +66,10 @@ pub(crate) trait ForegroundPlatform {
fn on_resign_active(&self, callback: Box<dyn FnMut()>);
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_event(&self, callback: Box<dyn FnMut(Event) -> bool>);
- fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>);
+ fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
- fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>);
+ fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn set_menus(&self, menus: Vec<Menu>);
fn prompt_for_paths(
&self,
@@ -164,6 +164,12 @@ impl FromStr for AppVersion {
}
}
+#[derive(Copy, Clone, Debug)]
+pub enum RasterizationOptions {
+ Alpha,
+ Bgra,
+}
+
pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
@@ -183,6 +189,7 @@ pub trait FontSystem: Send + Sync {
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
+ options: RasterizationOptions,
) -> Option<(RectI, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout;
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize>;
@@ -61,3 +61,20 @@ pub enum Event {
left_mouse_down: bool,
},
}
+
+impl Event {
+ pub fn position(&self) -> Option<Vector2F> {
+ match self {
+ Event::KeyDown { .. } => None,
+ Event::ScrollWheel { position, .. }
+ | Event::LeftMouseDown { position, .. }
+ | Event::LeftMouseUp { position }
+ | Event::LeftMouseDragged { position }
+ | Event::RightMouseDown { position, .. }
+ | Event::RightMouseUp { position }
+ | Event::NavigateMouseDown { position, .. }
+ | Event::NavigateMouseUp { position, .. }
+ | Event::MouseMoved { position, .. } => Some(*position),
+ }
+ }
+}
@@ -4,6 +4,7 @@ use crate::geometry::{
};
use etagere::BucketedAtlasAllocator;
use foreign_types::ForeignType;
+use log::warn;
use metal::{Device, TextureDescriptor};
use objc::{msg_send, sel, sel_impl};
@@ -40,31 +41,40 @@ impl AtlasAllocator {
)
}
- pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) {
- let (alloc_id, origin) = self
+ pub fn allocate(&mut self, requested_size: Vector2I) -> Option<(AllocId, Vector2I)> {
+ let allocation = self
.atlases
.last_mut()
.unwrap()
.allocate(requested_size)
- .unwrap_or_else(|| {
+ .or_else(|| {
let mut atlas = self.new_atlas(requested_size);
- let (id, origin) = atlas.allocate(requested_size).unwrap();
+ let (id, origin) = atlas.allocate(requested_size)?;
self.atlases.push(atlas);
- (id, origin)
+ Some((id, origin))
});
+ if allocation.is_none() {
+ warn!(
+ "allocation of size {:?} could not be created",
+ requested_size,
+ );
+ }
+
+ let (alloc_id, origin) = allocation?;
+
let id = AllocId {
atlas_id: self.atlases.len() - 1,
alloc_id,
};
- (id, origin)
+ Some((id, origin))
}
- pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) {
- let (alloc_id, origin) = self.allocate(size);
+ pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> Option<(AllocId, RectI)> {
+ let (alloc_id, origin) = self.allocate(size)?;
let bounds = RectI::new(origin, size);
self.atlases[alloc_id.atlas_id].upload(bounds, bytes);
- (alloc_id, bounds)
+ Some((alloc_id, bounds))
}
pub fn deallocate(&mut self, id: AllocId) {
@@ -3,26 +3,27 @@ use crate::{
geometry::{
rect::{RectF, RectI},
transform2d::Transform2F,
- vector::{vec2f, vec2i, Vector2F},
+ vector::{vec2f, Vector2F},
},
- platform,
+ platform::{self, RasterizationOptions},
text_layout::{Glyph, LineLayout, Run, RunStyle},
};
use cocoa::appkit::{CGFloat, CGPoint};
+use collections::HashMap;
use core_foundation::{
array::CFIndex,
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
base::{CFRange, TCFType},
- number::CFNumber,
string::CFString,
};
use core_graphics::{
- base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform,
+ base::{kCGImageAlphaPremultipliedLast, CGGlyph},
+ color_space::CGColorSpace,
+ context::CGContext,
};
-use core_text::{line::CTLine, string_attributes::kCTFontAttributeName};
+use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
use font_kit::{
- canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource,
- sources::mem::MemSource,
+ handle::Handle, hinting::HintingOptions, source::SystemSource, sources::mem::MemSource,
};
use parking_lot::RwLock;
use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc};
@@ -36,6 +37,8 @@ struct FontSystemState {
memory_source: MemSource,
system_source: SystemSource,
fonts: Vec<font_kit::font::Font>,
+ font_ids_by_postscript_name: HashMap<String, FontId>,
+ postscript_names_by_font_id: HashMap<FontId, String>,
}
impl FontSystem {
@@ -44,6 +47,8 @@ impl FontSystem {
memory_source: MemSource::empty(),
system_source: SystemSource::new(),
fonts: Vec::new(),
+ font_ids_by_postscript_name: Default::default(),
+ postscript_names_by_font_id: Default::default(),
}))
}
}
@@ -84,14 +89,20 @@ impl platform::FontSystem for FontSystem {
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
+ options: RasterizationOptions,
) -> Option<(RectI, Vec<u8>)> {
- self.0
- .read()
- .rasterize_glyph(font_id, font_size, glyph_id, subpixel_shift, scale_factor)
+ self.0.read().rasterize_glyph(
+ font_id,
+ font_size,
+ glyph_id,
+ subpixel_shift,
+ scale_factor,
+ options,
+ )
}
fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
- self.0.read().layout_line(text, font_size, runs)
+ self.0.write().layout_line(text, font_size, runs)
}
fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec<usize> {
@@ -118,7 +129,13 @@ impl FontSystemState {
.or_else(|_| self.system_source.select_family_by_name(name))?;
for font in family.fonts() {
let font = font.load()?;
- font_ids.push(FontId(self.fonts.len()));
+ let font_id = FontId(self.fonts.len());
+ font_ids.push(font_id);
+ let postscript_name = font.postscript_name().unwrap();
+ self.font_ids_by_postscript_name
+ .insert(postscript_name.clone(), font_id);
+ self.postscript_names_by_font_id
+ .insert(font_id, postscript_name);
self.fonts.push(font);
}
Ok(font_ids)
@@ -149,6 +166,32 @@ impl FontSystemState {
self.fonts[font_id.0].glyph_for_char(ch)
}
+ fn id_for_native_font(&mut self, requested_font: CTFont) -> FontId {
+ let postscript_name = requested_font.postscript_name();
+ if let Some(font_id) = self.font_ids_by_postscript_name.get(&postscript_name) {
+ *font_id
+ } else {
+ let font_id = FontId(self.fonts.len());
+ self.font_ids_by_postscript_name
+ .insert(postscript_name.clone(), font_id);
+ self.postscript_names_by_font_id
+ .insert(font_id, postscript_name);
+ self.fonts
+ .push(font_kit::font::Font::from_core_graphics_font(
+ requested_font.copy_to_CGFont(),
+ ));
+ font_id
+ }
+ }
+
+ fn is_emoji(&self, font_id: FontId) -> bool {
+ self.postscript_names_by_font_id
+ .get(&font_id)
+ .map_or(false, |postscript_name| {
+ postscript_name == "AppleColorEmoji"
+ })
+ }
+
fn rasterize_glyph(
&self,
font_id: FontId,
@@ -156,65 +199,103 @@ impl FontSystemState {
glyph_id: GlyphId,
subpixel_shift: Vector2F,
scale_factor: f32,
+ options: RasterizationOptions,
) -> Option<(RectI, Vec<u8>)> {
let font = &self.fonts[font_id.0];
let scale = Transform2F::from_scale(scale_factor);
- let bounds = font
+ let glyph_bounds = font
.raster_bounds(
glyph_id,
font_size,
scale,
HintingOptions::None,
- RasterizationOptions::GrayscaleAa,
+ font_kit::canvas::RasterizationOptions::GrayscaleAa,
)
.ok()?;
- if bounds.width() == 0 || bounds.height() == 0 {
+ if glyph_bounds.width() == 0 || glyph_bounds.height() == 0 {
None
} else {
// Make room for subpixel variants.
- let bounds = RectI::new(bounds.origin(), bounds.size() + vec2i(1, 1));
- let mut pixels = vec![0; bounds.width() as usize * bounds.height() as usize];
- let cx = CGContext::create_bitmap_context(
- Some(pixels.as_mut_ptr() as *mut _),
- bounds.width() as usize,
- bounds.height() as usize,
- 8,
- bounds.width() as usize,
- &CGColorSpace::create_device_gray(),
- kCGImageAlphaOnly,
+ let subpixel_padding = subpixel_shift.ceil().to_i32();
+ let cx_bounds = RectI::new(
+ glyph_bounds.origin(),
+ glyph_bounds.size() + subpixel_padding,
);
+ let mut bytes;
+ let cx;
+ match options {
+ RasterizationOptions::Alpha => {
+ bytes = vec![0; cx_bounds.width() as usize * cx_bounds.height() as usize];
+ cx = CGContext::create_bitmap_context(
+ Some(bytes.as_mut_ptr() as *mut _),
+ cx_bounds.width() as usize,
+ cx_bounds.height() as usize,
+ 8,
+ cx_bounds.width() as usize,
+ &CGColorSpace::create_device_gray(),
+ kCGImageAlphaOnly,
+ );
+ }
+ RasterizationOptions::Bgra => {
+ bytes = vec![0; cx_bounds.width() as usize * 4 * cx_bounds.height() as usize];
+ cx = CGContext::create_bitmap_context(
+ Some(bytes.as_mut_ptr() as *mut _),
+ cx_bounds.width() as usize,
+ cx_bounds.height() as usize,
+ 8,
+ cx_bounds.width() as usize * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaPremultipliedLast,
+ );
+ }
+ }
+
// Move the origin to bottom left and account for scaling, this
// makes drawing text consistent with the font-kit's raster_bounds.
- cx.translate(0.0, bounds.height() as CGFloat);
- let transform = scale.translate(-bounds.origin().to_f32());
- cx.set_text_matrix(&CGAffineTransform {
- a: transform.matrix.m11() as CGFloat,
- b: -transform.matrix.m21() as CGFloat,
- c: -transform.matrix.m12() as CGFloat,
- d: transform.matrix.m22() as CGFloat,
- tx: transform.vector.x() as CGFloat,
- ty: -transform.vector.y() as CGFloat,
- });
-
- cx.set_font(&font.native_font().copy_to_CGFont());
- cx.set_font_size(font_size as CGFloat);
- cx.show_glyphs_at_positions(
- &[glyph_id as CGGlyph],
- &[CGPoint::new(
- (subpixel_shift.x() / scale_factor) as CGFloat,
- (subpixel_shift.y() / scale_factor) as CGFloat,
- )],
+ cx.translate(
+ -glyph_bounds.origin_x() as CGFloat,
+ (glyph_bounds.origin_y() + glyph_bounds.height()) as CGFloat,
);
+ cx.scale(scale_factor as CGFloat, scale_factor as CGFloat);
+
+ cx.set_allows_font_subpixel_positioning(true);
+ cx.set_should_subpixel_position_fonts(true);
+ cx.set_allows_font_subpixel_quantization(false);
+ cx.set_should_subpixel_quantize_fonts(false);
+ font.native_font()
+ .clone_with_font_size(font_size as CGFloat)
+ .draw_glyphs(
+ &[glyph_id as CGGlyph],
+ &[CGPoint::new(
+ (subpixel_shift.x() / scale_factor) as CGFloat,
+ (subpixel_shift.y() / scale_factor) as CGFloat,
+ )],
+ cx,
+ );
+
+ if let RasterizationOptions::Bgra = options {
+ // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
+ for pixel in bytes.chunks_exact_mut(4) {
+ pixel.swap(0, 2);
+ let a = pixel[3] as f32 / 255.;
+ pixel[0] = (pixel[0] as f32 / a) as u8;
+ pixel[1] = (pixel[1] as f32 / a) as u8;
+ pixel[2] = (pixel[2] as f32 / a) as u8;
+ }
+ }
- Some((bounds, pixels))
+ Some((cx_bounds, bytes))
}
}
- fn layout_line(&self, text: &str, font_size: f32, runs: &[(usize, RunStyle)]) -> LineLayout {
- let font_id_attr_name = CFString::from_static_string("zed_font_id");
-
+ fn layout_line(
+ &mut self,
+ text: &str,
+ font_size: f32,
+ runs: &[(usize, RunStyle)],
+ ) -> LineLayout {
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
let mut string = CFMutableAttributedString::new();
{
@@ -264,11 +345,6 @@ impl FontSystemState {
kCTFontAttributeName,
&font.native_font().clone_with_font_size(font_size as f64),
);
- string.set_attribute(
- cf_range,
- font_id_attr_name.as_concrete_TypeRef(),
- &CFNumber::from(font_id.0 as i64),
- );
}
if utf16_end == utf16_line_len {
@@ -282,15 +358,14 @@ impl FontSystemState {
let mut runs = Vec::new();
for run in line.glyph_runs().into_iter() {
- let font_id = FontId(
- run.attributes()
+ let attributes = run.attributes().unwrap();
+ let font = unsafe {
+ attributes
+ .get(kCTFontAttributeName)
+ .downcast::<CTFont>()
.unwrap()
- .get(&font_id_attr_name)
- .downcast::<CFNumber>()
- .unwrap()
- .to_i64()
- .unwrap() as usize,
- );
+ };
+ let font_id = self.id_for_native_font(font);
let mut ix_converter = StringIndexConverter::new(text);
let mut glyphs = Vec::new();
@@ -306,6 +381,7 @@ impl FontSystemState {
id: *glyph_id as GlyphId,
position: vec2f(position.x as f32, position.y as f32),
index: ix_converter.utf8_ix,
+ is_emoji: self.is_emoji(font_id),
});
}
@@ -510,7 +586,14 @@ mod tests {
for i in 0..VARIANTS {
let variant = i as f32 / VARIANTS as f32;
let (bounds, bytes) = fonts
- .rasterize_glyph(font_id, 16.0, glyph_id, vec2f(variant, variant), 2.)
+ .rasterize_glyph(
+ font_id,
+ 16.0,
+ glyph_id,
+ vec2f(variant, variant),
+ 2.,
+ RasterizationOptions::Alpha,
+ )
.unwrap();
let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
@@ -1,20 +1,39 @@
-use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
-
use super::atlas::{AllocId, AtlasAllocator};
use crate::{
+ fonts::{FontId, GlyphId},
geometry::{rect::RectI, vector::Vector2I},
- ImageData,
+ platform::RasterizationOptions,
+ scene::ImageGlyph,
+ FontSystem, ImageData,
};
-use std::{collections::HashMap, mem};
+use anyhow::anyhow;
+use metal::{MTLPixelFormat, TextureDescriptor, TextureRef};
+use ordered_float::OrderedFloat;
+use std::{collections::HashMap, mem, sync::Arc};
+
+#[derive(Hash, Eq, PartialEq)]
+struct GlyphDescriptor {
+ font_id: FontId,
+ font_size: OrderedFloat<f32>,
+ glyph_id: GlyphId,
+}
pub struct ImageCache {
prev_frame: HashMap<usize, (AllocId, RectI)>,
curr_frame: HashMap<usize, (AllocId, RectI)>,
+ image_glyphs: HashMap<GlyphDescriptor, Option<(AllocId, RectI, Vector2I)>>,
atlases: AtlasAllocator,
+ scale_factor: f32,
+ fonts: Arc<dyn FontSystem>,
}
impl ImageCache {
- pub fn new(device: metal::Device, size: Vector2I) -> Self {
+ pub fn new(
+ device: metal::Device,
+ size: Vector2I,
+ scale_factor: f32,
+ fonts: Arc<dyn FontSystem>,
+ ) -> Self {
let descriptor = TextureDescriptor::new();
descriptor.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
descriptor.set_width(size.x() as u64);
@@ -22,7 +41,21 @@ impl ImageCache {
Self {
prev_frame: Default::default(),
curr_frame: Default::default(),
+ image_glyphs: Default::default(),
atlases: AtlasAllocator::new(device, descriptor),
+ scale_factor,
+ fonts,
+ }
+ }
+
+ pub fn set_scale_factor(&mut self, scale_factor: f32) {
+ if scale_factor != self.scale_factor {
+ self.scale_factor = scale_factor;
+ for (_, glyph) in self.image_glyphs.drain() {
+ if let Some((alloc_id, _, _)) = glyph {
+ self.atlases.deallocate(alloc_id);
+ }
+ }
}
}
@@ -31,11 +64,44 @@ impl ImageCache {
.prev_frame
.remove(&image.id)
.or_else(|| self.curr_frame.get(&image.id).copied())
- .unwrap_or_else(|| self.atlases.upload(image.size(), image.as_bytes()));
+ .or_else(|| self.atlases.upload(image.size(), image.as_bytes()))
+ .ok_or_else(|| anyhow!("could not upload image of size {:?}", image.size()))
+ .unwrap();
self.curr_frame.insert(image.id, (alloc_id, atlas_bounds));
(alloc_id, atlas_bounds)
}
+ pub fn render_glyph(&mut self, image_glyph: &ImageGlyph) -> Option<(AllocId, RectI, Vector2I)> {
+ *self
+ .image_glyphs
+ .entry(GlyphDescriptor {
+ font_id: image_glyph.font_id,
+ font_size: OrderedFloat(image_glyph.font_size),
+ glyph_id: image_glyph.id,
+ })
+ .or_insert_with(|| {
+ let (glyph_bounds, bytes) = self.fonts.rasterize_glyph(
+ image_glyph.font_id,
+ image_glyph.font_size,
+ image_glyph.id,
+ Default::default(),
+ self.scale_factor,
+ RasterizationOptions::Bgra,
+ )?;
+ let (alloc_id, atlas_bounds) = self
+ .atlases
+ .upload(glyph_bounds.size(), &bytes)
+ .ok_or_else(|| {
+ anyhow!(
+ "could not upload image glyph of size {:?}",
+ glyph_bounds.size()
+ )
+ })
+ .unwrap();
+ Some((alloc_id, atlas_bounds, glyph_bounds.origin()))
+ })
+ }
+
pub fn finish_frame(&mut self) {
mem::swap(&mut self.prev_frame, &mut self.curr_frame);
for (_, (id, _)) in self.curr_frame.drain() {
@@ -3,7 +3,7 @@ use crate::{
executor,
keymap::Keystroke,
platform::{self, CursorStyle},
- AnyAction, ClipboardItem, Event, Menu, MenuItem,
+ Action, ClipboardItem, Event, Menu, MenuItem,
};
use anyhow::{anyhow, Result};
use block::ConcreteBlock;
@@ -38,8 +38,8 @@ use ptr::null_mut;
use std::{
cell::{Cell, RefCell},
convert::TryInto,
- ffi::{c_void, CStr},
- os::raw::c_char,
+ ffi::{c_void, CStr, OsStr},
+ os::{raw::c_char, unix::ffi::OsStrExt},
path::{Path, PathBuf},
ptr,
rc::Rc,
@@ -91,8 +91,8 @@ unsafe fn build_classes() {
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
- sel!(application:openFiles:),
- open_files as extern "C" fn(&mut Object, Sel, id, id),
+ sel!(application:openURLs:),
+ open_urls as extern "C" fn(&mut Object, Sel, id, id),
);
decl.register()
}
@@ -107,10 +107,10 @@ pub struct MacForegroundPlatformState {
resign_active: Option<Box<dyn FnMut()>>,
quit: Option<Box<dyn FnMut()>>,
event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
- menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
- open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
+ menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+ open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
finish_launching: Option<Box<dyn FnOnce() -> ()>>,
- menu_actions: Vec<Box<dyn AnyAction>>,
+ menu_actions: Vec<Box<dyn Action>>,
}
impl MacForegroundPlatform {
@@ -210,8 +210,8 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
self.0.borrow_mut().event = Some(callback);
}
- fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
- self.0.borrow_mut().open_files = Some(callback);
+ fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
+ self.0.borrow_mut().open_urls = Some(callback);
}
fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
@@ -235,7 +235,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
}
}
- fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
+ fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.0.borrow_mut().menu_command = Some(callback);
}
@@ -265,10 +265,9 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
for i in 0..urls.count() {
let url = urls.objectAtIndex(i);
if url.isFileURL() == YES {
- let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
- .to_string_lossy()
- .to_string();
- result.push(PathBuf::from(path));
+ if let Ok(path) = ns_url_to_path(url) {
+ result.push(path)
+ }
}
}
Some(result)
@@ -296,19 +295,13 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
let (done_tx, done_rx) = oneshot::channel();
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
- let result = if response == NSModalResponse::NSModalResponseOk {
+ let mut result = None;
+ if response == NSModalResponse::NSModalResponseOk {
let url = panel.URL();
if url.isFileURL() == YES {
- let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
- .to_string_lossy()
- .to_string();
- Some(PathBuf::from(path))
- } else {
- None
+ result = ns_url_to_path(panel.URL()).ok()
}
- } else {
- None
- };
+ }
if let Some(mut done_tx) = done_tx.take() {
let _ = postage::sink::Sink::try_send(&mut done_tx, result);
@@ -603,19 +596,18 @@ impl platform::Platform for MacPlatform {
}
}
- fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result<PathBuf> {
+ fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();
if bundle.is_null() {
Err(anyhow!("app is not running inside a bundle"))
} else {
- let name = name.map_or(nil, |name| ns_string(name));
- let extension = extension.map_or(nil, |extension| ns_string(extension));
- let path: id = msg_send![bundle, pathForResource: name ofType: extension];
- if path.is_null() {
- Err(anyhow!("resource could not be found"))
+ let name = ns_string(name);
+ let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
+ if url.is_null() {
+ Err(anyhow!("resource not found"))
} else {
- Ok(path_from_objc(path))
+ ns_url_to_path(url)
}
}
}
@@ -710,14 +702,14 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
}
}
-extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
- let paths = unsafe {
- (0..paths.count())
+extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
+ let urls = unsafe {
+ (0..urls.count())
.into_iter()
.filter_map(|i| {
- let path = paths.objectAtIndex(i);
- match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
- Ok(string) => Some(PathBuf::from(string)),
+ let path = urls.objectAtIndex(i);
+ match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() {
+ Ok(string) => Some(string.to_string()),
Err(err) => {
log::error!("error converting path to string: {}", err);
None
@@ -727,8 +719,8 @@ extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
.collect::<Vec<_>>()
};
let platform = unsafe { get_foreground_platform(this) };
- if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() {
- callback(paths);
+ if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() {
+ callback(urls);
}
}
@@ -751,6 +743,20 @@ unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}
+unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
+ let path: *mut c_char = msg_send![url, fileSystemRepresentation];
+ if path.is_null() {
+ Err(anyhow!(
+ "url is not a file path: {}",
+ CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
+ ))
+ } else {
+ Ok(PathBuf::from(OsStr::from_bytes(
+ CStr::from_ptr(path).to_bytes(),
+ )))
+ }
+}
+
mod security {
#![allow(non_upper_case_globals)]
use super::*;
@@ -6,9 +6,10 @@ use crate::{
vector::{vec2f, vec2i, Vector2F},
},
platform,
- scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow, Underline},
+ scene::{Glyph, Icon, Image, ImageGlyph, Layer, Quad, Scene, Shadow, Underline},
};
use cocoa::foundation::NSUInteger;
+use log::warn;
use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
use shaders::ToFloat2 as _;
use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
@@ -66,8 +67,13 @@ impl Renderer {
MTLResourceOptions::StorageModeManaged,
);
- let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
- let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768));
+ let sprite_cache = SpriteCache::new(
+ device.clone(),
+ vec2i(1024, 768),
+ scale_factor,
+ fonts.clone(),
+ );
+ let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
let path_atlases =
AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
let quad_pipeline_state = build_pipeline_state(
@@ -140,6 +146,9 @@ impl Renderer {
command_buffer: &metal::CommandBufferRef,
output: &metal::TextureRef,
) {
+ self.sprite_cache.set_scale_factor(scene.scale_factor());
+ self.image_cache.set_scale_factor(scene.scale_factor());
+
let mut offset = 0;
let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer);
@@ -172,7 +181,14 @@ impl Renderer {
for path in layer.paths() {
let origin = path.bounds.origin() * scene.scale_factor();
let size = (path.bounds.size() * scene.scale_factor()).ceil();
- let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32());
+
+ let path_allocation = self.path_atlases.allocate(size.to_i32());
+ if path_allocation.is_none() {
+ // Path size was likely zero.
+ warn!("could not allocate path texture of size {:?}", size);
+ continue;
+ }
+ let (alloc_id, atlas_origin) = path_allocation.unwrap();
let atlas_origin = atlas_origin.to_f32();
sprites.push(PathSprite {
layer_id,
@@ -351,6 +367,7 @@ impl Renderer {
);
self.render_images(
layer.images(),
+ layer.image_glyphs(),
scale_factor,
offset,
drawable_size,
@@ -533,8 +550,6 @@ impl Renderer {
return;
}
- self.sprite_cache.set_scale_factor(scale_factor);
-
let mut sprites_by_atlas = HashMap::new();
for glyph in glyphs {
@@ -569,6 +584,10 @@ impl Renderer {
let sprite =
self.sprite_cache
.render_icon(source_size, icon.path.clone(), icon.svg.clone());
+ if sprite.is_none() {
+ continue;
+ }
+ let sprite = sprite.unwrap();
sprites_by_atlas
.entry(sprite.atlas_id)
@@ -641,12 +660,13 @@ impl Renderer {
fn render_images(
&mut self,
images: &[Image],
+ image_glyphs: &[ImageGlyph],
scale_factor: f32,
offset: &mut usize,
drawable_size: Vector2F,
command_encoder: &metal::RenderCommandEncoderRef,
) {
- if images.is_empty() {
+ if images.is_empty() && image_glyphs.is_empty() {
return;
}
@@ -674,6 +694,31 @@ impl Renderer {
});
}
+ for image_glyph in image_glyphs {
+ let origin = (image_glyph.origin * scale_factor).floor();
+ if let Some((alloc_id, atlas_bounds, glyph_origin)) =
+ self.image_cache.render_glyph(image_glyph)
+ {
+ images_by_atlas
+ .entry(alloc_id.atlas_id)
+ .or_insert_with(Vec::new)
+ .push(shaders::GPUIImage {
+ origin: (origin + glyph_origin.to_f32()).to_float2(),
+ target_size: atlas_bounds.size().to_float2(),
+ source_size: atlas_bounds.size().to_float2(),
+ atlas_origin: atlas_bounds.origin().to_float2(),
+ border_top: 0.,
+ border_right: 0.,
+ border_bottom: 0.,
+ border_left: 0.,
+ border_color: Default::default(),
+ corner_radius: 0.,
+ });
+ } else {
+ log::warn!("could not render glyph with id {}", image_glyph.id);
+ }
+ }
+
command_encoder.set_render_pipeline_state(&self.image_pipeline_state);
command_encoder.set_vertex_buffer(
shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64,
@@ -2,8 +2,9 @@ use super::atlas::AtlasAllocator;
use crate::{
fonts::{FontId, GlyphId},
geometry::vector::{vec2f, Vector2F, Vector2I},
- platform,
+ platform::{self, RasterizationOptions},
};
+use collections::hash_map::Entry;
use metal::{MTLPixelFormat, TextureDescriptor};
use ordered_float::OrderedFloat;
use std::{borrow::Cow, collections::HashMap, sync::Arc};
@@ -112,9 +113,12 @@ impl SpriteCache {
glyph_id,
subpixel_shift,
scale_factor,
+ RasterizationOptions::Alpha,
)?;
- let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask);
+ let (alloc_id, atlas_bounds) = atlases
+ .upload(glyph_bounds.size(), &mask)
+ .expect("could not upload glyph");
Some(GlyphSprite {
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
@@ -130,31 +134,31 @@ impl SpriteCache {
size: Vector2I,
path: Cow<'static, str>,
svg: usvg::Tree,
- ) -> IconSprite {
+ ) -> Option<IconSprite> {
let atlases = &mut self.atlases;
- self.icons
- .entry(IconDescriptor {
- path,
- width: size.x(),
- height: size.y(),
- })
- .or_insert_with(|| {
- let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32).unwrap();
+ match self.icons.entry(IconDescriptor {
+ path,
+ width: size.x(),
+ height: size.y(),
+ }) {
+ Entry::Occupied(entry) => Some(entry.get().clone()),
+ Entry::Vacant(entry) => {
+ let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32)?;
resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
let mask = pixmap
.pixels()
.iter()
.map(|a| a.alpha())
.collect::<Vec<_>>();
-
- let (alloc_id, atlas_bounds) = atlases.upload(size, &mask);
- IconSprite {
+ let (alloc_id, atlas_bounds) = atlases.upload(size, &mask)?;
+ let icon_sprite = IconSprite {
atlas_id: alloc_id.atlas_id,
atlas_origin: atlas_bounds.origin(),
size,
- }
- })
- .clone()
+ };
+ Some(entry.insert(icon_sprite).clone())
+ }
+ }
}
pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
@@ -361,9 +361,10 @@ impl platform::Window for Window {
}
});
let block = block.copy();
+ let native_window = self.0.borrow().native_window;
let _: () = msg_send![
alert,
- beginSheetModalForWindow: self.0.borrow().native_window
+ beginSheetModalForWindow: native_window
completionHandler: block
];
done_rx
@@ -1,7 +1,7 @@
use super::{AppVersion, CursorStyle, WindowBounds};
use crate::{
geometry::vector::{vec2f, Vector2F},
- AnyAction, ClipboardItem,
+ Action, ClipboardItem,
};
use anyhow::{anyhow, Result};
use parking_lot::Mutex;
@@ -66,13 +66,13 @@ impl super::ForegroundPlatform for ForegroundPlatform {
fn on_event(&self, _: Box<dyn FnMut(crate::Event) -> bool>) {}
- fn on_open_files(&self, _: Box<dyn FnMut(Vec<std::path::PathBuf>)>) {}
+ fn on_open_urls(&self, _: Box<dyn FnMut(Vec<String>)>) {}
fn run(&self, _on_finish_launching: Box<dyn FnOnce() -> ()>) {
unimplemented!()
}
- fn on_menu_command(&self, _: Box<dyn FnMut(&dyn AnyAction)>) {}
+ fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
fn set_menus(&self, _: Vec<crate::Menu>) {}
@@ -161,7 +161,7 @@ impl super::Platform for Platform {
UtcOffset::UTC
}
- fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result<PathBuf> {
+ fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
Err(anyhow!("app not running inside a bundle"))
}
@@ -6,7 +6,7 @@ use crate::{
json::{self, ToJson},
platform::Event,
text_layout::TextLayoutCache,
- Action, AnyAction, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
+ Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene,
UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
};
@@ -51,15 +51,21 @@ impl Presenter {
}
pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
+ if let Some(view_id) = app.focused_view_id(self.window_id) {
+ self.dispatch_path_from(view_id)
+ } else {
+ Vec::new()
+ }
+ }
+
+ pub(crate) fn dispatch_path_from(&self, mut view_id: usize) -> Vec<usize> {
let mut path = Vec::new();
- if let Some(mut view_id) = app.focused_view_id(self.window_id) {
- path.push(view_id);
- while let Some(parent_id) = self.parents.get(&view_id).copied() {
- path.push(parent_id);
- view_id = parent_id;
- }
- path.reverse();
+ path.push(view_id);
+ while let Some(parent_id) = self.parents.get(&view_id).copied() {
+ path.push(parent_id);
+ view_id = parent_id;
}
+ path.reverse();
path
}
@@ -209,21 +215,24 @@ impl Presenter {
}
pub fn debug_elements(&self, cx: &AppContext) -> Option<json::Value> {
- cx.root_view_id(self.window_id)
- .and_then(|root_view_id| self.rendered_views.get(&root_view_id))
- .map(|root_element| {
- root_element.debug(&DebugContext {
- rendered_views: &self.rendered_views,
- font_cache: &self.font_cache,
- app: cx,
+ let view = cx.root_view(self.window_id)?;
+ Some(json!({
+ "root_view": view.debug_json(cx),
+ "root_element": self.rendered_views.get(&view.id())
+ .map(|root_element| {
+ root_element.debug(&DebugContext {
+ rendered_views: &self.rendered_views,
+ font_cache: &self.font_cache,
+ app: cx,
+ })
})
- })
+ }))
}
}
pub struct DispatchDirective {
pub path: Vec<usize>,
- pub action: Box<dyn AnyAction>,
+ pub action: Box<dyn Action>,
}
pub struct LayoutContext<'a> {
@@ -535,6 +544,7 @@ impl Element for ChildView {
&mut self,
event: &Event,
_: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
@@ -553,6 +563,7 @@ impl Element for ChildView {
"type": "ChildView",
"view_id": self.view.id(),
"bounds": bounds.to_json(),
+ "view": self.view.debug_json(cx.app),
"child": if let Some(view) = cx.rendered_views.get(&self.view.id()) {
view.debug(cx)
} else {
@@ -29,6 +29,7 @@ pub struct Layer {
images: Vec<Image>,
shadows: Vec<Shadow>,
glyphs: Vec<Glyph>,
+ image_glyphs: Vec<ImageGlyph>,
icons: Vec<Icon>,
paths: Vec<Path>,
}
@@ -58,6 +59,14 @@ pub struct Glyph {
pub color: Color,
}
+#[derive(Debug)]
+pub struct ImageGlyph {
+ pub font_id: FontId,
+ pub font_size: f32,
+ pub id: GlyphId,
+ pub origin: Vector2F,
+}
+
pub struct Icon {
pub bounds: RectF,
pub svg: usvg::Tree,
@@ -204,6 +213,10 @@ impl Scene {
self.active_layer().push_glyph(glyph)
}
+ pub fn push_image_glyph(&mut self, image_glyph: ImageGlyph) {
+ self.active_layer().push_image_glyph(image_glyph)
+ }
+
pub fn push_icon(&mut self, icon: Icon) {
self.active_layer().push_icon(icon)
}
@@ -264,13 +277,14 @@ impl Layer {
pub fn new(clip_bounds: Option<RectF>) -> Self {
Self {
clip_bounds,
- quads: Vec::new(),
- underlines: Vec::new(),
- images: Vec::new(),
- shadows: Vec::new(),
- glyphs: Vec::new(),
- icons: Vec::new(),
- paths: Vec::new(),
+ quads: Default::default(),
+ underlines: Default::default(),
+ images: Default::default(),
+ shadows: Default::default(),
+ image_glyphs: Default::default(),
+ glyphs: Default::default(),
+ icons: Default::default(),
+ paths: Default::default(),
}
}
@@ -318,6 +332,14 @@ impl Layer {
self.shadows.as_slice()
}
+ fn push_image_glyph(&mut self, glyph: ImageGlyph) {
+ self.image_glyphs.push(glyph);
+ }
+
+ pub fn image_glyphs(&self) -> &[ImageGlyph] {
+ self.image_glyphs.as_slice()
+ }
+
fn push_glyph(&mut self, glyph: Glyph) {
self.glyphs.push(glyph);
}
@@ -191,6 +191,7 @@ pub struct Glyph {
pub id: GlyphId,
pub position: Vector2F,
pub index: usize,
+ pub is_emoji: bool,
}
impl Line {
@@ -323,13 +324,22 @@ impl Line {
});
}
- cx.scene.push_glyph(scene::Glyph {
- font_id: run.font_id,
- font_size: self.layout.font_size,
- id: glyph.id,
- origin: glyph_origin,
- color,
- });
+ if glyph.is_emoji {
+ cx.scene.push_image_glyph(scene::ImageGlyph {
+ font_id: run.font_id,
+ font_size: self.layout.font_size,
+ id: glyph.id,
+ origin: glyph_origin,
+ });
+ } else {
+ cx.scene.push_glyph(scene::Glyph {
+ font_id: run.font_id,
+ font_size: self.layout.font_size,
+ id: glyph.id,
+ origin: glyph_origin,
+ color,
+ });
+ }
}
}
@@ -389,13 +399,22 @@ impl Line {
.bounding_box(run.font_id, self.layout.font_size),
);
if glyph_bounds.intersects(visible_bounds) {
- cx.scene.push_glyph(scene::Glyph {
- font_id: run.font_id,
- font_size: self.layout.font_size,
- id: glyph.id,
- origin: glyph_bounds.origin() + baseline_origin,
- color,
- });
+ if glyph.is_emoji {
+ cx.scene.push_image_glyph(scene::ImageGlyph {
+ font_id: run.font_id,
+ font_size: self.layout.font_size,
+ id: glyph.id,
+ origin: glyph_bounds.origin() + baseline_origin,
+ });
+ } else {
+ cx.scene.push_glyph(scene::Glyph {
+ font_id: run.font_id,
+ font_size: self.layout.font_size,
+ id: glyph.id,
+ origin: glyph_bounds.origin() + baseline_origin,
+ color,
+ });
+ }
}
}
}
@@ -1,6 +1,8 @@
+use serde::Deserialize;
+
use crate::{
- action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext,
- WeakViewHandle,
+ actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View,
+ ViewContext, WeakViewHandle,
};
pub struct Select {
@@ -25,8 +27,11 @@ pub enum ItemType {
Unselected,
}
-action!(ToggleSelect);
-action!(SelectItem, usize);
+#[derive(Clone, Deserialize)]
+pub struct SelectItem(pub usize);
+
+actions!(select, [ToggleSelect]);
+impl_actions!(select, [SelectItem]);
pub enum Event {}
@@ -75,68 +75,65 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
match last_segment.map(|s| s.ident.to_string()).as_deref() {
Some("StdRng") => {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
+ continue;
}
Some("bool") => {
inner_fn_args.extend(quote!(is_last_iteration,));
+ continue;
}
- _ => {
- return TokenStream::from(
- syn::Error::new_spanned(arg, "invalid argument")
- .into_compile_error(),
- )
- }
- }
- } else if let Type::Reference(ty) = &*arg.ty {
- match &*ty.elem {
- Type::Path(ty) => {
- let last_segment = ty.path.segments.last();
- match last_segment.map(|s| s.ident.to_string()).as_deref() {
- Some("TestAppContext") => {
- let first_entity_id = ix * 100_000;
- let cx_varname = format_ident!("cx_{}", ix);
- cx_vars.extend(quote!(
- let mut #cx_varname = #namespace::TestAppContext::new(
- foreground_platform.clone(),
- cx.platform().clone(),
- deterministic.build_foreground(#ix),
- deterministic.build_background(),
- cx.font_cache().clone(),
- cx.leak_detector(),
- #first_entity_id,
- );
- ));
- cx_teardowns.extend(quote!(
- #cx_varname.update(|cx| cx.remove_all_windows());
- deterministic.run_until_parked();
- #cx_varname.update(|_| {}); // flush effects
- ));
- inner_fn_args.extend(quote!(&mut #cx_varname,));
- }
- _ => {
- return TokenStream::from(
- syn::Error::new_spanned(arg, "invalid argument")
- .into_compile_error(),
- )
+ Some("Arc") => {
+ if let syn::PathArguments::AngleBracketed(args) =
+ &last_segment.unwrap().arguments
+ {
+ if let Some(syn::GenericArgument::Type(syn::Type::Path(ty))) =
+ args.args.last()
+ {
+ let last_segment = ty.path.segments.last();
+ if let Some("Deterministic") =
+ last_segment.map(|s| s.ident.to_string()).as_deref()
+ {
+ inner_fn_args.extend(quote!(deterministic.clone(),));
+ continue;
+ }
}
}
}
- _ => {
- return TokenStream::from(
- syn::Error::new_spanned(arg, "invalid argument")
- .into_compile_error(),
- )
+ _ => {}
+ }
+ } else if let Type::Reference(ty) = &*arg.ty {
+ if let Type::Path(ty) = &*ty.elem {
+ let last_segment = ty.path.segments.last();
+ if let Some("TestAppContext") =
+ last_segment.map(|s| s.ident.to_string()).as_deref()
+ {
+ let first_entity_id = ix * 100_000;
+ let cx_varname = format_ident!("cx_{}", ix);
+ cx_vars.extend(quote!(
+ let mut #cx_varname = #namespace::TestAppContext::new(
+ foreground_platform.clone(),
+ cx.platform().clone(),
+ deterministic.build_foreground(#ix),
+ deterministic.build_background(),
+ cx.font_cache().clone(),
+ cx.leak_detector(),
+ #first_entity_id,
+ );
+ ));
+ cx_teardowns.extend(quote!(
+ #cx_varname.update(|cx| cx.remove_all_windows());
+ deterministic.run_until_parked();
+ #cx_varname.update(|_| {}); // flush effects
+ ));
+ inner_fn_args.extend(quote!(&mut #cx_varname,));
+ continue;
}
}
- } else {
- return TokenStream::from(
- syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
- );
}
- } else {
- return TokenStream::from(
- syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
- );
}
+
+ return TokenStream::from(
+ syn::Error::new_spanned(arg, "invalid argument").into_compile_error(),
+ );
}
parse_quote! {
@@ -14,4 +14,4 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
chrono = "0.4"
dirs = "4.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
@@ -1,14 +1,13 @@
use chrono::{Datelike, Local, Timelike};
use editor::{Autoscroll, Editor};
-use gpui::{action, keymap::Binding, MutableAppContext};
+use gpui::{actions, MutableAppContext};
use std::{fs::OpenOptions, sync::Arc};
use util::TryFutureExt as _;
use workspace::AppState;
-action!(NewJournalEntry);
+actions!(journal, [NewJournalEntry]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
- cx.add_bindings(vec![Binding::new("ctrl-alt-cmd-j", NewJournalEntry, None)]);
cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
}
@@ -44,7 +43,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.spawn(|mut cx| {
async move {
let (journal_dir, entry_path) = create_entry.await?;
- let workspace = cx
+ let (workspace, _) = cx
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
.await;
@@ -35,7 +35,7 @@ async-broadcast = "0.3.4"
async-trait = "0.1"
futures = "0.3"
lazy_static = "1.4"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
@@ -57,5 +57,6 @@ util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.8"
rand = "0.8.3"
+tree-sitter-json = "0.19.0"
tree-sitter-rust = "0.20.0"
unindent = "0.1.7"
@@ -66,7 +66,6 @@ pub struct Buffer {
file_update_count: usize,
completion_triggers: Vec<String>,
deferred_ops: OperationQueue<Operation>,
- indent_size: u32,
}
pub struct BufferSnapshot {
@@ -80,7 +79,6 @@ pub struct BufferSnapshot {
selections_update_count: usize,
language: Option<Arc<Language>>,
parse_count: usize,
- indent_size: u32,
}
#[derive(Clone, Debug)]
@@ -214,6 +212,7 @@ struct AutoindentRequest {
before_edit: BufferSnapshot,
edited: Vec<Anchor>,
inserted: Option<Vec<Range<Anchor>>>,
+ indent_size: u32,
}
#[derive(Debug)]
@@ -427,8 +426,6 @@ impl Buffer {
file_update_count: 0,
completion_triggers: Default::default(),
deferred_ops: OperationQueue::new(),
- // TODO: make this configurable
- indent_size: 4,
}
}
@@ -444,7 +441,6 @@ impl Buffer {
language: self.language.clone(),
parse_count: self.parse_count,
selections_update_count: self.selections_update_count,
- indent_size: self.indent_size,
}
}
@@ -486,6 +482,7 @@ impl Buffer {
}
pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
+ *self.syntax_tree.lock() = None;
self.language = language;
self.reparse(cx);
}
@@ -785,7 +782,7 @@ impl Buffer {
.indent_column_for_line(suggestion.basis_row)
});
let delta = if suggestion.indent {
- snapshot.indent_size
+ request.indent_size
} else {
0
};
@@ -808,7 +805,7 @@ impl Buffer {
.flatten();
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
let delta = if suggestion.indent {
- snapshot.indent_size
+ request.indent_size
} else {
0
};
@@ -844,7 +841,7 @@ impl Buffer {
.flatten();
for (row, suggestion) in inserted_row_range.zip(suggestions) {
let delta = if suggestion.indent {
- snapshot.indent_size
+ request.indent_size
} else {
0
};
@@ -1054,7 +1051,7 @@ impl Buffer {
where
T: Into<String>,
{
- self.edit_internal([0..self.len()], text, false, cx)
+ self.edit_internal([0..self.len()], text, None, cx)
}
pub fn edit<I, S, T>(
@@ -1068,13 +1065,14 @@ impl Buffer {
S: ToOffset,
T: Into<String>,
{
- self.edit_internal(ranges_iter, new_text, false, cx)
+ self.edit_internal(ranges_iter, new_text, None, cx)
}
pub fn edit_with_autoindent<I, S, T>(
&mut self,
ranges_iter: I,
new_text: T,
+ indent_size: u32,
cx: &mut ModelContext<Self>,
) -> Option<clock::Local>
where
@@ -1082,14 +1080,14 @@ impl Buffer {
S: ToOffset,
T: Into<String>,
{
- self.edit_internal(ranges_iter, new_text, true, cx)
+ self.edit_internal(ranges_iter, new_text, Some(indent_size), cx)
}
pub fn edit_internal<I, S, T>(
&mut self,
ranges_iter: I,
new_text: T,
- autoindent: bool,
+ autoindent_size: Option<u32>,
cx: &mut ModelContext<Self>,
) -> Option<clock::Local>
where
@@ -1121,23 +1119,27 @@ impl Buffer {
self.start_transaction();
self.pending_autoindent.take();
- let autoindent_request = if autoindent && self.language.is_some() {
- let before_edit = self.snapshot();
- let edited = ranges
- .iter()
- .filter_map(|range| {
- let start = range.start.to_point(self);
- if new_text.starts_with('\n') && start.column == self.line_len(start.row) {
- None
- } else {
- Some(self.anchor_before(range.start))
- }
- })
- .collect();
- Some((before_edit, edited))
- } else {
- None
- };
+ let autoindent_request =
+ self.language
+ .as_ref()
+ .and_then(|_| autoindent_size)
+ .map(|autoindent_size| {
+ let before_edit = self.snapshot();
+ let edited = ranges
+ .iter()
+ .filter_map(|range| {
+ let start = range.start.to_point(self);
+ if new_text.starts_with('\n')
+ && start.column == self.line_len(start.row)
+ {
+ None
+ } else {
+ Some(self.anchor_before(range.start))
+ }
+ })
+ .collect();
+ (before_edit, edited, autoindent_size)
+ });
let first_newline_ix = new_text.find('\n');
let new_text_len = new_text.len();
@@ -1145,7 +1147,7 @@ impl Buffer {
let edit = self.text.edit(ranges.iter().cloned(), new_text);
let edit_id = edit.local_timestamp();
- if let Some((before_edit, edited)) = autoindent_request {
+ if let Some((before_edit, edited, size)) = autoindent_request {
let mut inserted = None;
if let Some(first_newline_ix) = first_newline_ix {
let mut delta = 0isize;
@@ -1168,6 +1170,7 @@ impl Buffer {
before_edit,
edited,
inserted,
+ indent_size: size,
}));
}
@@ -1924,10 +1927,6 @@ impl BufferSnapshot {
pub fn file_update_count(&self) -> usize {
self.file_update_count
}
-
- pub fn indent_size(&self) -> u32 {
- self.indent_size
- }
}
impl Clone for BufferSnapshot {
@@ -1943,7 +1942,6 @@ impl Clone for BufferSnapshot {
file_update_count: self.file_update_count,
language: self.language.clone(),
parse_count: self.parse_count,
- indent_size: self.indent_size,
}
}
}
@@ -234,6 +234,14 @@ impl LanguageRegistry {
.cloned()
}
+ pub fn language_names(&self) -> Vec<String> {
+ self.languages
+ .read()
+ .iter()
+ .map(|language| language.name().to_string())
+ .collect()
+ }
+
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<Arc<Language>> {
let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str());
@@ -276,12 +276,32 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
"arguments: (arguments (identifier)))))))",
)
);
+}
- fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
- buffer.read_with(cx, |buffer, _| {
- buffer.syntax_tree().unwrap().root_node().to_sexp()
- })
- }
+#[gpui::test]
+async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
+ let buffer = cx.add_model(|cx| {
+ let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx);
+ buffer.set_sync_parse_timeout(Duration::ZERO);
+ buffer
+ });
+
+ // Wait for the initial text to parse
+ buffer
+ .condition(&cx, |buffer, _| !buffer.is_parsing())
+ .await;
+ assert_eq!(
+ get_tree_sexp(&buffer, &cx),
+ "(source_file (expression_statement (block)))"
+ );
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.set_language(Some(Arc::new(json_lang())), cx)
+ });
+ buffer
+ .condition(&cx, |buffer, _| !buffer.is_parsing())
+ .await;
+ assert_eq!(get_tree_sexp(&buffer, &cx), "(document (object))");
}
#[gpui::test]
@@ -556,13 +576,13 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
let text = "fn a() {}";
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
- buffer.edit_with_autoindent([8..8], "\n\n", cx);
+ buffer.edit_with_autoindent([8..8], "\n\n", 4, cx);
assert_eq!(buffer.text(), "fn a() {\n \n}");
- buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
+ buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", 4, cx);
assert_eq!(buffer.text(), "fn a() {\n b()\n \n}");
- buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
+ buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", 4, cx);
assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}");
buffer
@@ -584,7 +604,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
// their indentation is not adjusted.
- buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
+ buffer.edit_with_autoindent(
+ [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
+ "()",
+ 4,
+ cx,
+ );
assert_eq!(
buffer.text(),
"
@@ -601,6 +626,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
buffer.edit_with_autoindent(
[empty(Point::new(1, 1)), empty(Point::new(2, 1))],
"\n.f\n.g",
+ 4,
cx,
);
assert_eq!(
@@ -631,7 +657,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
- buffer.edit_with_autoindent([5..5], "\nb", cx);
+ buffer.edit_with_autoindent([5..5], "\nb", 4, cx);
assert_eq!(
buffer.text(),
"
@@ -643,7 +669,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
// The indentation suggestion changed because `@end` node (a close paren)
// is now at the beginning of the line.
- buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
+ buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", 4, cx);
assert_eq!(
buffer.text(),
"
@@ -795,7 +821,10 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
}
50..=59 if replica_ids.len() < max_peers => {
let old_buffer = buffer.read(cx).to_proto();
- let new_replica_id = replica_ids.len() as ReplicaId;
+ let new_replica_id = (0..=replica_ids.len() as ReplicaId)
+ .filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
+ .choose(&mut rng)
+ .unwrap();
log::info!(
"Adding new replica {} (replicating from {})",
new_replica_id,
@@ -804,6 +833,11 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
new_buffer = Some(cx.add_model(|cx| {
let mut new_buffer =
Buffer::from_proto(new_replica_id, old_buffer, None, cx).unwrap();
+ log::info!(
+ "New replica {} text: {:?}",
+ new_buffer.replica_id(),
+ new_buffer.text()
+ );
new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
@@ -817,8 +851,33 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
.detach();
new_buffer
}));
- replica_ids.push(new_replica_id);
network.borrow_mut().replicate(replica_id, new_replica_id);
+
+ if new_replica_id as usize == replica_ids.len() {
+ replica_ids.push(new_replica_id);
+ } else {
+ let new_buffer = new_buffer.take().unwrap();
+ while network.borrow().has_unreceived(new_replica_id) {
+ let ops = network
+ .borrow_mut()
+ .receive(new_replica_id)
+ .into_iter()
+ .map(|op| proto::deserialize_operation(op).unwrap());
+ if ops.len() > 0 {
+ log::info!(
+ "peer {} (version: {:?}) applying {} ops from the network. {:?}",
+ new_replica_id,
+ buffer.read(cx).version(),
+ ops.len(),
+ ops
+ );
+ new_buffer.update(cx, |new_buffer, cx| {
+ new_buffer.apply_ops(ops, cx).unwrap();
+ });
+ }
+ }
+ buffers[new_replica_id as usize] = new_buffer;
+ }
}
60..=69 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
@@ -835,9 +894,11 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
.map(|op| proto::deserialize_operation(op).unwrap());
if ops.len() > 0 {
log::info!(
- "peer {} applying {} ops from the network.",
+ "peer {} (version: {:?}) applying {} ops from the network. {:?}",
replica_id,
- ops.len()
+ buffer.read(cx).version(),
+ ops.len(),
+ ops
);
buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx).unwrap());
}
@@ -860,6 +921,12 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
let first_buffer = buffers[0].read(cx).snapshot();
for buffer in &buffers[1..] {
let buffer = buffer.read(cx).snapshot();
+ assert_eq!(
+ buffer.version(),
+ first_buffer.version(),
+ "Replica {} version != Replica 0 version",
+ buffer.replica_id()
+ );
assert_eq!(
buffer.text(),
first_buffer.text(),
@@ -889,7 +956,12 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
.filter(|(replica_id, _)| **replica_id != buffer.replica_id())
.map(|(replica_id, selections)| (*replica_id, selections.iter().collect::<Vec<_>>()))
.collect::<Vec<_>>();
- assert_eq!(actual_remote_selections, expected_remote_selections);
+ assert_eq!(
+ actual_remote_selections,
+ expected_remote_selections,
+ "Replica {} remote selections != expected selections",
+ buffer.replica_id()
+ );
}
}
@@ -978,6 +1050,23 @@ fn rust_lang() -> Language {
.unwrap()
}
+fn json_lang() -> Language {
+ Language::new(
+ LanguageConfig {
+ name: "Json".into(),
+ path_suffixes: vec!["js".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_json::language()),
+ )
+}
+
+fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
+ buffer.read_with(cx, |buffer, _| {
+ buffer.syntax_tree().unwrap().root_node().to_sexp()
+ })
+}
+
fn empty(point: Point) -> Range<Point> {
point..point
}
@@ -17,7 +17,7 @@ util = { path = "../util" }
anyhow = "1.0"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp-types = "0.91"
parking_lot = "0.11"
postage = { version = "0.4.1", features = ["futures-traits"] }
@@ -201,6 +201,9 @@ impl LanguageServer {
std::str::from_utf8(&buffer)?
));
}
+
+ // Don't starve the main thread when receiving lots of messages at once.
+ smol::future::yield_now().await;
}
}
.log_err()
@@ -12,6 +12,8 @@ editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
language = { path = "../language" }
+picker = { path = "../picker" }
+settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
ordered-float = "2.1.1"
@@ -4,45 +4,31 @@ use editor::{
};
use fuzzy::StringMatch;
use gpui::{
- action,
- elements::*,
- geometry::vector::Vector2F,
- keymap::{self, Binding},
- AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
- WeakViewHandle,
+ actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext,
+ RenderContext, Task, View, ViewContext, ViewHandle,
};
use language::Outline;
use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
use std::cmp::{self, Reverse};
-use workspace::{
- menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
- Settings, Workspace,
-};
+use workspace::Workspace;
-action!(Toggle);
+actions!(outline, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings([
- Binding::new("cmd-shift-O", Toggle, Some("Editor")),
- Binding::new("escape", Toggle, Some("OutlineView")),
- ]);
cx.add_action(OutlineView::toggle);
- cx.add_action(OutlineView::confirm);
- cx.add_action(OutlineView::select_prev);
- cx.add_action(OutlineView::select_next);
- cx.add_action(OutlineView::select_first);
- cx.add_action(OutlineView::select_last);
+ Picker::<OutlineView>::init(cx);
}
struct OutlineView {
- handle: WeakViewHandle<Self>,
+ picker: ViewHandle<Picker<Self>>,
active_editor: ViewHandle<Editor>,
outline: Outline<Anchor>,
selected_match_index: usize,
prev_scroll_position: Option<Vector2F>,
matches: Vec<StringMatch>,
- query_editor: ViewHandle<Editor>,
- list_state: UniformListState,
+ last_query: String,
}
pub enum Event {
@@ -62,38 +48,12 @@ impl View for OutlineView {
"OutlineView"
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
- let mut cx = Self::default_keymap_context();
- cx.set.insert("menu".into());
- cx
- }
-
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = cx.global::<Settings>();
-
- Flex::new(Axis::Vertical)
- .with_child(
- Container::new(ChildView::new(&self.query_editor).boxed())
- .with_style(settings.theme.selector.input_editor.container)
- .boxed(),
- )
- .with_child(
- FlexItem::new(self.render_matches(cx))
- .flex(1.0, false)
- .boxed(),
- )
- .contained()
- .with_style(settings.theme.selector.container)
- .constrained()
- .with_max_width(800.0)
- .with_max_height(1200.0)
- .aligned()
- .top()
- .named("outline view")
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ ChildView::new(self.picker.clone()).boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.query_editor);
+ cx.focus(&self.picker);
}
}
@@ -103,24 +63,16 @@ impl OutlineView {
editor: ViewHandle<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
- let query_editor = cx.add_view(|cx| {
- Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
- });
- cx.subscribe(&query_editor, Self::on_query_editor_event)
- .detach();
-
- let mut this = Self {
- handle: cx.weak_handle(),
+ let handle = cx.weak_handle();
+ Self {
+ picker: cx.add_view(|cx| Picker::new(handle, cx).with_max_size(800., 1200.)),
+ last_query: Default::default(),
matches: Default::default(),
selected_match_index: 0,
prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
active_editor: editor,
outline,
- query_editor,
- list_state: Default::default(),
- };
- this.update_matches(cx);
- this
+ }
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
@@ -144,34 +96,18 @@ impl OutlineView {
}
}
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- if self.selected_match_index > 0 {
- self.select(self.selected_match_index - 1, true, false, cx);
- }
- }
-
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- if self.selected_match_index + 1 < self.matches.len() {
- self.select(self.selected_match_index + 1, true, false, cx);
- }
- }
-
- fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
- self.select(0, true, false, cx);
- }
-
- fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
- self.select(self.matches.len().saturating_sub(1), true, false, cx);
+ fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
+ self.active_editor.update(cx, |editor, cx| {
+ editor.highlight_rows(None);
+ if let Some(scroll_position) = self.prev_scroll_position {
+ editor.set_scroll_position(scroll_position, cx);
+ }
+ })
}
- fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
- self.selected_match_index = index;
- self.list_state.scroll_to(if center {
- ScrollTarget::Center(index)
- } else {
- ScrollTarget::Show(index)
- });
- if navigate {
+ fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<Self>) {
+ self.selected_match_index = ix;
+ if navigate && !self.matches.is_empty() {
let selected_match = &self.matches[self.selected_match_index];
let outline_item = &self.outline.items[selected_match.candidate_id];
self.active_editor.update(cx, |active_editor, cx| {
@@ -188,27 +124,6 @@ impl OutlineView {
cx.notify();
}
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- self.prev_scroll_position.take();
- self.active_editor.update(cx, |active_editor, cx| {
- if let Some(rows) = active_editor.highlighted_rows() {
- let snapshot = active_editor.snapshot(cx).display_snapshot;
- let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
- active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
- }
- });
- cx.emit(Event::Dismissed);
- }
-
- fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
- self.active_editor.update(cx, |editor, cx| {
- editor.highlight_rows(None);
- if let Some(scroll_position) = self.prev_scroll_position {
- editor.set_scroll_position(scroll_position, cx);
- }
- })
- }
-
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
@@ -219,24 +134,27 @@ impl OutlineView {
Event::Dismissed => workspace.dismiss_modal(cx),
}
}
+}
- fn on_query_editor_event(
- &mut self,
- _: ViewHandle<Editor>,
- event: &editor::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- editor::Event::Blurred => cx.emit(Event::Dismissed),
- editor::Event::BufferEdited { .. } => self.update_matches(cx),
- _ => {}
- }
+impl PickerDelegate for OutlineView {
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_match_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+ self.set_selected_index(ix, true, cx);
+ }
+
+ fn center_selection_after_match_updates(&self) -> bool {
+ true
}
- fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
let selected_index;
- let navigate_to_selected_index;
- let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
if query.is_empty() {
self.restore_active_editor(cx);
self.matches = self
@@ -276,9 +194,8 @@ impl OutlineView {
(ix, depth, distance_to_closest_endpoint)
})
.max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
- .unwrap()
- .0;
- navigate_to_selected_index = false;
+ .map(|(ix, _, _)| ix)
+ .unwrap_or(0);
} else {
self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
selected_index = self
@@ -288,57 +205,33 @@ impl OutlineView {
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
- navigate_to_selected_index = !self.matches.is_empty();
}
- self.select(selected_index, navigate_to_selected_index, true, cx);
+ self.last_query = query;
+ self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
+ Task::ready(())
}
- fn render_matches(&self, cx: &AppContext) -> ElementBox {
- if self.matches.is_empty() {
- let settings = cx.global::<Settings>();
- return Container::new(
- Label::new(
- "No matches".into(),
- settings.theme.selector.empty.label.clone(),
- )
- .boxed(),
- )
- .with_style(settings.theme.selector.empty.container)
- .named("empty matches");
- }
-
- let handle = self.handle.clone();
- let list = UniformList::new(
- self.list_state.clone(),
- self.matches.len(),
- move |mut range, items, cx| {
- let cx = cx.as_ref();
- let view = handle.upgrade(cx).unwrap();
- let view = view.read(cx);
- let start = range.start;
- range.end = cmp::min(range.end, view.matches.len());
- items.extend(
- view.matches[range]
- .iter()
- .enumerate()
- .map(move |(ix, m)| view.render_match(m, start + ix, cx)),
- );
- },
- );
+ fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+ self.prev_scroll_position.take();
+ self.active_editor.update(cx, |active_editor, cx| {
+ if let Some(rows) = active_editor.highlighted_rows() {
+ let snapshot = active_editor.snapshot(cx).display_snapshot;
+ let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+ active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
+ }
+ });
+ cx.emit(Event::Dismissed);
+ }
- Container::new(list.boxed())
- .with_margin_top(6.0)
- .named("matches")
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ self.restore_active_editor(cx);
+ cx.emit(Event::Dismissed);
}
- fn render_match(
- &self,
- string_match: &StringMatch,
- index: usize,
- cx: &AppContext,
- ) -> ElementBox {
+ fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
let settings = cx.global::<Settings>();
- let style = if index == self.selected_match_index {
+ let string_match = &self.matches[ix];
+ let style = if selected {
&settings.theme.selector.active_item
} else {
&settings.theme.selector.item
@@ -0,0 +1,23 @@
+[package]
+name = "picker"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/picker.rs"
+doctest = false
+
+[dependencies]
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"
@@ -0,0 +1,277 @@
+use editor::Editor;
+use gpui::{
+ elements::{
+ ChildView, EventHandler, Flex, Label, ParentElement, ScrollTarget, UniformList,
+ UniformListState,
+ },
+ geometry::vector::{vec2f, Vector2F},
+ keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task,
+ View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use settings::Settings;
+use std::cmp;
+use workspace::menu::{
+ Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev,
+};
+
+pub struct Picker<D: PickerDelegate> {
+ delegate: WeakViewHandle<D>,
+ query_editor: ViewHandle<Editor>,
+ list_state: UniformListState,
+ max_size: Vector2F,
+ confirmed: bool,
+}
+
+pub trait PickerDelegate: View {
+ fn match_count(&self) -> usize;
+ fn selected_index(&self) -> usize;
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>);
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()>;
+ fn confirm(&mut self, cx: &mut ViewContext<Self>);
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>);
+ fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox;
+ fn center_selection_after_match_updates(&self) -> bool {
+ false
+ }
+}
+
+impl<D: PickerDelegate> Entity for Picker<D> {
+ type Event = ();
+}
+
+impl<D: PickerDelegate> View for Picker<D> {
+ fn ui_name() -> &'static str {
+ "Picker"
+ }
+
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
+ let settings = cx.global::<Settings>();
+ let delegate = self.delegate.clone();
+ let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
+ delegate.read(cx).match_count()
+ } else {
+ 0
+ };
+
+ Flex::new(Axis::Vertical)
+ .with_child(
+ ChildView::new(&self.query_editor)
+ .contained()
+ .with_style(settings.theme.selector.input_editor.container)
+ .boxed(),
+ )
+ .with_child(
+ if match_count == 0 {
+ Label::new(
+ "No matches".into(),
+ settings.theme.selector.empty.label.clone(),
+ )
+ .contained()
+ .with_style(settings.theme.selector.empty.container)
+ } else {
+ UniformList::new(
+ self.list_state.clone(),
+ match_count,
+ move |mut range, items, cx| {
+ let cx = cx.as_ref();
+ let delegate = delegate.upgrade(cx).unwrap();
+ let delegate = delegate.read(cx);
+ let selected_ix = delegate.selected_index();
+ range.end = cmp::min(range.end, delegate.match_count());
+ items.extend(range.map(move |ix| {
+ EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx))
+ .on_mouse_down(move |cx| {
+ cx.dispatch_action(SelectIndex(ix));
+ true
+ })
+ .boxed()
+ }));
+ },
+ )
+ .contained()
+ .with_margin_top(6.0)
+ }
+ .flex(1., false)
+ .boxed(),
+ )
+ .contained()
+ .with_style(settings.theme.selector.container)
+ .constrained()
+ .with_max_width(self.max_size.x())
+ .with_max_height(self.max_size.y())
+ .aligned()
+ .top()
+ .named("picker")
+ }
+
+ fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ let mut cx = Self::default_keymap_context();
+ cx.set.insert("menu".into());
+ cx
+ }
+
+ fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+ cx.focus(&self.query_editor);
+ }
+}
+
+impl<D: PickerDelegate> Picker<D> {
+ pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(Self::select_first);
+ cx.add_action(Self::select_last);
+ cx.add_action(Self::select_next);
+ cx.add_action(Self::select_prev);
+ cx.add_action(Self::select_index);
+ cx.add_action(Self::confirm);
+ cx.add_action(Self::cancel);
+ }
+
+ pub fn new(delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self {
+ let query_editor = cx.add_view(|cx| {
+ Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
+ });
+ cx.subscribe(&query_editor, Self::on_query_editor_event)
+ .detach();
+ let this = Self {
+ query_editor,
+ list_state: Default::default(),
+ delegate,
+ max_size: vec2f(540., 420.),
+ confirmed: false,
+ };
+ cx.defer(|this, cx| {
+ if let Some(delegate) = this.delegate.upgrade(cx) {
+ cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
+ this.update_matches(String::new(), cx)
+ }
+ });
+ this
+ }
+
+ pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
+ self.max_size = vec2f(width, height);
+ self
+ }
+
+ pub fn query(&self, cx: &AppContext) -> String {
+ self.query_editor.read(cx).text(cx)
+ }
+
+ fn on_query_editor_event(
+ &mut self,
+ _: ViewHandle<Editor>,
+ event: &editor::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
+ editor::Event::Blurred if !self.confirmed => {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ delegate.update(cx, |delegate, cx| {
+ delegate.dismiss(cx);
+ })
+ }
+ }
+ _ => {}
+ }
+ }
+
+ pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ let update = delegate.update(cx, |d, cx| d.update_matches(query, cx));
+ cx.spawn(|this, mut cx| async move {
+ update.await;
+ this.update(&mut cx, |this, cx| {
+ if let Some(delegate) = this.delegate.upgrade(cx) {
+ let delegate = delegate.read(cx);
+ let index = delegate.selected_index();
+ let target = if delegate.center_selection_after_match_updates() {
+ ScrollTarget::Center(index)
+ } else {
+ ScrollTarget::Show(index)
+ };
+ this.list_state.scroll_to(target);
+ cx.notify();
+ }
+ });
+ })
+ .detach()
+ }
+ }
+
+ pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ let index = 0;
+ delegate.update(cx, |delegate, cx| delegate.set_selected_index(0, cx));
+ self.list_state.scroll_to(ScrollTarget::Show(index));
+ cx.notify();
+ }
+ }
+
+ pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ let index = action.0;
+ self.confirmed = true;
+ delegate.update(cx, |delegate, cx| {
+ delegate.set_selected_index(index, cx);
+ delegate.confirm(cx);
+ });
+ }
+ }
+
+ pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ let index = delegate.update(cx, |delegate, cx| {
+ let match_count = delegate.match_count();
+ let index = if match_count > 0 { match_count - 1 } else { 0 };
+ delegate.set_selected_index(index, cx);
+ index
+ });
+ self.list_state.scroll_to(ScrollTarget::Show(index));
+ cx.notify();
+ }
+ }
+
+ pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ let index = delegate.update(cx, |delegate, cx| {
+ let mut selected_index = delegate.selected_index();
+ if selected_index + 1 < delegate.match_count() {
+ selected_index += 1;
+ delegate.set_selected_index(selected_index, cx);
+ }
+ selected_index
+ });
+ self.list_state.scroll_to(ScrollTarget::Show(index));
+ cx.notify();
+ }
+ }
+
+ pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ let index = delegate.update(cx, |delegate, cx| {
+ let mut selected_index = delegate.selected_index();
+ if selected_index > 0 {
+ selected_index -= 1;
+ delegate.set_selected_index(selected_index, cx);
+ }
+ selected_index
+ });
+ self.list_state.scroll_to(ScrollTarget::Show(index));
+ cx.notify();
+ }
+ }
+
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ self.confirmed = true;
+ delegate.update(cx, |delegate, cx| delegate.confirm(cx));
+ }
+ }
+
+ fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ if let Some(delegate) = self.delegate.upgrade(cx) {
+ delegate.update(cx, |delegate, cx| delegate.dismiss(cx));
+ }
+ }
+}
@@ -25,6 +25,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
rpc = { path = "../rpc" }
+settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
util = { path = "../util" }
aho-corasick = "0.7"
@@ -34,7 +35,7 @@ futures = "0.3"
ignore = "0.4"
lazy_static = "1.4.0"
libc = "0.2"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = "0.8.3"
@@ -28,6 +28,8 @@ use parking_lot::Mutex;
use postage::watch;
use rand::prelude::*;
use search::SearchQuery;
+use serde::Serialize;
+use settings::Settings;
use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff};
use std::{
@@ -73,7 +75,6 @@ pub struct Project {
client_state: ProjectClientState,
collaborators: HashMap<PeerId, Collaborator>,
subscriptions: Vec<client::Subscription>,
- language_servers_with_diagnostics_running: isize,
opened_buffer: (Rc<RefCell<watch::Sender<()>>>, watch::Receiver<()>),
shared_buffers: HashMap<PeerId, HashSet<u64>>,
loading_buffers: HashMap<
@@ -132,16 +133,18 @@ pub enum Event {
CollaboratorLeft(PeerId),
}
+#[derive(Serialize)]
pub struct LanguageServerStatus {
pub name: String,
pub pending_work: BTreeMap<String, LanguageServerProgress>,
- pending_diagnostic_updates: isize,
+ pub pending_diagnostic_updates: isize,
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize)]
pub struct LanguageServerProgress {
pub message: Option<String>,
pub percentage: Option<usize>,
+ #[serde(skip_serializing)]
pub last_update_at: Instant,
}
@@ -151,12 +154,10 @@ pub struct ProjectPath {
pub path: Arc<Path>,
}
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)]
pub struct DiagnosticSummary {
pub error_count: usize,
pub warning_count: usize,
- pub info_count: usize,
- pub hint_count: usize,
}
#[derive(Debug)]
@@ -192,8 +193,6 @@ impl DiagnosticSummary {
let mut this = Self {
error_count: 0,
warning_count: 0,
- info_count: 0,
- hint_count: 0,
};
for entry in diagnostics {
@@ -201,8 +200,6 @@ impl DiagnosticSummary {
match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => this.error_count += 1,
DiagnosticSeverity::WARNING => this.warning_count += 1,
- DiagnosticSeverity::INFORMATION => this.info_count += 1,
- DiagnosticSeverity::HINT => this.hint_count += 1,
_ => {}
}
}
@@ -211,13 +208,15 @@ impl DiagnosticSummary {
this
}
+ pub fn is_empty(&self) -> bool {
+ self.error_count == 0 && self.warning_count == 0
+ }
+
pub fn to_proto(&self, path: &Path) -> proto::DiagnosticSummary {
proto::DiagnosticSummary {
path: path.to_string_lossy().to_string(),
error_count: self.error_count as u32,
warning_count: self.warning_count as u32,
- info_count: self.info_count as u32,
- hint_count: self.hint_count as u32,
}
}
}
@@ -329,7 +328,6 @@ impl Project {
user_store,
fs,
next_entry_id: Default::default(),
- language_servers_with_diagnostics_running: 0,
language_servers: Default::default(),
started_language_servers: Default::default(),
language_server_statuses: Default::default(),
@@ -403,7 +401,6 @@ impl Project {
.log_err()
}),
},
- language_servers_with_diagnostics_running: 0,
language_servers: Default::default(),
started_language_servers: Default::default(),
language_server_settings: Default::default(),
@@ -469,7 +466,6 @@ impl Project {
.and_then(|buffer| buffer.upgrade(cx))
}
- #[cfg(any(test, feature = "test-support"))]
pub fn languages(&self) -> &Arc<LanguageRegistry> {
&self.languages
}
@@ -815,13 +811,19 @@ impl Project {
!self.is_local()
}
- pub fn create_buffer(&mut self, cx: &mut ModelContext<Self>) -> Result<ModelHandle<Buffer>> {
+ pub fn create_buffer(
+ &mut self,
+ text: &str,
+ language: Option<Arc<Language>>,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<ModelHandle<Buffer>> {
if self.is_remote() {
return Err(anyhow!("creating buffers as a guest is not supported yet"));
}
let buffer = cx.add_model(|cx| {
- Buffer::new(self.replica_id(), "", cx).with_language(language::PLAIN_TEXT.clone(), cx)
+ Buffer::new(self.replica_id(), text, cx)
+ .with_language(language.unwrap_or(language::PLAIN_TEXT.clone()), cx)
});
self.register_buffer(&buffer, cx)?;
Ok(buffer)
@@ -1019,7 +1021,14 @@ impl Project {
cx: &mut ModelContext<Project>,
) -> Task<Result<()>> {
let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
+ let old_path =
+ File::from_dyn(buffer.read(cx).file()).and_then(|f| Some(f.as_local()?.abs_path(cx)));
cx.spawn(|this, mut cx| async move {
+ if let Some(old_path) = old_path {
+ this.update(&mut cx, |this, cx| {
+ this.unregister_buffer_from_language_server(&buffer, old_path, cx);
+ });
+ }
let (worktree, path) = worktree_task.await?;
worktree
.update(&mut cx, |worktree, cx| {
@@ -1091,6 +1100,23 @@ impl Project {
self.assign_language_to_buffer(buffer, cx);
self.register_buffer_with_language_server(buffer, cx);
+ cx.observe_release(buffer, |this, buffer, cx| {
+ if let Some(file) = File::from_dyn(buffer.file()) {
+ if file.is_local() {
+ let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+ if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) {
+ server
+ .notify::<lsp::notification::DidCloseTextDocument>(
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(uri.clone()),
+ },
+ )
+ .log_err();
+ }
+ }
+ }
+ })
+ .detach();
Ok(())
}
@@ -1143,30 +1169,33 @@ impl Project {
self.buffer_snapshots
.insert(buffer_id, vec![(0, initial_snapshot)]);
}
-
- cx.observe_release(buffer_handle, |this, buffer, cx| {
- if let Some(file) = File::from_dyn(buffer.file()) {
- if file.is_local() {
- let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
- if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) {
- server
- .notify::<lsp::notification::DidCloseTextDocument>(
- lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(
- uri.clone(),
- ),
- },
- )
- .log_err();
- }
- }
- }
- })
- .detach();
}
}
}
+ fn unregister_buffer_from_language_server(
+ &mut self,
+ buffer: &ModelHandle<Buffer>,
+ old_path: PathBuf,
+ cx: &mut ModelContext<Self>,
+ ) {
+ buffer.update(cx, |buffer, cx| {
+ buffer.update_diagnostics(Default::default(), cx);
+ self.buffer_snapshots.remove(&buffer.remote_id());
+ if let Some((_, language_server)) = self.language_server_for_buffer(buffer, cx) {
+ language_server
+ .notify::<lsp::notification::DidCloseTextDocument>(
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(
+ lsp::Url::from_file_path(old_path).unwrap(),
+ ),
+ },
+ )
+ .log_err();
+ }
+ });
+ }
+
fn on_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
@@ -1191,7 +1220,7 @@ impl Project {
let file = File::from_dyn(buffer.file())?;
let abs_path = file.as_local()?.abs_path(cx);
let uri = lsp::Url::from_file_path(abs_path).unwrap();
- let buffer_snapshots = self.buffer_snapshots.entry(buffer.remote_id()).or_default();
+ let buffer_snapshots = self.buffer_snapshots.get_mut(&buffer.remote_id())?;
let (version, prev_snapshot) = buffer_snapshots.last()?;
let next_snapshot = buffer.text_snapshot();
let next_version = version + 1;
@@ -1605,93 +1634,84 @@ impl Project {
return;
}
};
-
- match progress.value {
- lsp::ProgressParamsValue::WorkDone(progress) => match progress {
- lsp::WorkDoneProgress::Begin(_) => {
- let language_server_status =
- if let Some(status) = self.language_server_statuses.get_mut(&server_id) {
- status
- } else {
- return;
- };
-
- if Some(token.as_str()) == disk_based_diagnostics_progress_token {
- language_server_status.pending_diagnostic_updates += 1;
- if language_server_status.pending_diagnostic_updates == 1 {
- self.disk_based_diagnostics_started(cx);
- self.broadcast_language_server_update(
- server_id,
- proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
- proto::LspDiskBasedDiagnosticsUpdating {},
- ),
- );
- }
- } else {
- self.on_lsp_work_start(server_id, token.clone(), cx);
+ let progress = match progress.value {
+ lsp::ProgressParamsValue::WorkDone(value) => value,
+ };
+ let language_server_status =
+ if let Some(status) = self.language_server_statuses.get_mut(&server_id) {
+ status
+ } else {
+ return;
+ };
+ match progress {
+ lsp::WorkDoneProgress::Begin(_) => {
+ if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+ language_server_status.pending_diagnostic_updates += 1;
+ if language_server_status.pending_diagnostic_updates == 1 {
+ self.disk_based_diagnostics_started(cx);
self.broadcast_language_server_update(
server_id,
- proto::update_language_server::Variant::WorkStart(
- proto::LspWorkStart { token },
+ proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+ proto::LspDiskBasedDiagnosticsUpdating {},
),
);
}
+ } else {
+ self.on_lsp_work_start(server_id, token.clone(), cx);
+ self.broadcast_language_server_update(
+ server_id,
+ proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
+ token,
+ }),
+ );
}
- lsp::WorkDoneProgress::Report(report) => {
- if Some(token.as_str()) != disk_based_diagnostics_progress_token {
- self.on_lsp_work_progress(
- server_id,
- token.clone(),
- LanguageServerProgress {
- message: report.message.clone(),
- percentage: report.percentage.map(|p| p as usize),
- last_update_at: Instant::now(),
+ }
+ lsp::WorkDoneProgress::Report(report) => {
+ if Some(token.as_str()) != disk_based_diagnostics_progress_token {
+ self.on_lsp_work_progress(
+ server_id,
+ token.clone(),
+ LanguageServerProgress {
+ message: report.message.clone(),
+ percentage: report.percentage.map(|p| p as usize),
+ last_update_at: Instant::now(),
+ },
+ cx,
+ );
+ self.broadcast_language_server_update(
+ server_id,
+ proto::update_language_server::Variant::WorkProgress(
+ proto::LspWorkProgress {
+ token,
+ message: report.message,
+ percentage: report.percentage.map(|p| p as u32),
},
- cx,
- );
- self.broadcast_language_server_update(
- server_id,
- proto::update_language_server::Variant::WorkProgress(
- proto::LspWorkProgress {
- token,
- message: report.message,
- percentage: report.percentage.map(|p| p as u32),
- },
- ),
- );
- }
+ ),
+ );
}
- lsp::WorkDoneProgress::End(_) => {
- if Some(token.as_str()) == disk_based_diagnostics_progress_token {
- let language_server_status = if let Some(status) =
- self.language_server_statuses.get_mut(&server_id)
- {
- status
- } else {
- return;
- };
-
- language_server_status.pending_diagnostic_updates -= 1;
- if language_server_status.pending_diagnostic_updates == 0 {
- self.disk_based_diagnostics_finished(cx);
- self.broadcast_language_server_update(
- server_id,
- proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
- proto::LspDiskBasedDiagnosticsUpdated {},
- ),
- );
- }
- } else {
- self.on_lsp_work_end(server_id, token.clone(), cx);
+ }
+ lsp::WorkDoneProgress::End(_) => {
+ if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+ language_server_status.pending_diagnostic_updates -= 1;
+ if language_server_status.pending_diagnostic_updates == 0 {
+ self.disk_based_diagnostics_finished(cx);
self.broadcast_language_server_update(
server_id,
- proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
- token,
- }),
+ proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+ proto::LspDiskBasedDiagnosticsUpdated {},
+ ),
);
}
+ } else {
+ self.on_lsp_work_end(server_id, token.clone(), cx);
+ self.broadcast_language_server_update(
+ server_id,
+ proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
+ token,
+ }),
+ );
}
- },
+ }
}
}
@@ -1937,26 +1957,19 @@ impl Project {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
};
-
- for buffer in self.opened_buffers.values() {
- if let Some(buffer) = buffer.upgrade(cx) {
- if buffer
- .read(cx)
- .file()
- .map_or(false, |file| *file.path() == project_path.path)
- {
- self.update_buffer_diagnostics(&buffer, diagnostics.clone(), version, cx)?;
- break;
- }
- }
+ if let Some(buffer) = self.get_open_buffer(&project_path, cx) {
+ self.update_buffer_diagnostics(&buffer, diagnostics.clone(), version, cx)?;
}
- worktree.update(cx, |worktree, cx| {
+
+ let updated = worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.ok_or_else(|| anyhow!("not a local worktree"))?
.update_diagnostics(project_path.path.clone(), diagnostics, cx)
})?;
- cx.emit(Event::DiagnosticsUpdated(project_path));
+ if updated {
+ cx.emit(Event::DiagnosticsUpdated(project_path));
+ }
Ok(())
}
@@ -2146,6 +2159,10 @@ impl Project {
lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
);
let capabilities = &language_server.capabilities();
+ let tab_size = cx.update(|cx| {
+ let language_name = buffer.read(cx).language().map(|language| language.name());
+ cx.global::<Settings>().tab_size(language_name.as_deref())
+ });
let lsp_edits = if capabilities
.document_formatting_provider
.as_ref()
@@ -2155,7 +2172,7 @@ impl Project {
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
text_document,
options: lsp::FormattingOptions {
- tab_size: 4,
+ tab_size,
insert_spaces: true,
insert_final_newline: Some(true),
..Default::default()
@@ -2250,86 +2267,81 @@ impl Project {
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
if self.is_local() {
- let mut language_servers = HashMap::default();
+ let mut requests = Vec::new();
for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() {
+ let worktree_id = *worktree_id;
if let Some(worktree) = self
- .worktree_for_id(*worktree_id, cx)
+ .worktree_for_id(worktree_id, cx)
.and_then(|worktree| worktree.read(cx).as_local())
{
- language_servers
- .entry(Arc::as_ptr(language_server))
- .or_insert((
- lsp_adapter.clone(),
- language_server.clone(),
- *worktree_id,
- worktree.abs_path().clone(),
- ));
+ let lsp_adapter = lsp_adapter.clone();
+ let worktree_abs_path = worktree.abs_path().clone();
+ requests.push(
+ language_server
+ .request::<lsp::request::WorkspaceSymbol>(lsp::WorkspaceSymbolParams {
+ query: query.to_string(),
+ ..Default::default()
+ })
+ .log_err()
+ .map(move |response| {
+ (
+ lsp_adapter,
+ worktree_id,
+ worktree_abs_path,
+ response.unwrap_or_default(),
+ )
+ }),
+ );
}
}
- let mut requests = Vec::new();
- for (_, language_server, _, _) in language_servers.values() {
- requests.push(language_server.request::<lsp::request::WorkspaceSymbol>(
- lsp::WorkspaceSymbolParams {
- query: query.to_string(),
- ..Default::default()
- },
- ));
- }
-
cx.spawn_weak(|this, cx| async move {
- let responses = futures::future::try_join_all(requests).await?;
-
- let mut symbols = Vec::new();
- if let Some(this) = this.upgrade(&cx) {
- this.read_with(&cx, |this, cx| {
- for ((adapter, _, source_worktree_id, worktree_abs_path), lsp_symbols) in
- language_servers.into_values().zip(responses)
- {
- symbols.extend(lsp_symbols.into_iter().flatten().filter_map(
- |lsp_symbol| {
- let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
- let mut worktree_id = source_worktree_id;
- let path;
- if let Some((worktree, rel_path)) =
- this.find_local_worktree(&abs_path, cx)
- {
- worktree_id = worktree.read(cx).id();
- path = rel_path;
- } else {
- path = relativize_path(&worktree_abs_path, &abs_path);
- }
-
- let label = this
- .languages
- .select_language(&path)
- .and_then(|language| {
- language
- .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
- })
- .unwrap_or_else(|| {
- CodeLabel::plain(lsp_symbol.name.clone(), None)
- });
- let signature = this.symbol_signature(worktree_id, &path);
-
- Some(Symbol {
- source_worktree_id,
- worktree_id,
- language_server_name: adapter.name(),
- name: lsp_symbol.name,
- kind: lsp_symbol.kind,
- label,
- path,
- range: range_from_lsp(lsp_symbol.location.range),
- signature,
- })
- },
- ));
- }
- })
- }
+ let responses = futures::future::join_all(requests).await;
+ let this = if let Some(this) = this.upgrade(&cx) {
+ this
+ } else {
+ return Ok(Default::default());
+ };
+ this.read_with(&cx, |this, cx| {
+ let mut symbols = Vec::new();
+ for (adapter, source_worktree_id, worktree_abs_path, response) in responses {
+ symbols.extend(response.into_iter().flatten().filter_map(|lsp_symbol| {
+ let abs_path = lsp_symbol.location.uri.to_file_path().ok()?;
+ let mut worktree_id = source_worktree_id;
+ let path;
+ if let Some((worktree, rel_path)) =
+ this.find_local_worktree(&abs_path, cx)
+ {
+ worktree_id = worktree.read(cx).id();
+ path = rel_path;
+ } else {
+ path = relativize_path(&worktree_abs_path, &abs_path);
+ }
- Ok(symbols)
+ let label = this
+ .languages
+ .select_language(&path)
+ .and_then(|language| {
+ language.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
+ })
+ .unwrap_or_else(|| CodeLabel::plain(lsp_symbol.name.clone(), None));
+ let signature = this.symbol_signature(worktree_id, &path);
+
+ Some(Symbol {
+ source_worktree_id,
+ worktree_id,
+ language_server_name: adapter.name(),
+ name: lsp_symbol.name,
+ kind: lsp_symbol.kind,
+ label,
+ path,
+ range: range_from_lsp(lsp_symbol.location.range),
+ signature,
+ })
+ }));
+ }
+ Ok(symbols)
+ })
})
} else if let Some(project_id) = self.remote_id() {
let request = self.client.request(proto::GetProjectSymbols {
@@ -3387,6 +3399,7 @@ impl Project {
) {
let snapshot = worktree_handle.read(cx).snapshot();
let mut buffers_to_delete = Vec::new();
+ let mut renamed_buffers = Vec::new();
for (buffer_id, buffer) in &self.opened_buffers {
if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| {
@@ -3426,6 +3439,11 @@ impl Project {
}
};
+ let old_path = old_file.abs_path(cx);
+ if new_file.abs_path(cx) != old_path {
+ renamed_buffers.push((cx.handle(), old_path));
+ }
+
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateBufferFile {
@@ -3446,6 +3464,12 @@ impl Project {
for buffer_id in buffers_to_delete {
self.opened_buffers.remove(&buffer_id);
}
+
+ for (buffer, old_path) in renamed_buffers {
+ self.unregister_buffer_from_language_server(&buffer, old_path, cx);
+ self.assign_language_to_buffer(&buffer, cx);
+ self.register_buffer_with_language_server(&buffer, cx);
+ }
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
@@ -3461,7 +3485,9 @@ impl Project {
}
pub fn is_running_disk_based_diagnostics(&self) -> bool {
- self.language_servers_with_diagnostics_running > 0
+ self.language_server_statuses
+ .values()
+ .any(|status| status.pending_diagnostic_updates > 0)
}
pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
@@ -3469,8 +3495,6 @@ impl Project {
for (_, path_summary) in self.diagnostic_summaries(cx) {
summary.error_count += path_summary.error_count;
summary.warning_count += path_summary.warning_count;
- summary.info_count += path_summary.info_count;
- summary.hint_count += path_summary.hint_count;
}
summary
}
@@ -3489,16 +3513,26 @@ impl Project {
}
pub fn disk_based_diagnostics_started(&mut self, cx: &mut ModelContext<Self>) {
- self.language_servers_with_diagnostics_running += 1;
- if self.language_servers_with_diagnostics_running == 1 {
+ if self
+ .language_server_statuses
+ .values()
+ .map(|status| status.pending_diagnostic_updates)
+ .sum::<isize>()
+ == 1
+ {
cx.emit(Event::DiskBasedDiagnosticsStarted);
}
}
pub fn disk_based_diagnostics_finished(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(Event::DiskBasedDiagnosticsUpdated);
- self.language_servers_with_diagnostics_running -= 1;
- if self.language_servers_with_diagnostics_running == 0 {
+ if self
+ .language_server_statuses
+ .values()
+ .map(|status| status.pending_diagnostic_updates)
+ .sum::<isize>()
+ == 0
+ {
cx.emit(Event::DiskBasedDiagnosticsFinished);
}
}
@@ -3806,7 +3840,7 @@ impl Project {
let buffer = this
.opened_buffers
.get(&buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?;
Ok::<_, anyhow::Error>((project_id, buffer))
})?;
@@ -3838,7 +3872,7 @@ impl Project {
buffers.insert(
this.opened_buffers
.get(buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
);
}
@@ -3867,7 +3901,7 @@ impl Project {
buffers.insert(
this.opened_buffers
.get(buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
);
}
@@ -3898,7 +3932,7 @@ impl Project {
let buffer = this.read_with(&cx, |this, cx| {
this.opened_buffers
.get(&envelope.payload.buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
})?;
buffer
@@ -3928,7 +3962,7 @@ impl Project {
let buffer = this
.opened_buffers
.get(&envelope.payload.buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
let language = buffer.read(cx).language();
let completion = language::proto::deserialize_completion(
@@ -3970,7 +4004,7 @@ impl Project {
let buffer = this.update(&mut cx, |this, cx| {
this.opened_buffers
.get(&envelope.payload.buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
})?;
buffer
@@ -4011,7 +4045,7 @@ impl Project {
let buffer = this
.opened_buffers
.get(&envelope.payload.buffer_id)
- .map(|buffer| buffer.upgrade(cx).unwrap())
+ .and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?;
Ok::<_, anyhow::Error>(this.apply_code_action(buffer, action, false, cx))
})?;
@@ -4847,7 +4881,7 @@ mod tests {
};
use lsp::Url;
use serde_json::json;
- use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc};
+ use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
use unindent::Unindent as _;
use util::{assert_set_eq, test::temp_tree};
use worktree::WorktreeHandle as _;
@@ -4970,7 +5004,7 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs.clone(), cx);
project.update(cx, |project, _| {
project.languages.add(Arc::new(rust_language));
project.languages.add(Arc::new(json_language));
@@ -5122,6 +5156,110 @@ mod tests {
)
);
+ // Renames are reported only to servers matching the buffer's language.
+ fs.rename(
+ Path::new("/the-root/test2.rs"),
+ Path::new("/the-root/test3.rs"),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(
+ fake_rust_server
+ .receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await
+ .text_document,
+ lsp::TextDocumentIdentifier::new(
+ lsp::Url::from_file_path("/the-root/test2.rs").unwrap()
+ ),
+ );
+ assert_eq!(
+ fake_rust_server
+ .receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await
+ .text_document,
+ lsp::TextDocumentItem {
+ uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),
+ version: 0,
+ text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+ language_id: Default::default()
+ },
+ );
+
+ rust_buffer2.update(cx, |buffer, cx| {
+ buffer.update_diagnostics(
+ DiagnosticSet::from_sorted_entries(
+ vec![DiagnosticEntry {
+ diagnostic: Default::default(),
+ range: Anchor::MIN..Anchor::MAX,
+ }],
+ &buffer.snapshot(),
+ ),
+ cx,
+ );
+ assert_eq!(
+ buffer
+ .snapshot()
+ .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+ .count(),
+ 1
+ );
+ });
+
+ // When the rename changes the extension of the file, the buffer gets closed on the old
+ // language server and gets opened on the new one.
+ fs.rename(
+ Path::new("/the-root/test3.rs"),
+ Path::new("/the-root/test3.json"),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ assert_eq!(
+ fake_rust_server
+ .receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await
+ .text_document,
+ lsp::TextDocumentIdentifier::new(
+ lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),
+ ),
+ );
+ assert_eq!(
+ fake_json_server
+ .receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await
+ .text_document,
+ lsp::TextDocumentItem {
+ uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
+ version: 0,
+ text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
+ language_id: Default::default()
+ },
+ );
+ // We clear the diagnostics, since the language has changed.
+ rust_buffer2.read_with(cx, |buffer, _| {
+ assert_eq!(
+ buffer
+ .snapshot()
+ .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
+ .count(),
+ 0
+ );
+ });
+
+ // The renamed file's version resets after changing language server.
+ rust_buffer2.update(cx, |buffer, cx| buffer.edit([0..0], "// ", cx));
+ assert_eq!(
+ fake_json_server
+ .receive_notification::<lsp::notification::DidChangeTextDocument>()
+ .await
+ .text_document,
+ lsp::VersionedTextDocumentIdentifier::new(
+ lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
+ 1
+ )
+ );
+
// Restart language servers
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
@@ -5139,48 +5277,48 @@ mod tests {
let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
let mut fake_json_server = fake_json_servers.next().await.unwrap();
- // Ensure both rust documents are reopened in new rust language server without worrying about order
+ // Ensure rust document is reopened in new rust language server
+ assert_eq!(
+ fake_rust_server
+ .receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await
+ .text_document,
+ lsp::TextDocumentItem {
+ uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
+ version: 1,
+ text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
+ language_id: Default::default()
+ }
+ );
+
+ // Ensure json documents are reopened in new json language server
assert_set_eq!(
[
- fake_rust_server
+ fake_json_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
- fake_rust_server
+ fake_json_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
],
[
lsp::TextDocumentItem {
- uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
- version: 1,
- text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
+ uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
+ version: 0,
+ text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default()
},
lsp::TextDocumentItem {
- uri: lsp::Url::from_file_path("/the-root/test2.rs").unwrap(),
+ uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
version: 1,
text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default()
- },
+ }
]
);
- // Ensure json document is reopened in new json language server
- assert_eq!(
- fake_json_server
- .receive_notification::<lsp::notification::DidOpenTextDocument>()
- .await
- .text_document,
- lsp::TextDocumentItem {
- uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
- version: 0,
- text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
- language_id: Default::default()
- }
- );
-
// Close notifications are reported only to servers matching the buffer's language.
cx.update(|_| drop(json_buffer));
let close_message = lsp::DidCloseTextDocumentParams {
@@ -5196,6 +5334,122 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "a.rs": "let a = 1;",
+ "b.rs": "let b = 2;"
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, cx);
+ let worktree_a_id = project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree("/dir/a.rs", true, cx)
+ })
+ .await
+ .unwrap()
+ .0
+ .read_with(cx, |tree, _| tree.id());
+ let worktree_b_id = project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree("/dir/b.rs", true, cx)
+ })
+ .await
+ .unwrap()
+ .0
+ .read_with(cx, |tree, _| tree.id());
+
+ let buffer_a = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_a_id, ""), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_b = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_b_id, ""), cx)
+ })
+ .await
+ .unwrap();
+
+ project.update(cx, |project, cx| {
+ project
+ .update_diagnostics(
+ lsp::PublishDiagnosticsParams {
+ uri: Url::from_file_path("/dir/a.rs").unwrap(),
+ version: None,
+ diagnostics: vec![lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 4),
+ lsp::Position::new(0, 5),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ message: "error 1".to_string(),
+ ..Default::default()
+ }],
+ },
+ &[],
+ cx,
+ )
+ .unwrap();
+ project
+ .update_diagnostics(
+ lsp::PublishDiagnosticsParams {
+ uri: Url::from_file_path("/dir/b.rs").unwrap(),
+ version: None,
+ diagnostics: vec![lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 4),
+ lsp::Position::new(0, 5),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ message: "error 2".to_string(),
+ ..Default::default()
+ }],
+ },
+ &[],
+ cx,
+ )
+ .unwrap();
+ });
+
+ buffer_a.read_with(cx, |buffer, _| {
+ let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
+ assert_eq!(
+ chunks
+ .iter()
+ .map(|(s, d)| (s.as_str(), *d))
+ .collect::<Vec<_>>(),
+ &[
+ ("let ", None),
+ ("a", Some(DiagnosticSeverity::ERROR)),
+ (" = 1;", None),
+ ]
+ );
+ });
+ buffer_b.read_with(cx, |buffer, _| {
+ let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
+ assert_eq!(
+ chunks
+ .iter()
+ .map(|(s, d)| (s.as_str(), *d))
+ .collect::<Vec<_>>(),
+ &[
+ ("let ", None),
+ ("b", Some(DiagnosticSeverity::WARNING)),
+ (" = 2;", None),
+ ]
+ );
+ });
+ }
+
#[gpui::test]
async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
@@ -11,7 +11,10 @@ use client::{proto, Client, TypedEnvelope};
use clock::ReplicaId;
use collections::HashMap;
use futures::{
- channel::mpsc::{self, UnboundedSender},
+ channel::{
+ mpsc::{self, UnboundedSender},
+ oneshot,
+ },
Stream, StreamExt,
};
use fuzzy::CharBag;
@@ -26,7 +29,6 @@ use language::{
use lazy_static::lazy_static;
use parking_lot::Mutex;
use postage::{
- oneshot,
prelude::{Sink as _, Stream as _},
watch,
};
@@ -231,8 +233,6 @@ impl Worktree {
DiagnosticSummary {
error_count: summary.error_count as usize,
warning_count: summary.warning_count as usize,
- info_count: summary.info_count as usize,
- hint_count: summary.hint_count as usize,
},
)
}),
@@ -564,29 +564,37 @@ impl LocalWorktree {
worktree_path: Arc<Path>,
diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
_: &mut ModelContext<Worktree>,
- ) -> Result<()> {
- let summary = DiagnosticSummary::new(&diagnostics);
- self.diagnostic_summaries
- .insert(PathKey(worktree_path.clone()), summary.clone());
- self.diagnostics.insert(worktree_path.clone(), diagnostics);
-
- if let Some(share) = self.share.as_ref() {
- self.client
- .send(proto::UpdateDiagnosticSummary {
- project_id: share.project_id,
- worktree_id: self.id().to_proto(),
- summary: Some(proto::DiagnosticSummary {
- path: worktree_path.to_string_lossy().to_string(),
- error_count: summary.error_count as u32,
- warning_count: summary.warning_count as u32,
- info_count: summary.info_count as u32,
- hint_count: summary.hint_count as u32,
- }),
- })
- .log_err();
+ ) -> Result<bool> {
+ self.diagnostics.remove(&worktree_path);
+ let old_summary = self
+ .diagnostic_summaries
+ .remove(&PathKey(worktree_path.clone()))
+ .unwrap_or_default();
+ let new_summary = DiagnosticSummary::new(&diagnostics);
+ if !new_summary.is_empty() {
+ self.diagnostic_summaries
+ .insert(PathKey(worktree_path.clone()), new_summary);
+ self.diagnostics.insert(worktree_path.clone(), diagnostics);
+ }
+
+ let updated = !old_summary.is_empty() || !new_summary.is_empty();
+ if updated {
+ if let Some(share) = self.share.as_ref() {
+ self.client
+ .send(proto::UpdateDiagnosticSummary {
+ project_id: share.project_id,
+ worktree_id: self.id().to_proto(),
+ summary: Some(proto::DiagnosticSummary {
+ path: worktree_path.to_string_lossy().to_string(),
+ error_count: new_summary.error_count as u32,
+ warning_count: new_summary.warning_count as u32,
+ }),
+ })
+ .log_err();
+ }
}
- Ok(())
+ Ok(updated)
}
pub fn scan_complete(&self) -> impl Future<Output = ()> {
@@ -727,11 +735,11 @@ impl LocalWorktree {
pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
let register = self.register(project_id, cx);
- let (mut share_tx, mut share_rx) = oneshot::channel();
+ let (share_tx, share_rx) = oneshot::channel();
let (snapshots_to_send_tx, snapshots_to_send_rx) =
smol::channel::unbounded::<LocalSnapshot>();
if self.share.is_some() {
- let _ = share_tx.try_send(Ok(()));
+ let _ = share_tx.send(Ok(()));
} else {
let rpc = self.client.clone();
let worktree_id = cx.model_id() as u64;
@@ -756,15 +764,15 @@ impl LocalWorktree {
})
.await
{
- let _ = share_tx.try_send(Err(error));
+ let _ = share_tx.send(Err(error));
return Err(anyhow!("failed to send initial update worktree"));
} else {
- let _ = share_tx.try_send(Ok(()));
+ let _ = share_tx.send(Ok(()));
snapshot
}
}
Err(error) => {
- let _ = share_tx.try_send(Err(error.into()));
+ let _ = share_tx.send(Err(error.into()));
return Err(anyhow!("failed to send initial update worktree"));
}
};
@@ -804,9 +812,8 @@ impl LocalWorktree {
});
}
share_rx
- .next()
.await
- .unwrap_or_else(|| Err(anyhow!("share ended")))
+ .unwrap_or_else(|_| Err(anyhow!("share ended")))
})
}
@@ -845,15 +852,16 @@ impl RemoteWorktree {
path: Arc<Path>,
summary: &proto::DiagnosticSummary,
) {
- self.diagnostic_summaries.insert(
- PathKey(path.clone()),
- DiagnosticSummary {
- error_count: summary.error_count as usize,
- warning_count: summary.warning_count as usize,
- info_count: summary.info_count as usize,
- hint_count: summary.hint_count as usize,
- },
- );
+ let summary = DiagnosticSummary {
+ error_count: summary.error_count as usize,
+ warning_count: summary.warning_count as usize,
+ };
+ if summary.is_empty() {
+ self.diagnostic_summaries.remove(&PathKey(path.clone()));
+ } else {
+ self.diagnostic_summaries
+ .insert(PathKey(path.clone()), summary);
+ }
}
}
@@ -10,6 +10,7 @@ doctest = false
[dependencies]
gpui = { path = "../gpui" }
project = { path = "../project" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
@@ -1,15 +1,16 @@
use gpui::{
- action,
+ actions,
elements::{
Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
Svg, UniformList, UniformListState,
},
- keymap::{self, Binding},
+ impl_internal_actions, keymap,
platform::CursorStyle,
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use settings::Settings;
use std::{
collections::{hash_map, HashMap},
ffi::OsStr,
@@ -17,7 +18,7 @@ use std::{
};
use workspace::{
menu::{SelectNext, SelectPrev},
- Settings, Workspace,
+ Workspace,
};
pub struct ProjectPanel {
@@ -45,10 +46,14 @@ struct EntryDetails {
is_selected: bool,
}
-action!(ExpandSelectedEntry);
-action!(CollapseSelectedEntry);
-action!(ToggleExpanded, ProjectEntryId);
-action!(Open, ProjectEntryId);
+#[derive(Clone)]
+pub struct ToggleExpanded(pub ProjectEntryId);
+
+#[derive(Clone)]
+pub struct Open(pub ProjectEntryId);
+
+actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]);
+impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::expand_selected_entry);
@@ -57,10 +62,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::select_prev);
cx.add_action(ProjectPanel::select_next);
cx.add_action(ProjectPanel::open_entry);
- cx.add_bindings([
- Binding::new("right", ExpandSelectedEntry, Some("ProjectPanel")),
- Binding::new("left", CollapseSelectedEntry, Some("ProjectPanel")),
- ]);
}
pub enum Event {
@@ -11,11 +11,21 @@ doctest = false
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
+picker = { path = "../picker" }
project = { path = "../project" }
text = { path = "../text" }
+settings = { path = "../settings" }
workspace = { path = "../workspace" }
util = { path = "../util" }
anyhow = "1.0.38"
ordered-float = "2.1.1"
postage = { version = "0.4", features = ["futures-traits"] }
smol = "1.2"
+
+[dev-dependencies]
+futures = "0.3"
+settings = { path = "../settings", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
@@ -3,49 +3,33 @@ use editor::{
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- action,
- elements::*,
- keymap::{self, Binding},
- AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
- ViewContext, ViewHandle, WeakViewHandle,
+ actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
+ View, ViewContext, ViewHandle,
};
use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
use project::{Project, Symbol};
-use std::{
- borrow::Cow,
- cmp::{self, Reverse},
-};
+use settings::Settings;
+use std::{borrow::Cow, cmp::Reverse};
use util::ResultExt;
-use workspace::{
- menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
- Settings, Workspace,
-};
+use workspace::Workspace;
-action!(Toggle);
+actions!(project_symbols, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings([
- Binding::new("cmd-t", Toggle, None),
- Binding::new("escape", Toggle, Some("ProjectSymbolsView")),
- ]);
cx.add_action(ProjectSymbolsView::toggle);
- cx.add_action(ProjectSymbolsView::confirm);
- cx.add_action(ProjectSymbolsView::select_prev);
- cx.add_action(ProjectSymbolsView::select_next);
- cx.add_action(ProjectSymbolsView::select_first);
- cx.add_action(ProjectSymbolsView::select_last);
+ Picker::<ProjectSymbolsView>::init(cx);
}
pub struct ProjectSymbolsView {
- handle: WeakViewHandle<Self>,
+ picker: ViewHandle<Picker<Self>>,
project: ModelHandle<Project>,
selected_match_index: usize,
- list_state: UniformListState,
symbols: Vec<Symbol>,
match_candidates: Vec<StringMatchCandidate>,
+ show_worktree_root_name: bool,
+ pending_update: Task<()>,
matches: Vec<StringMatch>,
- pending_symbols_task: Task<Option<()>>,
- query_editor: ViewHandle<Editor>,
}
pub enum Event {
@@ -62,60 +46,28 @@ impl View for ProjectSymbolsView {
"ProjectSymbolsView"
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
- let mut cx = Self::default_keymap_context();
- cx.set.insert("menu".into());
- cx
- }
-
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let settings = cx.global::<Settings>();
- Flex::new(Axis::Vertical)
- .with_child(
- Container::new(ChildView::new(&self.query_editor).boxed())
- .with_style(settings.theme.selector.input_editor.container)
- .boxed(),
- )
- .with_child(
- FlexItem::new(self.render_matches(cx))
- .flex(1., false)
- .boxed(),
- )
- .contained()
- .with_style(settings.theme.selector.container)
- .constrained()
- .with_max_width(500.0)
- .with_max_height(420.0)
- .aligned()
- .top()
- .named("project symbols view")
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ ChildView::new(self.picker.clone()).boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.query_editor);
+ cx.focus(&self.picker);
}
}
impl ProjectSymbolsView {
fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
- let query_editor = cx.add_view(|cx| {
- Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
- });
- cx.subscribe(&query_editor, Self::on_query_editor_event)
- .detach();
- let mut this = Self {
- handle: cx.weak_handle(),
+ let handle = cx.weak_handle();
+ Self {
project,
+ picker: cx.add_view(|cx| Picker::new(handle, cx)),
selected_match_index: 0,
- list_state: Default::default(),
symbols: Default::default(),
match_candidates: Default::default(),
matches: Default::default(),
- pending_symbols_task: Task::ready(None),
- query_editor,
- };
- this.update_matches(cx);
- this
+ show_worktree_root_name: false,
+ pending_update: Task::ready(()),
+ }
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
@@ -127,72 +79,7 @@ impl ProjectSymbolsView {
});
}
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- if self.selected_match_index > 0 {
- self.select(self.selected_match_index - 1, cx);
- }
- }
-
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- if self.selected_match_index + 1 < self.matches.len() {
- self.select(self.selected_match_index + 1, cx);
- }
- }
-
- fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
- self.select(0, cx);
- }
-
- fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
- self.select(self.matches.len().saturating_sub(1), cx);
- }
-
- fn select(&mut self, index: usize, cx: &mut ViewContext<Self>) {
- self.selected_match_index = index;
- self.list_state.scroll_to(ScrollTarget::Show(index));
- cx.notify();
- }
-
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- if let Some(symbol) = self
- .matches
- .get(self.selected_match_index)
- .map(|mat| self.symbols[mat.candidate_id].clone())
- {
- cx.emit(Event::Selected(symbol));
- }
- }
-
- fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
- self.filter(cx);
- let query = self.query_editor.read(cx).text(cx);
- let symbols = self
- .project
- .update(cx, |project, cx| project.symbols(&query, cx));
- self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move {
- let symbols = symbols.await.log_err()?;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.match_candidates = symbols
- .iter()
- .enumerate()
- .map(|(id, symbol)| {
- StringMatchCandidate::new(
- id,
- symbol.label.text[symbol.label.filter_range.clone()].to_string(),
- )
- })
- .collect();
- this.symbols = symbols;
- this.filter(cx);
- });
- }
- None
- });
- }
-
- fn filter(&mut self, cx: &mut ViewContext<Self>) {
- let query = self.query_editor.read(cx).text(cx);
+ fn filter(&mut self, query: &str, cx: &mut ViewContext<Self>) {
let mut matches = if query.is_empty() {
self.match_candidates
.iter()
@@ -205,9 +92,9 @@ impl ProjectSymbolsView {
})
.collect()
} else {
- smol::block_on(fuzzy::match_strings(
+ cx.background_executor().block(fuzzy::match_strings(
&self.match_candidates,
- &query,
+ query,
false,
100,
&Default::default(),
@@ -231,57 +118,112 @@ impl ProjectSymbolsView {
}
self.matches = matches;
- self.select_first(&SelectFirst, cx);
+ self.set_selected_index(0, cx);
cx.notify();
}
- fn render_matches(&self, cx: &AppContext) -> ElementBox {
- if self.matches.is_empty() {
- let settings = cx.global::<Settings>();
- return Container::new(
- Label::new(
- "No matches".into(),
- settings.theme.selector.empty.label.clone(),
- )
- .boxed(),
- )
- .with_style(settings.theme.selector.empty.container)
- .named("empty matches");
+ fn on_event(
+ workspace: &mut Workspace,
+ _: ViewHandle<Self>,
+ event: &Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ match event {
+ Event::Dismissed => workspace.dismiss_modal(cx),
+ Event::Selected(symbol) => {
+ let buffer = workspace
+ .project()
+ .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+
+ let symbol = symbol.clone();
+ cx.spawn(|workspace, mut cx| async move {
+ let buffer = buffer.await?;
+ workspace.update(&mut cx, |workspace, cx| {
+ let position = buffer
+ .read(cx)
+ .clip_point_utf16(symbol.range.start, Bias::Left);
+
+ let editor = workspace.open_project_item::<Editor>(buffer, cx);
+ editor.update(cx, |editor, cx| {
+ editor.select_ranges(
+ [position..position],
+ Some(Autoscroll::Center),
+ cx,
+ );
+ });
+ });
+ Ok::<_, anyhow::Error>(())
+ })
+ .detach_and_log_err(cx);
+ workspace.dismiss_modal(cx);
+ }
}
+ }
+}
- let handle = self.handle.clone();
- let list = UniformList::new(
- self.list_state.clone(),
- self.matches.len(),
- move |mut range, items, cx| {
- let cx = cx.as_ref();
- let view = handle.upgrade(cx).unwrap();
- let view = view.read(cx);
- let start = range.start;
- range.end = cmp::min(range.end, view.matches.len());
-
- let show_worktree_root_name =
- view.project.read(cx).visible_worktrees(cx).count() > 1;
- items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| {
- view.render_match(m, start + ix, show_worktree_root_name, cx)
- }));
- },
- );
+impl PickerDelegate for ProjectSymbolsView {
+ fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(symbol) = self
+ .matches
+ .get(self.selected_match_index)
+ .map(|mat| self.symbols[mat.candidate_id].clone())
+ {
+ cx.emit(Event::Selected(symbol));
+ }
+ }
+
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(Event::Dismissed);
+ }
- Container::new(list.boxed())
- .with_margin_top(6.0)
- .named("matches")
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_match_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+ self.selected_match_index = ix;
+ cx.notify();
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+ self.filter(&query, cx);
+ self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
+ let symbols = self
+ .project
+ .update(cx, |project, cx| project.symbols(&query, cx));
+ self.pending_update = cx.spawn_weak(|this, mut cx| async move {
+ let symbols = symbols.await.log_err();
+ if let Some(this) = this.upgrade(&cx) {
+ if let Some(symbols) = symbols {
+ this.update(&mut cx, |this, cx| {
+ this.match_candidates = symbols
+ .iter()
+ .enumerate()
+ .map(|(id, symbol)| {
+ StringMatchCandidate::new(
+ id,
+ symbol.label.text[symbol.label.filter_range.clone()]
+ .to_string(),
+ )
+ })
+ .collect();
+ this.symbols = symbols;
+ this.filter(&query, cx);
+ });
+ }
+ }
+ });
+ Task::ready(())
}
- fn render_match(
- &self,
- string_match: &StringMatch,
- index: usize,
- show_worktree_root_name: bool,
- cx: &AppContext,
- ) -> ElementBox {
+ fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+ let string_match = &self.matches[ix];
let settings = cx.global::<Settings>();
- let style = if index == self.selected_match_index {
+ let style = if selected {
&settings.theme.selector.active_item
} else {
&settings.theme.selector.item
@@ -290,7 +232,7 @@ impl ProjectSymbolsView {
let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
let mut path = symbol.path.to_string_lossy();
- if show_worktree_root_name {
+ if self.show_worktree_root_name {
let project = self.project.read(cx);
if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
path = Cow::Owned(format!(
@@ -323,55 +265,139 @@ impl ProjectSymbolsView {
.with_style(style.container)
.boxed()
}
+}
- fn on_query_editor_event(
- &mut self,
- _: ViewHandle<Editor>,
- event: &editor::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- editor::Event::Blurred => cx.emit(Event::Dismissed),
- editor::Event::BufferEdited { .. } => self.update_matches(cx),
- _ => {}
- }
- }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use futures::StreamExt;
+ use gpui::{serde_json::json, TestAppContext};
+ use language::{FakeLspAdapter, Language, LanguageConfig};
+ use project::FakeFs;
+ use std::sync::Arc;
+
+ #[gpui::test]
+ async fn test_project_symbols(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| cx.set_global(Settings::test(cx)));
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ None,
+ );
+ let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter::default());
- fn on_event(
- workspace: &mut Workspace,
- _: ViewHandle<Self>,
- event: &Event,
- cx: &mut ViewContext<Workspace>,
- ) {
- match event {
- Event::Dismissed => workspace.dismiss_modal(cx),
- Event::Selected(symbol) => {
- let buffer = workspace
- .project()
- .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
- let symbol = symbol.clone();
- cx.spawn(|workspace, mut cx| async move {
- let buffer = buffer.await?;
- workspace.update(&mut cx, |workspace, cx| {
- let position = buffer
- .read(cx)
- .clip_point_utf16(symbol.range.start, Bias::Left);
+ let project = Project::test(fs.clone(), cx);
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::new(language));
+ });
- let editor = workspace.open_project_item::<Editor>(buffer, cx);
- editor.update(cx, |editor, cx| {
- editor.select_ranges(
- [position..position],
- Some(Autoscroll::Center),
- cx,
- );
- });
- });
- Ok::<_, anyhow::Error>(())
- })
- .detach_and_log_err(cx);
- workspace.dismiss_modal(cx);
- }
+ let worktree_id = project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree("/dir", true, cx)
+ })
+ .await
+ .unwrap()
+ .0
+ .read_with(cx, |tree, _| tree.id());
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "test.rs"), cx)
+ })
+ .await
+ .unwrap();
+
+ // Set up fake langauge server to return fuzzy matches against
+ // a fixed set of symbol names.
+ let fake_symbol_names = ["one", "ton", "uno"];
+ let fake_server = fake_servers.next().await.unwrap();
+ fake_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
+ move |params: lsp::WorkspaceSymbolParams, cx| {
+ let executor = cx.background();
+ async move {
+ let candidates = fake_symbol_names
+ .into_iter()
+ .map(|name| StringMatchCandidate::new(0, name.into()))
+ .collect::<Vec<_>>();
+ let matches = fuzzy::match_strings(
+ &candidates,
+ ¶ms.query,
+ true,
+ 100,
+ &Default::default(),
+ executor.clone(),
+ )
+ .await;
+ Ok(Some(
+ matches.into_iter().map(|mat| symbol(&mat.string)).collect(),
+ ))
+ }
+ },
+ );
+
+ // Create the project symbols view.
+ let (_, symbols_view) = cx.add_window(|cx| ProjectSymbolsView::new(project.clone(), cx));
+ let picker = symbols_view.read_with(cx, |symbols_view, _| symbols_view.picker.clone());
+
+ // Spawn multiples updates before the first update completes,
+ // such that in the end, there are no matches. Testing for regression:
+ // https://github.com/zed-industries/zed/issues/861
+ picker.update(cx, |p, cx| {
+ p.update_matches("o".to_string(), cx);
+ p.update_matches("on".to_string(), cx);
+ p.update_matches("onex".to_string(), cx);
+ });
+
+ cx.foreground().run_until_parked();
+ symbols_view.read_with(cx, |symbols_view, _| {
+ assert_eq!(symbols_view.matches.len(), 0);
+ });
+
+ // Spawn more updates such that in the end, there are matches.
+ picker.update(cx, |p, cx| {
+ p.update_matches("one".to_string(), cx);
+ p.update_matches("on".to_string(), cx);
+ });
+
+ cx.foreground().run_until_parked();
+ symbols_view.read_with(cx, |symbols_view, _| {
+ assert_eq!(symbols_view.matches.len(), 2);
+ assert_eq!(symbols_view.matches[0].string, "one");
+ assert_eq!(symbols_view.matches[1].string, "ton");
+ });
+
+ // Spawn more updates such that in the end, there are again no matches.
+ picker.update(cx, |p, cx| {
+ p.update_matches("o".to_string(), cx);
+ p.update_matches("".to_string(), cx);
+ });
+
+ cx.foreground().run_until_parked();
+ symbols_view.read_with(cx, |symbols_view, _| {
+ assert_eq!(symbols_view.matches.len(), 0);
+ });
+ }
+
+ fn symbol(name: &str) -> lsp::SymbolInformation {
+ #[allow(deprecated)]
+ lsp::SymbolInformation {
+ name: name.to_string(),
+ kind: lsp::SymbolKind::FUNCTION,
+ tags: None,
+ deprecated: None,
+ container_name: None,
+ location: lsp::Location::new(
+ lsp::Url::from_file_path("/a/b").unwrap(),
+ lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+ ),
}
}
}
@@ -9,31 +9,32 @@ path = "src/rpc.rs"
doctest = false
[features]
-test-support = ["gpui/test-support"]
+test-support = ["collections/test-support", "gpui/test-support"]
[dependencies]
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+gpui = { path = "../gpui", optional = true }
+util = { path = "../util" }
anyhow = "1.0"
async-lock = "2.4"
async-tungstenite = "0.16"
base64 = "0.13"
futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
prost = "0.8"
rand = "0.8"
rsa = "0.4"
serde = { version = "1", features = ["derive"] }
smol-timeout = "0.6"
zstd = "0.9"
-clock = { path = "../clock" }
-gpui = { path = "../gpui", optional = true }
-util = { path = "../util" }
[build-dependencies]
prost-build = "0.8"
[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
smol = "1.2.5"
tempdir = "0.3.7"
@@ -453,8 +453,6 @@ message DiagnosticSummary {
string path = 1;
uint32 error_count = 2;
uint32 warning_count = 3;
- uint32 info_count = 4;
- uint32 hint_count = 5;
}
message UpdateLanguageServer {
@@ -35,21 +35,24 @@ impl Connection {
#[cfg(any(test, feature = "test-support"))]
pub fn in_memory(
executor: std::sync::Arc<gpui::executor::Background>,
- ) -> (Self, Self, postage::barrier::Sender) {
- use postage::prelude::Stream;
+ ) -> (Self, Self, std::sync::Arc<std::sync::atomic::AtomicBool>) {
+ use std::sync::{
+ atomic::{AtomicBool, Ordering::SeqCst},
+ Arc,
+ };
- let (kill_tx, kill_rx) = postage::barrier::channel();
- let (a_tx, a_rx) = channel(kill_rx.clone(), executor.clone());
- let (b_tx, b_rx) = channel(kill_rx, executor);
+ let killed = Arc::new(AtomicBool::new(false));
+ let (a_tx, a_rx) = channel(killed.clone(), executor.clone());
+ let (b_tx, b_rx) = channel(killed.clone(), executor);
return (
Self { tx: a_tx, rx: b_rx },
Self { tx: b_tx, rx: a_rx },
- kill_tx,
+ killed,
);
fn channel(
- kill_rx: postage::barrier::Receiver,
- executor: std::sync::Arc<gpui::executor::Background>,
+ killed: Arc<AtomicBool>,
+ executor: Arc<gpui::executor::Background>,
) -> (
Box<dyn Send + Unpin + futures::Sink<WebSocketMessage, Error = WebSocketError>>,
Box<
@@ -57,20 +60,17 @@ impl Connection {
>,
) {
use futures::channel::mpsc;
- use std::{
- io::{Error, ErrorKind},
- sync::Arc,
- };
+ use std::io::{Error, ErrorKind};
let (tx, rx) = mpsc::unbounded::<WebSocketMessage>();
let tx = tx
.sink_map_err(|e| WebSocketError::from(Error::new(ErrorKind::Other, e)))
.with({
- let kill_rx = kill_rx.clone();
+ let killed = killed.clone();
let executor = Arc::downgrade(&executor);
move |msg| {
- let mut kill_rx = kill_rx.clone();
+ let killed = killed.clone();
let executor = executor.clone();
Box::pin(async move {
if let Some(executor) = executor.upgrade() {
@@ -78,7 +78,7 @@ impl Connection {
}
// Writes to a half-open TCP connection will error.
- if kill_rx.try_recv().is_ok() {
+ if killed.load(SeqCst) {
std::io::Result::Err(
Error::new(ErrorKind::Other, "connection lost").into(),
)?;
@@ -90,10 +90,10 @@ impl Connection {
});
let rx = rx.then({
- let kill_rx = kill_rx.clone();
+ let killed = killed.clone();
let executor = Arc::downgrade(&executor);
move |msg| {
- let mut kill_rx = kill_rx.clone();
+ let killed = killed.clone();
let executor = executor.clone();
Box::pin(async move {
if let Some(executor) = executor.upgrade() {
@@ -101,7 +101,7 @@ impl Connection {
}
// Reads from a half-open TCP connection will hang.
- if kill_rx.try_recv().is_ok() {
+ if killed.load(SeqCst) {
futures::future::pending::<()>().await;
}
@@ -1,16 +1,18 @@
-use super::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage};
-use super::Connection;
+use super::{
+ proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage},
+ Connection,
+};
use anyhow::{anyhow, Context, Result};
-use futures::{channel::oneshot, stream::BoxStream, FutureExt as _, StreamExt};
-use parking_lot::{Mutex, RwLock};
-use postage::{
- barrier, mpsc,
- prelude::{Sink as _, Stream as _},
+use collections::HashMap;
+use futures::{
+ channel::{mpsc, oneshot},
+ stream::BoxStream,
+ FutureExt, SinkExt, StreamExt,
};
-use smol_timeout::TimeoutExt as _;
+use parking_lot::{Mutex, RwLock};
+use smol_timeout::TimeoutExt;
use std::sync::atomic::Ordering::SeqCst;
use std::{
- collections::HashMap,
fmt,
future::Future,
marker::PhantomData,
@@ -88,10 +90,10 @@ pub struct Peer {
#[derive(Clone)]
pub struct ConnectionState {
- outgoing_tx: futures::channel::mpsc::UnboundedSender<proto::Message>,
+ outgoing_tx: mpsc::UnboundedSender<proto::Message>,
next_message_id: Arc<AtomicU32>,
response_channels:
- Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, barrier::Sender)>>>>>,
+ Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, oneshot::Sender<()>)>>>>>,
}
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
@@ -124,8 +126,12 @@ impl Peer {
// can always send messages without yielding. For incoming messages, use a
// bounded channel so that other peers will receive backpressure if they send
// messages faster than this peer can process them.
- let (mut incoming_tx, incoming_rx) = mpsc::channel(64);
- let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded();
+ #[cfg(any(test, feature = "test-support"))]
+ const INCOMING_BUFFER_SIZE: usize = 1;
+ #[cfg(not(any(test, feature = "test-support")))]
+ const INCOMING_BUFFER_SIZE: usize = 64;
+ let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE);
+ let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
let connection_id = ConnectionId(self.next_connection_id.fetch_add(1, SeqCst));
let connection_state = ConnectionState {
@@ -173,8 +179,10 @@ impl Peer {
let incoming = incoming.context("received invalid RPC message")?;
receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
if let proto::Message::Envelope(incoming) = incoming {
- if incoming_tx.send(incoming).await.is_err() {
- return Ok(());
+ match incoming_tx.send(incoming).timeout(RECEIVE_TIMEOUT).await {
+ Some(Ok(_)) => {},
+ Some(Err(_)) => return Ok(()),
+ None => Err(anyhow!("timed out processing incoming message"))?,
}
}
break;
@@ -206,14 +214,14 @@ impl Peer {
if let Some(responding_to) = incoming.responding_to {
let channel = response_channels.lock().as_mut()?.remove(&responding_to);
if let Some(tx) = channel {
- let mut requester_resumed = barrier::channel();
+ let requester_resumed = oneshot::channel();
if let Err(error) = tx.send((incoming, requester_resumed.0)) {
log::debug!(
"received RPC but request future was dropped {:?}",
error.0
);
}
- requester_resumed.1.recv().await;
+ let _ = requester_resumed.1.await;
} else {
log::warn!("received RPC response to unknown request {}", responding_to);
}
@@ -719,26 +727,26 @@ mod tests {
.add_test_connection(client_conn, cx.background())
.await;
- let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel();
+ let (io_ended_tx, io_ended_rx) = oneshot::channel();
executor
.spawn(async move {
io_handler.await.ok();
- io_ended_tx.send(()).await.unwrap();
+ io_ended_tx.send(()).unwrap();
})
.detach();
- let (mut messages_ended_tx, mut messages_ended_rx) = postage::barrier::channel();
+ let (messages_ended_tx, messages_ended_rx) = oneshot::channel();
executor
.spawn(async move {
incoming.next().await;
- messages_ended_tx.send(()).await.unwrap();
+ messages_ended_tx.send(()).unwrap();
})
.detach();
client.disconnect(connection_id);
- io_ended_rx.recv().await;
- messages_ended_rx.recv().await;
+ let _ = io_ended_rx.await;
+ let _ = messages_ended_rx.await;
assert!(server_conn
.send(WebSocketMessage::Binary(vec![]))
.await
@@ -5,4 +5,4 @@ pub mod proto;
pub use conn::Connection;
pub use peer::*;
-pub const PROTOCOL_VERSION: u32 = 13;
+pub const PROTOCOL_VERSION: u32 = 14;
@@ -13,12 +13,14 @@ editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1", features = ["derive"] }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
@@ -1,50 +1,47 @@
-use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch};
+use crate::{
+ active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch,
+ SelectPrevMatch,
+};
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
use gpui::{
- action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity,
- MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
+ actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, AppContext,
+ Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
WeakViewHandle,
};
use language::OffsetRangeExt;
use project::search::SearchQuery;
+use serde::Deserialize;
+use settings::Settings;
use std::ops::Range;
-use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
+use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
+
+#[derive(Clone, Deserialize)]
+pub struct Deploy {
+ pub focus: bool,
+}
-action!(Deploy, bool);
-action!(Dismiss);
-action!(FocusEditor);
-action!(ToggleSearchOption, SearchOption);
+#[derive(Clone)]
+pub struct ToggleSearchOption(pub SearchOption);
+
+actions!(buffer_search, [Dismiss, FocusEditor]);
+impl_actions!(buffer_search, [Deploy]);
+impl_internal_actions!(buffer_search, [ToggleSearchOption]);
pub enum Event {
UpdateLocation,
}
pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings([
- Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
- Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
- Binding::new("escape", Dismiss, Some("BufferSearchBar")),
- Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
- Binding::new(
- "enter",
- SelectMatch(Direction::Next),
- Some("BufferSearchBar"),
- ),
- Binding::new(
- "shift-enter",
- SelectMatch(Direction::Prev),
- Some("BufferSearchBar"),
- ),
- Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
- Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
- ]);
cx.add_action(BufferSearchBar::deploy);
cx.add_action(BufferSearchBar::dismiss);
cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::toggle_search_option);
- cx.add_action(BufferSearchBar::select_match);
- cx.add_action(BufferSearchBar::select_match_on_pane);
+ cx.add_action(BufferSearchBar::select_next_match);
+ cx.add_action(BufferSearchBar::select_prev_match);
+ cx.add_action(BufferSearchBar::select_next_match_on_pane);
+ cx.add_action(BufferSearchBar::select_prev_match_on_pane);
+ cx.add_action(BufferSearchBar::handle_editor_cancel);
}
pub struct BufferSearchBar {
@@ -320,14 +317,27 @@ impl BufferSearchBar {
.with_style(style.container)
.boxed()
})
- .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+ .on_click(move |cx| match direction {
+ Direction::Prev => cx.dispatch_action(SelectPrevMatch),
+ Direction::Next => cx.dispatch_action(SelectNextMatch),
+ })
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
- fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
+ fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+ if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, cx)) {
+ return;
+ }
+ }
+ cx.propagate_action();
+ }
+
+ fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
- if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
+ if !search_bar.read(cx).dismissed {
+ search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx));
return;
}
}
@@ -363,7 +373,15 @@ impl BufferSearchBar {
cx.notify();
}
- fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
+ fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
+ self.select_match(Direction::Next, cx);
+ }
+
+ fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
+ self.select_match(Direction::Prev, cx);
+ }
+
+ fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
if let Some(editor) = self.active_editor.as_ref() {
editor.update(cx, |editor, cx| {
@@ -384,9 +402,23 @@ impl BufferSearchBar {
}
}
- fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
+ fn select_next_match_on_pane(
+ pane: &mut Pane,
+ action: &SelectNextMatch,
+ cx: &mut ViewContext<Pane>,
+ ) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
- search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
+ search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx));
+ }
+ }
+
+ fn select_prev_match_on_pane(
+ pane: &mut Pane,
+ action: &SelectPrevMatch,
+ cx: &mut ViewContext<Pane>,
+ ) {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+ search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx));
}
}
@@ -512,14 +544,14 @@ impl BufferSearchBar {
}
}
- let theme = &cx.global::<Settings>().theme.search;
editor.highlight_background::<Self>(
ranges,
- theme.match_background,
+ |theme| theme.search.match_background,
cx,
);
});
}
+ cx.notify();
});
}
}));
@@ -694,7 +726,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
- search_bar.select_match(&SelectMatch(Direction::Next), cx);
+ search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -705,7 +737,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
- search_bar.select_match(&SelectMatch(Direction::Next), cx);
+ search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -716,7 +748,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
- search_bar.select_match(&SelectMatch(Direction::Next), cx);
+ search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -727,7 +759,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
- search_bar.select_match(&SelectMatch(Direction::Next), cx);
+ search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -738,7 +770,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
- search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -749,7 +781,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
- search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -760,7 +792,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
- search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -777,7 +809,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
- search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -794,7 +826,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
- search_bar.select_match(&SelectMatch(Direction::Next), cx);
+ search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
@@ -811,7 +843,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
- search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -828,7 +860,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
- search_bar.select_match(&SelectMatch(Direction::Next), cx);
+ search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
@@ -845,7 +877,7 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
- search_bar.select_match(&SelectMatch(Direction::Prev), cx);
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
@@ -1,29 +1,25 @@
use crate::{
- active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch,
- ToggleSearchOption,
+ active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch,
+ SelectPrevMatch, ToggleSearchOption,
};
use collections::HashMap;
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use gpui::{
- action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
- ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
- ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+ actions, elements::*, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext,
+ ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
+ ViewHandle, WeakModelHandle, WeakViewHandle,
};
use project::{search::SearchQuery, Project};
+use settings::Settings;
use std::{
any::{Any, TypeId},
ops::Range,
path::PathBuf,
};
use util::ResultExt as _;
-use workspace::{
- Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
-};
+use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace};
-action!(Deploy);
-action!(Search);
-action!(SearchInNew);
-action!(ToggleFocus);
+actions!(project_search, [Deploy, Search, SearchInNew, ToggleFocus]);
const MAX_TAB_TITLE_LEN: usize = 24;
@@ -32,20 +28,12 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
pub fn init(cx: &mut MutableAppContext) {
cx.set_global(ActiveSearches::default());
- cx.add_bindings([
- Binding::new("cmd-shift-F", ToggleFocus, Some("Pane")),
- Binding::new("cmd-f", ToggleFocus, Some("Pane")),
- Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
- Binding::new("enter", Search, Some("ProjectSearchBar")),
- Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
- Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
- Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
- ]);
cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchBar::search);
cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::toggle_search_option);
- cx.add_action(ProjectSearchBar::select_match);
+ cx.add_action(ProjectSearchBar::select_next_match);
+ cx.add_action(ProjectSearchBar::select_prev_match);
cx.add_action(ProjectSearchBar::toggle_focus);
cx.capture_action(ProjectSearchBar::tab);
}
@@ -67,6 +55,7 @@ pub struct ProjectSearchView {
regex: bool,
query_contains_error: bool,
active_match_index: Option<usize>,
+ results_editor_was_focused: bool,
}
pub struct ProjectSearchBar {
@@ -140,6 +129,7 @@ impl ProjectSearch {
pub enum ViewEvent {
UpdateTab,
+ EditorEvent(editor::Event),
}
impl Entity for ProjectSearchView {
@@ -181,10 +171,10 @@ impl View for ProjectSearchView {
.insert(self.model.read(cx).project.downgrade(), handle)
});
- if self.model.read(cx).match_ranges.is_empty() {
- cx.focus(&self.query_editor);
- } else {
+ if self.results_editor_was_focused && !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
+ } else {
+ cx.focus(&self.query_editor);
}
}
}
@@ -308,6 +298,14 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.navigate(data, cx))
}
+ fn should_activate_item_on_event(event: &Self::Event) -> bool {
+ if let ViewEvent::EditorEvent(editor_event) = event {
+ Editor::should_activate_item_on_event(editor_event)
+ } else {
+ false
+ }
+ }
+
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
matches!(event, ViewEvent::UpdateTab)
}
@@ -342,6 +340,15 @@ impl ProjectSearchView {
editor.set_text(query_text, cx);
editor
});
+ // Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&query_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+ cx.observe_focus(&query_editor, |this, _, _| {
+ this.results_editor_was_focused = false;
+ })
+ .detach();
let results_editor = cx.add_view(|cx| {
let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx);
@@ -350,10 +357,16 @@ impl ProjectSearchView {
});
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach();
+ cx.observe_focus(&results_editor, |this, _, _| {
+ this.results_editor_was_focused = true;
+ })
+ .detach();
cx.subscribe(&results_editor, |this, _, event, cx| {
if matches!(event, editor::Event::SelectionsChanged { .. }) {
this.update_match_index(cx);
}
+ // Reraise editor events for workspace item activation purposes
+ cx.emit(ViewEvent::EditorEvent(event.clone()));
})
.detach();
@@ -366,6 +379,7 @@ impl ProjectSearchView {
regex,
query_contains_error: false,
active_match_index: None,
+ results_editor_was_focused: false,
};
this.model_changed(false, cx);
this
@@ -394,6 +408,9 @@ impl ProjectSearchView {
if let Some(existing) = existing {
workspace.activate_item(&existing, cx);
+ existing.update(cx, |existing, cx| {
+ existing.focus_query_editor(cx);
+ });
} else {
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
workspace.add_item(
@@ -472,8 +489,11 @@ impl ProjectSearchView {
if reset_selections {
editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
}
- let theme = &cx.global::<Settings>().theme.search;
- editor.highlight_background::<Self>(match_ranges, theme.match_background, cx);
+ editor.highlight_background::<Self>(
+ match_ranges,
+ |theme| theme.search.match_background,
+ cx,
+ );
});
if self.query_editor.is_focused(cx) {
self.focus_results_editor(cx);
@@ -549,18 +569,23 @@ impl ProjectSearchBar {
}
}
- fn select_match(
- pane: &mut Pane,
- &SelectMatch(direction): &SelectMatch,
- cx: &mut ViewContext<Pane>,
- ) {
+ fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
- search_view.update(cx, |search_view, cx| {
- search_view.select_match(direction, cx);
- });
+ search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
} else {
cx.propagate_action();
}
@@ -639,7 +664,10 @@ impl ProjectSearchBar {
.with_style(style.container)
.boxed()
})
- .on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
+ .on_click(move |cx| match direction {
+ Direction::Prev => cx.dispatch_action(SelectPrevMatch),
+ Direction::Next => cx.dispatch_action(SelectNextMatch),
+ })
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -1,6 +1,6 @@
pub use buffer_search::BufferSearchBar;
use editor::{Anchor, MultiBufferSnapshot};
-use gpui::{action, MutableAppContext};
+use gpui::{actions, impl_internal_actions, MutableAppContext};
pub use project_search::{ProjectSearchBar, ProjectSearchView};
use std::{
cmp::{self, Ordering},
@@ -15,8 +15,11 @@ pub fn init(cx: &mut MutableAppContext) {
project_search::init(cx);
}
-action!(ToggleSearchOption, SearchOption);
-action!(SelectMatch, Direction);
+#[derive(Clone)]
+pub struct ToggleSearchOption(pub SearchOption);
+
+actions!(search, [SelectNextMatch, SelectPrevMatch]);
+impl_internal_actions!(search, [ToggleSearchOption]);
#[derive(Clone, Copy)]
pub enum SearchOption {
@@ -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-----
-"""
@@ -0,0 +1,24 @@
+[package]
+name = "settings"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/settings.rs"
+doctest = false
+
+[features]
+test-support = []
+
+[dependencies]
+assets = { path = "../assets" }
+collections = { path = "../collections" }
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+anyhow = "1.0.38"
+schemars = "0.8"
+serde = { version = "1", features = ["derive", "rc"] }
+serde_json = { version = "1.0.64", features = ["preserve_order"] }
+serde_path_to_error = "0.1.4"
+toml = "0.5"
@@ -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(())
+ }
+}
@@ -0,0 +1,262 @@
+mod keymap_file;
+
+use anyhow::Result;
+use gpui::font_cache::{FamilyId, FontCache};
+use schemars::{
+ gen::{SchemaGenerator, SchemaSettings},
+ schema::{
+ InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
+ },
+ JsonSchema,
+};
+use serde::Deserialize;
+use serde_json::Value;
+use std::{collections::HashMap, sync::Arc};
+use theme::{Theme, ThemeRegistry};
+use util::ResultExt as _;
+
+pub use keymap_file::KeymapFile;
+
+#[derive(Clone)]
+pub struct Settings {
+ pub buffer_font_family: FamilyId,
+ pub buffer_font_size: f32,
+ pub vim_mode: bool,
+ pub tab_size: u32,
+ pub soft_wrap: SoftWrap,
+ pub preferred_line_length: u32,
+ pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+ pub theme: Arc<Theme>,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct LanguageOverride {
+ pub tab_size: Option<u32>,
+ pub soft_wrap: Option<SoftWrap>,
+ pub preferred_line_length: Option<u32>,
+}
+
+#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+ None,
+ EditorWidth,
+ PreferredLineLength,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
+pub struct SettingsFileContent {
+ #[serde(default)]
+ pub buffer_font_family: Option<String>,
+ #[serde(default)]
+ pub buffer_font_size: Option<f32>,
+ #[serde(default)]
+ pub vim_mode: Option<bool>,
+ #[serde(flatten)]
+ pub editor: LanguageOverride,
+ #[serde(default)]
+ pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
+ #[serde(default)]
+ pub theme: Option<String>,
+}
+
+impl Settings {
+ pub fn new(
+ buffer_font_family: &str,
+ font_cache: &FontCache,
+ theme: Arc<Theme>,
+ ) -> Result<Self> {
+ Ok(Self {
+ buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
+ buffer_font_size: 15.,
+ vim_mode: false,
+ tab_size: 4,
+ soft_wrap: SoftWrap::None,
+ preferred_line_length: 80,
+ language_overrides: Default::default(),
+ theme,
+ })
+ }
+
+ pub fn file_json_schema(
+ theme_names: Vec<String>,
+ language_names: Vec<String>,
+ ) -> serde_json::Value {
+ let settings = SchemaSettings::draft07().with(|settings| {
+ settings.option_add_null_type = false;
+ });
+ let generator = SchemaGenerator::new(settings);
+ let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
+
+ // Construct theme names reference type
+ let theme_names = theme_names
+ .into_iter()
+ .map(|name| Value::String(name))
+ .collect();
+ let theme_names_schema = Schema::Object(SchemaObject {
+ instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
+ enum_values: Some(theme_names),
+ ..Default::default()
+ });
+ root_schema
+ .definitions
+ .insert("ThemeName".to_owned(), theme_names_schema);
+
+ // Construct language overrides reference type
+ let language_override_schema_reference = Schema::Object(SchemaObject {
+ reference: Some("#/definitions/LanguageOverride".to_owned()),
+ ..Default::default()
+ });
+ let language_overrides_properties = language_names
+ .into_iter()
+ .map(|name| {
+ (
+ name,
+ Schema::Object(SchemaObject {
+ subschemas: Some(Box::new(SubschemaValidation {
+ all_of: Some(vec![language_override_schema_reference.clone()]),
+ ..Default::default()
+ })),
+ ..Default::default()
+ }),
+ )
+ })
+ .collect();
+ let language_overrides_schema = Schema::Object(SchemaObject {
+ instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
+ object: Some(Box::new(ObjectValidation {
+ properties: language_overrides_properties,
+ ..Default::default()
+ })),
+ ..Default::default()
+ });
+ root_schema
+ .definitions
+ .insert("LanguageOverrides".to_owned(), language_overrides_schema);
+
+ // Modify theme property to use new theme reference type
+ let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
+ let language_overrides_schema_reference = Schema::Object(SchemaObject {
+ reference: Some("#/definitions/ThemeName".to_owned()),
+ ..Default::default()
+ });
+ settings_file_schema.properties.insert(
+ "theme".to_owned(),
+ Schema::Object(SchemaObject {
+ subschemas: Some(Box::new(SubschemaValidation {
+ all_of: Some(vec![language_overrides_schema_reference]),
+ ..Default::default()
+ })),
+ ..Default::default()
+ }),
+ );
+
+ // Modify language_overrides property to use LanguageOverrides reference
+ settings_file_schema.properties.insert(
+ "language_overrides".to_owned(),
+ Schema::Object(SchemaObject {
+ reference: Some("#/definitions/LanguageOverrides".to_owned()),
+ ..Default::default()
+ }),
+ );
+ serde_json::to_value(root_schema).unwrap()
+ }
+
+ pub fn with_overrides(
+ mut self,
+ language_name: impl Into<Arc<str>>,
+ overrides: LanguageOverride,
+ ) -> Self {
+ self.language_overrides
+ .insert(language_name.into(), overrides);
+ self
+ }
+
+ pub fn tab_size(&self, language: Option<&str>) -> u32 {
+ language
+ .and_then(|language| self.language_overrides.get(language))
+ .and_then(|settings| settings.tab_size)
+ .unwrap_or(self.tab_size)
+ }
+
+ pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
+ language
+ .and_then(|language| self.language_overrides.get(language))
+ .and_then(|settings| settings.soft_wrap)
+ .unwrap_or(self.soft_wrap)
+ }
+
+ pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
+ language
+ .and_then(|language| self.language_overrides.get(language))
+ .and_then(|settings| settings.preferred_line_length)
+ .unwrap_or(self.preferred_line_length)
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(cx: &gpui::AppContext) -> Settings {
+ Settings {
+ buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
+ buffer_font_size: 14.,
+ vim_mode: false,
+ tab_size: 4,
+ soft_wrap: SoftWrap::None,
+ preferred_line_length: 80,
+ language_overrides: Default::default(),
+ theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
+ }
+ }
+
+ pub fn merge(
+ &mut self,
+ data: &SettingsFileContent,
+ theme_registry: &ThemeRegistry,
+ font_cache: &FontCache,
+ ) {
+ if let Some(value) = &data.buffer_font_family {
+ if let Some(id) = font_cache.load_family(&[value]).log_err() {
+ self.buffer_font_family = id;
+ }
+ }
+ if let Some(value) = &data.theme {
+ if let Some(theme) = theme_registry.get(&value.to_string()).log_err() {
+ self.theme = theme;
+ }
+ }
+
+ merge(&mut self.buffer_font_size, data.buffer_font_size);
+ merge(&mut self.vim_mode, data.vim_mode);
+ merge(&mut self.soft_wrap, data.editor.soft_wrap);
+ merge(&mut self.tab_size, data.editor.tab_size);
+ merge(
+ &mut self.preferred_line_length,
+ data.editor.preferred_line_length,
+ );
+
+ for (language_name, settings) in data.language_overrides.clone().into_iter() {
+ let target = self
+ .language_overrides
+ .entry(language_name.into())
+ .or_default();
+
+ merge_option(&mut target.tab_size, settings.tab_size);
+ merge_option(&mut target.soft_wrap, settings.soft_wrap);
+ merge_option(
+ &mut target.preferred_line_length,
+ settings.preferred_line_length,
+ );
+ }
+ }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+ if let Some(value) = value {
+ *target = value;
+ }
+}
+
+fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
+ if value.is_some() {
+ *target = value;
+ }
+}
@@ -9,7 +9,7 @@ doctest = false
[dependencies]
arrayvec = "0.7.1"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
[dev-dependencies]
ctor = "0.1"
@@ -17,7 +17,7 @@ sum_tree = { path = "../sum_tree" }
anyhow = "1.0.38"
arrayvec = "0.7.1"
lazy_static = "1.4"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11"
postage = { version = "0.4.1", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
@@ -826,6 +826,8 @@ impl Buffer {
edit.timestamp,
);
self.snapshot.version.observe(edit.timestamp.local());
+ self.local_clock.observe(edit.timestamp.local());
+ self.lamport_clock.observe(edit.timestamp.lamport());
self.resolve_edit(edit.timestamp.local());
}
}
@@ -836,6 +838,7 @@ impl Buffer {
if !self.version.observed(undo.id) {
self.apply_undo(&undo)?;
self.snapshot.version.observe(undo.id);
+ self.local_clock.observe(undo.id);
self.lamport_clock.observe(lamport_timestamp);
}
}
@@ -1033,8 +1036,6 @@ impl Buffer {
self.snapshot.visible_text = visible_text;
self.snapshot.deleted_text = deleted_text;
self.snapshot.insertions.edit(new_insertions, &());
- self.local_clock.observe(timestamp.local());
- self.lamport_clock.observe(timestamp.lamport());
self.subscriptions.publish_mut(&edits);
}
@@ -1,497 +0,0 @@
-use anyhow::{anyhow, Result};
-use indexmap::IndexMap;
-use serde_json::Value;
-use std::{
- cell::RefCell,
- mem,
- rc::{Rc, Weak},
-};
-
-pub fn resolve_references(value: Value) -> Result<Value> {
- let tree = Tree::from_json(value)?;
- tree.resolve()?;
- tree.to_json()
-}
-
-#[derive(Clone)]
-enum Node {
- Reference {
- path: String,
- parent: Option<Weak<RefCell<Node>>>,
- },
- Object {
- base: Option<String>,
- children: IndexMap<String, Tree>,
- resolved: bool,
- parent: Option<Weak<RefCell<Node>>>,
- },
- Array {
- children: Vec<Tree>,
- resolved: bool,
- parent: Option<Weak<RefCell<Node>>>,
- },
- String {
- value: String,
- parent: Option<Weak<RefCell<Node>>>,
- },
- Number {
- value: serde_json::Number,
- parent: Option<Weak<RefCell<Node>>>,
- },
- Bool {
- value: bool,
- parent: Option<Weak<RefCell<Node>>>,
- },
- Null {
- parent: Option<Weak<RefCell<Node>>>,
- },
-}
-
-#[derive(Clone)]
-struct Tree(Rc<RefCell<Node>>);
-
-impl Tree {
- pub fn new(node: Node) -> Self {
- Self(Rc::new(RefCell::new(node)))
- }
-
- fn from_json(value: Value) -> Result<Self> {
- match value {
- Value::String(value) => {
- if let Some(path) = value.strip_prefix("$") {
- Ok(Self::new(Node::Reference {
- path: path.to_string(),
- parent: None,
- }))
- } else {
- Ok(Self::new(Node::String {
- value,
- parent: None,
- }))
- }
- }
- Value::Number(value) => Ok(Self::new(Node::Number {
- value,
- parent: None,
- })),
- Value::Bool(value) => Ok(Self::new(Node::Bool {
- value,
- parent: None,
- })),
- Value::Null => Ok(Self::new(Node::Null { parent: None })),
- Value::Object(object) => {
- let tree = Self::new(Node::Object {
- base: Default::default(),
- children: Default::default(),
- resolved: false,
- parent: None,
- });
- let mut children = IndexMap::new();
- let mut resolved = true;
- let mut base = None;
- for (key, value) in object.into_iter() {
- let value = if key == "extends" {
- if value.is_string() {
- if let Value::String(value) = value {
- base = value.strip_prefix("$").map(str::to_string);
- resolved = false;
- Self::new(Node::String {
- value,
- parent: None,
- })
- } else {
- unreachable!()
- }
- } else {
- Tree::from_json(value)?
- }
- } else {
- Tree::from_json(value)?
- };
- value
- .0
- .borrow_mut()
- .set_parent(Some(Rc::downgrade(&tree.0)));
- resolved &= value.is_resolved();
- children.insert(key.clone(), value);
- }
-
- *tree.0.borrow_mut() = Node::Object {
- base,
- children,
- resolved,
- parent: None,
- };
- Ok(tree)
- }
- Value::Array(elements) => {
- let tree = Self::new(Node::Array {
- children: Default::default(),
- resolved: false,
- parent: None,
- });
-
- let mut children = Vec::new();
- let mut resolved = true;
- for element in elements {
- let child = Tree::from_json(element)?;
- child
- .0
- .borrow_mut()
- .set_parent(Some(Rc::downgrade(&tree.0)));
- resolved &= child.is_resolved();
- children.push(child);
- }
-
- *tree.0.borrow_mut() = Node::Array {
- children,
- resolved,
- parent: None,
- };
- Ok(tree)
- }
- }
- }
-
- fn to_json(&self) -> Result<Value> {
- match &*self.0.borrow() {
- Node::Reference { .. } => Err(anyhow!("unresolved tree")),
- Node::String { value, .. } => Ok(Value::String(value.clone())),
- Node::Number { value, .. } => Ok(Value::Number(value.clone())),
- Node::Bool { value, .. } => Ok(Value::Bool(*value)),
- Node::Null { .. } => Ok(Value::Null),
- Node::Object { children, .. } => {
- let mut json_children = serde_json::Map::new();
- for (key, value) in children {
- json_children.insert(key.clone(), value.to_json()?);
- }
- Ok(Value::Object(json_children))
- }
- Node::Array { children, .. } => {
- let mut json_children = Vec::new();
- for child in children {
- json_children.push(child.to_json()?);
- }
- Ok(Value::Array(json_children))
- }
- }
- }
-
- fn parent(&self) -> Option<Tree> {
- match &*self.0.borrow() {
- Node::Reference { parent, .. }
- | Node::Object { parent, .. }
- | Node::Array { parent, .. }
- | Node::String { parent, .. }
- | Node::Number { parent, .. }
- | Node::Bool { parent, .. }
- | Node::Null { parent } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree),
- }
- }
-
- fn get(&self, path: &str) -> Result<Option<Tree>> {
- let mut tree = self.clone();
- for component in path.split('.') {
- let node = tree.0.borrow();
- match &*node {
- Node::Object { children, .. } => {
- if let Some(subtree) = children.get(component).cloned() {
- drop(node);
- tree = subtree;
- } else {
- return Err(anyhow!(
- "key \"{}\" does not exist in path \"{}\"",
- component,
- path
- ));
- }
- }
- Node::Reference { .. } => return Ok(None),
- Node::Array { .. }
- | Node::String { .. }
- | Node::Number { .. }
- | Node::Bool { .. }
- | Node::Null { .. } => {
- return Err(anyhow!(
- "key \"{}\" in path \"{}\" is not an object",
- component,
- path
- ))
- }
- }
- }
-
- Ok(Some(tree))
- }
-
- fn is_resolved(&self) -> bool {
- match &*self.0.borrow() {
- Node::Reference { .. } => false,
- Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved,
- Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
- true
- }
- }
- }
-
- fn update_resolved(&self) {
- match &mut *self.0.borrow_mut() {
- Node::Object {
- resolved,
- base,
- children,
- ..
- } => {
- *resolved = base.is_none() && children.values().all(|c| c.is_resolved());
- }
- Node::Array {
- resolved, children, ..
- } => {
- *resolved = children.iter().all(|c| c.is_resolved());
- }
- _ => {}
- }
- }
-
- pub fn resolve(&self) -> Result<()> {
- let mut unresolved = vec![self.clone()];
- let mut made_progress = true;
-
- while made_progress && !unresolved.is_empty() {
- made_progress = false;
- for mut tree in mem::take(&mut unresolved) {
- made_progress |= tree.resolve_subtree(self, &mut unresolved)?;
- if tree.is_resolved() {
- while let Some(parent) = tree.parent() {
- parent.update_resolved();
- if !parent.is_resolved() {
- break;
- }
- tree = parent;
- }
- }
- }
- }
-
- if unresolved.is_empty() {
- Ok(())
- } else {
- Err(anyhow!("tree contains cycles"))
- }
- }
-
- fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec<Tree>) -> Result<bool> {
- let node = self.0.borrow();
- match &*node {
- Node::Reference { path, parent } => {
- if let Some(subtree) = root.get(&path)? {
- if subtree.is_resolved() {
- let parent = parent.clone();
- drop(node);
- let mut new_node = subtree.0.borrow().clone();
- new_node.set_parent(parent);
- *self.0.borrow_mut() = new_node;
- Ok(true)
- } else {
- unresolved.push(self.clone());
- Ok(false)
- }
- } else {
- unresolved.push(self.clone());
- Ok(false)
- }
- }
- Node::Object {
- base,
- children,
- resolved,
- ..
- } => {
- if *resolved {
- Ok(false)
- } else {
- let mut made_progress = false;
- let mut children_resolved = true;
- for child in children.values() {
- made_progress |= child.resolve_subtree(root, unresolved)?;
- children_resolved &= child.is_resolved();
- }
-
- if children_resolved {
- let mut has_base = false;
- let mut resolved_base = None;
- if let Some(base) = base {
- has_base = true;
- if let Some(base) = root.get(base)? {
- if base.is_resolved() {
- resolved_base = Some(base);
- }
- }
- }
-
- drop(node);
-
- if let Some(base) = resolved_base.as_ref() {
- self.extend_from(&base);
- made_progress = true;
- }
-
- if let Node::Object { resolved, base, .. } = &mut *self.0.borrow_mut() {
- if has_base {
- if resolved_base.is_some() {
- base.take();
- *resolved = true;
- } else {
- unresolved.push(self.clone());
- }
- } else {
- *resolved = true;
- }
- }
- } else if base.is_some() {
- unresolved.push(self.clone());
- }
-
- Ok(made_progress)
- }
- }
- Node::Array {
- children, resolved, ..
- } => {
- if *resolved {
- Ok(false)
- } else {
- let mut made_progress = false;
- let mut children_resolved = true;
- for child in children.iter() {
- made_progress |= child.resolve_subtree(root, unresolved)?;
- children_resolved &= child.is_resolved();
- }
-
- if children_resolved {
- drop(node);
-
- if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() {
- *resolved = true;
- }
- }
-
- Ok(made_progress)
- }
- }
- Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
- Ok(false)
- }
- }
- }
-
- fn extend_from(&self, base: &Tree) {
- if Rc::ptr_eq(&self.0, &base.0) {
- return;
- }
-
- if let (
- Node::Object { children, .. },
- Node::Object {
- children: base_children,
- ..
- },
- ) = (&mut *self.0.borrow_mut(), &*base.0.borrow())
- {
- for (key, base_value) in base_children {
- if let Some(value) = children.get(key) {
- value.extend_from(base_value);
- } else {
- let base_value = base_value.clone();
- base_value
- .0
- .borrow_mut()
- .set_parent(Some(Rc::downgrade(&self.0)));
- children.insert(key.clone(), base_value);
- }
- }
- }
- }
-}
-
-impl Node {
- fn set_parent(&mut self, new_parent: Option<Weak<RefCell<Node>>>) {
- match self {
- Node::Reference { parent, .. }
- | Node::Object { parent, .. }
- | Node::Array { parent, .. }
- | Node::String { parent, .. }
- | Node::Number { parent, .. }
- | Node::Bool { parent, .. }
- | Node::Null { parent } => *parent = new_parent,
- }
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_references() {
- let json = serde_json::json!({
- "a": {
- "extends": "$g",
- "x": "$b.d"
- },
- "b": {
- "c": "$a",
- "d": "$e.f"
- },
- "e": {
- "extends": "$a",
- "f": "1"
- },
- "g": {
- "h": 2
- }
- });
-
- assert_eq!(
- resolve_references(json).unwrap(),
- serde_json::json!({
- "a": {
- "extends": "$g",
- "x": "1",
- "h": 2
- },
- "b": {
- "c": {
- "extends": "$g",
- "x": "1",
- "h": 2
- },
- "d": "1"
- },
- "e": {
- "extends": "$a",
- "f": "1",
- "x": "1",
- "h": 2
- },
- "g": {
- "h": 2
- }
- })
- )
- }
-
- #[test]
- fn test_cycles() {
- let json = serde_json::json!({
- "a": {
- "b": "$c.d"
- },
- "c": {
- "d": "$a.b",
- },
- });
-
- assert!(resolve_references(json).is_err());
- }
-}
@@ -1,4 +1,3 @@
-mod resolution;
mod theme_registry;
use gpui::{
@@ -12,7 +11,7 @@ use std::{collections::HashMap, sync::Arc};
pub use theme_registry::*;
-pub const DEFAULT_THEME_NAME: &'static str = "black";
+pub const DEFAULT_THEME_NAME: &'static str = "dark";
#[derive(Deserialize, Default)]
pub struct Theme {
@@ -22,6 +21,7 @@ pub struct Theme {
pub chat_panel: ChatPanel,
pub contacts_panel: ContactsPanel,
pub project_panel: ProjectPanel,
+ pub command_palette: CommandPalette,
pub selector: Selector,
pub editor: Editor,
pub search: Search,
@@ -190,6 +190,12 @@ pub struct ProjectPanelEntry {
pub icon_spacing: f32,
}
+#[derive(Debug, Deserialize, Default)]
+pub struct CommandPalette {
+ pub key: ContainedLabel,
+ pub keystroke_spacing: f32,
+}
+
#[derive(Deserialize, Default)]
pub struct ContactsPanel {
#[serde(flatten)]
@@ -262,7 +268,7 @@ pub struct ContainedText {
pub text: TextStyle,
}
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default)]
pub struct ContainedLabel {
#[serde(flatten)]
pub container: ContainerStyle,
@@ -1,8 +1,8 @@
-use crate::{resolution::resolve_references, Theme};
+use crate::Theme;
use anyhow::{Context, Result};
use gpui::{fonts, AssetSource, FontCache};
use parking_lot::Mutex;
-use serde_json::{Map, Value};
+use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
pub struct ThemeRegistry {
@@ -25,12 +25,8 @@ impl ThemeRegistry {
pub fn list(&self) -> impl Iterator<Item = String> {
self.assets.list("themes/").into_iter().filter_map(|path| {
let filename = path.strip_prefix("themes/")?;
- let theme_name = filename.strip_suffix(".toml")?;
- if theme_name.starts_with('_') {
- None
- } else {
- Some(theme_name.to_string())
- }
+ let theme_name = filename.strip_suffix(".json")?;
+ Some(theme_name.to_string())
})
}
@@ -44,9 +40,14 @@ impl ThemeRegistry {
return Ok(theme.clone());
}
- let theme_data = self.load(name, true)?;
+ let asset_path = format!("themes/{}.json", name);
+ let theme_json = self
+ .assets
+ .load(&asset_path)
+ .with_context(|| format!("failed to load theme file {}", asset_path))?;
+
let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || {
- serde_path_to_error::deserialize(theme_data.as_ref())
+ serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(&theme_json))
})?;
theme.name = name.into();
@@ -54,217 +55,4 @@ impl ThemeRegistry {
self.themes.lock().insert(name.to_string(), theme.clone());
Ok(theme)
}
-
- fn load(&self, name: &str, evaluate_references: bool) -> Result<Arc<Value>> {
- if let Some(data) = self.theme_data.lock().get(name) {
- return Ok(data.clone());
- }
-
- let asset_path = format!("themes/{}.toml", name);
- let source_code = self
- .assets
- .load(&asset_path)
- .with_context(|| format!("failed to load theme file {}", asset_path))?;
-
- let mut theme_data: Map<String, Value> = toml::from_slice(source_code.as_ref())
- .with_context(|| format!("failed to parse {}.toml", name))?;
-
- // If this theme extends another base theme, deeply merge it into the base theme's data
- if let Some(base_name) = theme_data
- .get("extends")
- .and_then(|name| name.as_str())
- .map(str::to_string)
- {
- let base_theme_data = self
- .load(&base_name, false)
- .with_context(|| format!("failed to load base theme {}", base_name))?
- .as_ref()
- .clone();
- if let Value::Object(mut base_theme_object) = base_theme_data {
- deep_merge_json(&mut base_theme_object, theme_data);
- theme_data = base_theme_object;
- }
- }
-
- let mut theme_data = Value::Object(theme_data);
-
- // Find all of the key path references in the object, and then sort them according
- // to their dependencies.
- if evaluate_references {
- theme_data = resolve_references(theme_data)?;
- }
-
- let result = Arc::new(theme_data);
- self.theme_data
- .lock()
- .insert(name.to_string(), result.clone());
-
- Ok(result)
- }
-}
-
-fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
- for (key, extension_value) in extension {
- if let Value::Object(extension_object) = extension_value {
- if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) {
- deep_merge_json(base_object, extension_object);
- } else {
- base.insert(key, Value::Object(extension_object));
- }
- } else {
- base.insert(key, extension_value);
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use anyhow::anyhow;
- use gpui::MutableAppContext;
-
- #[gpui::test]
- fn test_theme_extension(cx: &mut MutableAppContext) {
- let assets = TestAssets(&[
- (
- "themes/_base.toml",
- r##"
- [ui.active_tab]
- extends = "$ui.tab"
- border.color = "#666666"
- text = "$text_colors.bright"
-
- [ui.tab]
- extends = "$ui.element"
- text = "$text_colors.dull"
-
- [ui.element]
- background = "#111111"
- border = {width = 2.0, color = "#00000000"}
-
- [editor]
- background = "#222222"
- default_text = "$text_colors.regular"
- "##,
- ),
- (
- "themes/light.toml",
- r##"
- extends = "_base"
-
- [text_colors]
- bright = "#ffffff"
- regular = "#eeeeee"
- dull = "#dddddd"
-
- [editor]
- background = "#232323"
- "##,
- ),
- ]);
-
- let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
- let theme_data = registry.load("light", true).unwrap();
-
- assert_eq!(
- theme_data.as_ref(),
- &serde_json::json!({
- "ui": {
- "active_tab": {
- "background": "#111111",
- "border": {
- "width": 2.0,
- "color": "#666666"
- },
- "extends": "$ui.tab",
- "text": "#ffffff"
- },
- "tab": {
- "background": "#111111",
- "border": {
- "width": 2.0,
- "color": "#00000000"
- },
- "extends": "$ui.element",
- "text": "#dddddd"
- },
- "element": {
- "background": "#111111",
- "border": {
- "width": 2.0,
- "color": "#00000000"
- }
- }
- },
- "editor": {
- "background": "#232323",
- "default_text": "#eeeeee"
- },
- "extends": "_base",
- "text_colors": {
- "bright": "#ffffff",
- "regular": "#eeeeee",
- "dull": "#dddddd"
- }
- })
- );
- }
-
- #[gpui::test]
- fn test_nested_extension(cx: &mut MutableAppContext) {
- let assets = TestAssets(&[(
- "themes/theme.toml",
- r##"
- [a]
- text = { extends = "$text.0" }
-
- [b]
- extends = "$a"
- text = { extends = "$text.1" }
-
- [text]
- 0 = { color = "red" }
- 1 = { color = "blue" }
- "##,
- )]);
-
- let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
- let theme_data = registry.load("theme", true).unwrap();
- assert_eq!(
- theme_data
- .get("b")
- .unwrap()
- .get("text")
- .unwrap()
- .get("color")
- .unwrap(),
- "blue"
- );
- }
-
- struct TestAssets(&'static [(&'static str, &'static str)]);
-
- impl AssetSource for TestAssets {
- fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
- if let Some(row) = self.0.iter().find(|e| e.0 == path) {
- Ok(row.1.as_bytes().into())
- } else {
- Err(anyhow!("no such path {}", path))
- }
- }
-
- fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
- self.0
- .iter()
- .copied()
- .filter_map(|(path, _)| {
- if path.starts_with(prefix) {
- Some(path.into())
- } else {
- None
- }
- })
- .collect()
- }
- }
}
@@ -11,9 +11,11 @@ doctest = false
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
+picker = { path = "../picker" }
theme = { path = "../theme" }
+settings = { path = "../settings" }
workspace = { path = "../workspace" }
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
smol = "1.2.5"
@@ -1,44 +1,30 @@
-use editor::Editor;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
- action,
- elements::*,
- keymap::{self, Binding},
- AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
- ViewContext, ViewHandle,
+ actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext,
+ RenderContext, View, ViewContext, ViewHandle,
};
-use std::{cmp, sync::Arc};
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
+use std::sync::Arc;
use theme::{Theme, ThemeRegistry};
-use workspace::{
- menu::{Confirm, SelectNext, SelectPrev},
- Settings, Workspace,
-};
+use workspace::Workspace;
pub struct ThemeSelector {
- themes: Arc<ThemeRegistry>,
+ registry: Arc<ThemeRegistry>,
+ theme_names: Vec<String>,
matches: Vec<StringMatch>,
- query_editor: ViewHandle<Editor>,
- list_state: UniformListState,
- selected_index: usize,
original_theme: Arc<Theme>,
+ picker: ViewHandle<Picker<Self>>,
selection_completed: bool,
+ selected_index: usize,
}
-action!(Toggle, Arc<ThemeRegistry>);
-action!(Reload, Arc<ThemeRegistry>);
+actions!(theme_selector, [Toggle, Reload]);
-pub fn init(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
- cx.add_action(ThemeSelector::confirm);
- cx.add_action(ThemeSelector::select_prev);
- cx.add_action(ThemeSelector::select_next);
+pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ThemeSelector::toggle);
cx.add_action(ThemeSelector::reload);
-
- cx.add_bindings(vec![
- Binding::new("cmd-k cmd-t", Toggle(themes.clone()), None),
- Binding::new("cmd-k t", Reload(themes.clone()), None),
- Binding::new("escape", Toggle(themes.clone()), Some("ThemeSelector")),
- ]);
+ Picker::<ThemeSelector>::init(cx);
}
pub enum Event {
@@ -47,44 +33,46 @@ pub enum Event {
impl ThemeSelector {
fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
- let query_editor = cx.add_view(|cx| {
- Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
- });
-
- cx.subscribe(&query_editor, Self::on_query_editor_event)
- .detach();
-
+ let handle = cx.weak_handle();
+ let picker = cx.add_view(|cx| Picker::new(handle, cx));
let original_theme = cx.global::<Settings>().theme.clone();
-
+ let theme_names = registry.list().collect::<Vec<_>>();
+ let matches = theme_names
+ .iter()
+ .map(|name| StringMatch {
+ candidate_id: 0,
+ score: 0.0,
+ positions: Default::default(),
+ string: name.clone(),
+ })
+ .collect();
let mut this = Self {
- themes: registry,
- query_editor,
- matches: Vec::new(),
- list_state: Default::default(),
- selected_index: 0, // Default index for now
+ registry,
+ theme_names,
+ matches,
+ picker,
original_theme: original_theme.clone(),
+ selected_index: 0,
selection_completed: false,
};
- this.update_matches(cx);
-
- // Set selected index to current theme
this.select_if_matching(&original_theme.name);
-
this
}
- fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
+ fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+ let themes = workspace.themes();
workspace.toggle_modal(cx, |cx, _| {
- let selector = cx.add_view(|cx| Self::new(action.0.clone(), cx));
- cx.subscribe(&selector, Self::on_event).detach();
- selector
+ let this = cx.add_view(|cx| Self::new(themes, cx));
+ cx.subscribe(&this, Self::on_event).detach();
+ this
});
}
- fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
+ fn reload(workspace: &mut Workspace, _: &Reload, cx: &mut ViewContext<Workspace>) {
let current_theme_name = cx.global::<Settings>().theme.name.clone();
- action.0.clear();
- match action.0.get(¤t_theme_name) {
+ let themes = workspace.themes();
+ themes.clear();
+ match themes.get(¤t_theme_name) {
Ok(theme) => {
Self::set_theme(theme, cx);
log::info!("reloaded theme {}", current_theme_name);
@@ -95,36 +83,9 @@ impl ThemeSelector {
}
}
- fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
- self.selection_completed = true;
- cx.emit(Event::Dismissed);
- }
-
- fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
- if self.selected_index > 0 {
- self.selected_index -= 1;
- }
- self.list_state
- .scroll_to(ScrollTarget::Show(self.selected_index));
-
- self.show_selected_theme(cx);
- cx.notify();
- }
-
- fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
- if self.selected_index + 1 < self.matches.len() {
- self.selected_index += 1;
- }
- self.list_state
- .scroll_to(ScrollTarget::Show(self.selected_index));
-
- self.show_selected_theme(cx);
- cx.notify();
- }
-
fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
if let Some(mat) = self.matches.get(self.selected_index) {
- match self.themes.get(&mat.string) {
+ match self.registry.get(&mat.string) {
Ok(theme) => Self::set_theme(theme, cx),
Err(error) => {
log::error!("error loading theme {}: {}", mat.string, error)
@@ -141,49 +102,6 @@ impl ThemeSelector {
.unwrap_or(self.selected_index);
}
- fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
- let background = cx.background().clone();
- let candidates = self
- .themes
- .list()
- .enumerate()
- .map(|(id, name)| StringMatchCandidate {
- id,
- char_bag: name.as_str().into(),
- string: name,
- })
- .collect::<Vec<_>>();
- let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
-
- self.matches = if query.is_empty() {
- candidates
- .into_iter()
- .enumerate()
- .map(|(index, candidate)| StringMatch {
- candidate_id: index,
- string: candidate.string,
- positions: Vec::new(),
- score: 0.0,
- })
- .collect()
- } else {
- smol::block_on(match_strings(
- &candidates,
- &query,
- false,
- 100,
- &Default::default(),
- background,
- ))
- };
-
- self.selected_index = self
- .selected_index
- .min(self.matches.len().saturating_sub(1));
-
- cx.notify();
- }
-
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<ThemeSelector>,
@@ -197,89 +115,104 @@ impl ThemeSelector {
}
}
- fn on_query_editor_event(
- &mut self,
- _: ViewHandle<Editor>,
- event: &editor::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- editor::Event::BufferEdited { .. } => {
- self.update_matches(cx);
- self.select_if_matching(&cx.global::<Settings>().theme.name);
- self.show_selected_theme(cx);
- }
- editor::Event::Blurred => cx.emit(Event::Dismissed),
- _ => {}
- }
+ fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
+ cx.update_global::<Settings, _, _>(|settings, cx| {
+ settings.theme = theme;
+ cx.refresh_windows();
+ });
}
+}
- fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
- if self.matches.is_empty() {
- let settings = cx.global::<Settings>();
- return Container::new(
- Label::new(
- "No matches".into(),
- settings.theme.selector.empty.label.clone(),
- )
- .boxed(),
- )
- .with_style(settings.theme.selector.empty.container)
- .named("empty matches");
+impl PickerDelegate for ThemeSelector {
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+ self.selection_completed = true;
+ cx.emit(Event::Dismissed);
+ }
+
+ fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ if !self.selection_completed {
+ Self::set_theme(self.original_theme.clone(), cx);
+ self.selection_completed = true;
}
+ cx.emit(Event::Dismissed);
+ }
- let handle = cx.handle();
- let list =
- UniformList::new(
- self.list_state.clone(),
- self.matches.len(),
- move |mut range, items, cx| {
- let cx = cx.as_ref();
- let selector = handle.upgrade(cx).unwrap();
- let selector = selector.read(cx);
- let start = range.start;
- range.end = cmp::min(range.end, selector.matches.len());
- items.extend(selector.matches[range].iter().enumerate().map(
- move |(i, path_match)| selector.render_match(path_match, start + i, cx),
- ));
- },
- );
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
- Container::new(list.boxed())
- .with_margin_top(6.0)
- .named("matches")
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+ self.selected_index = ix;
+ self.show_selected_theme(cx);
}
- fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox {
- let settings = cx.global::<Settings>();
- let theme = &settings.theme;
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+ let background = cx.background().clone();
+ let candidates = self
+ .theme_names
+ .iter()
+ .enumerate()
+ .map(|(id, name)| StringMatchCandidate {
+ id,
+ char_bag: name.as_str().into(),
+ string: name.clone(),
+ })
+ .collect::<Vec<_>>();
- let container = Container::new(
- Label::new(
- theme_match.string.clone(),
- if index == self.selected_index {
- theme.selector.active_item.label.clone()
- } else {
- theme.selector.item.label.clone()
- },
- )
- .with_highlights(theme_match.positions.clone())
- .boxed(),
- )
- .with_style(if index == self.selected_index {
- theme.selector.active_item.container
- } else {
- theme.selector.item.container
- });
+ cx.spawn(|this, mut cx| async move {
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ match_strings(
+ &candidates,
+ &query,
+ false,
+ 100,
+ &Default::default(),
+ background,
+ )
+ .await
+ };
- container.boxed()
+ this.update(&mut cx, |this, cx| {
+ this.matches = matches;
+ this.selected_index = this
+ .selected_index
+ .min(this.matches.len().saturating_sub(1));
+ this.show_selected_theme(cx);
+ cx.notify();
+ });
+ })
}
- fn set_theme(theme: Arc<Theme>, cx: &mut MutableAppContext) {
- cx.update_global::<Settings, _, _>(|settings, cx| {
- settings.theme = theme;
- cx.refresh_windows();
- });
+ fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+ let settings = cx.global::<Settings>();
+ let theme = &settings.theme;
+ let theme_match = &self.matches[ix];
+ let style = if selected {
+ &theme.selector.active_item
+ } else {
+ &theme.selector.item
+ };
+
+ Label::new(theme_match.string.clone(), style.label.clone())
+ .with_highlights(theme_match.positions.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed()
}
}
@@ -298,43 +231,11 @@ impl View for ThemeSelector {
"ThemeSelector"
}
- fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let theme = cx.global::<Settings>().theme.clone();
- Align::new(
- ConstrainedBox::new(
- Container::new(
- Flex::new(Axis::Vertical)
- .with_child(
- ChildView::new(&self.query_editor)
- .contained()
- .with_style(theme.selector.input_editor.container)
- .boxed(),
- )
- .with_child(
- FlexItem::new(self.render_matches(cx))
- .flex(1., false)
- .boxed(),
- )
- .boxed(),
- )
- .with_style(theme.selector.container)
- .boxed(),
- )
- .with_max_width(600.0)
- .with_max_height(400.0)
- .boxed(),
- )
- .top()
- .named("theme selector")
+ fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ ChildView::new(self.picker.clone()).boxed()
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
- cx.focus(&self.query_editor);
- }
-
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
- let mut cx = Self::default_keymap_context();
- cx.set.insert("menu".into());
- cx
+ cx.focus(&self.picker);
}
}
@@ -12,7 +12,7 @@ test-support = ["rand", "serde_json", "tempdir"]
[dependencies]
anyhow = "1.0.38"
futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
rand = { version = "0.8", optional = true }
surf = "2.2"
tempdir = { version = "0.3.7", optional = true }
@@ -8,18 +8,22 @@ path = "src/vim.rs"
doctest = false
[dependencies]
+assets = { path = "../assets" }
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
+serde = { version = "1", features = ["derive"] }
+settings = { path = "../settings" }
workspace = { path = "../workspace" }
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
[dev-dependencies]
indoc = "1.0.4"
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
+settings = { path = "../settings" }
workspace = { path = "../workspace", features = ["test-support"] }
@@ -1,7 +1,7 @@
use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
use gpui::MutableAppContext;
-use crate::{mode::Mode, SwitchMode, VimState};
+use crate::{state::Mode, Vim};
pub fn init(cx: &mut MutableAppContext) {
cx.subscribe_global(editor_created).detach();
@@ -11,9 +11,9 @@ pub fn init(cx: &mut MutableAppContext) {
}
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
- cx.update_default_global(|vim_state: &mut VimState, cx| {
- vim_state.editors.insert(editor.id(), editor.downgrade());
- vim_state.sync_editor_options(cx);
+ cx.update_default_global(|vim: &mut Vim, cx| {
+ vim.editors.insert(editor.id(), editor.downgrade());
+ vim.sync_editor_options(cx);
})
}
@@ -21,17 +21,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
Mode::Insert
} else {
- Mode::normal()
+ Mode::Normal
};
- VimState::update_global(cx, |state, cx| {
+ Vim::update(cx, |state, cx| {
state.active_editor = Some(editor.downgrade());
- state.switch_mode(&SwitchMode(mode), cx);
+ state.switch_mode(mode, cx);
});
}
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
- VimState::update_global(cx, |state, cx| {
+ Vim::update(cx, |state, cx| {
if let Some(previous_editor) = state.active_editor.clone() {
if previous_editor == editor.clone() {
state.active_editor = None;
@@ -42,11 +42,11 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont
}
fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
- cx.update_default_global(|vim_state: &mut VimState, _| {
- vim_state.editors.remove(&editor.id());
- if let Some(previous_editor) = vim_state.active_editor.clone() {
+ cx.update_default_global(|vim: &mut Vim, _| {
+ vim.editors.remove(&editor.id());
+ if let Some(previous_editor) = vim.active_editor.clone() {
if previous_editor == editor.clone() {
- vim_state.active_editor = None;
+ vim.active_editor = None;
}
}
});
@@ -1,47 +1,40 @@
+use crate::{state::Mode, Vim};
use editor::Bias;
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use gpui::{actions, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
-use crate::{mode::Mode, SwitchMode, VimState};
-
-action!(NormalBefore);
+actions!(vim, [NormalBefore]);
pub fn init(cx: &mut MutableAppContext) {
- let context = Some("Editor && vim_mode == insert");
- cx.add_bindings(vec![
- Binding::new("escape", NormalBefore, context),
- Binding::new("ctrl-c", NormalBefore, context),
- ]);
-
cx.add_action(normal_before);
}
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
+ Vim::update(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
- state.switch_mode(&SwitchMode(Mode::normal()), cx);
+ state.switch_mode(Mode::Normal, cx);
})
}
#[cfg(test)]
mod test {
- use crate::{mode::Mode, vim_test_context::VimTestContext};
+ use crate::{state::Mode, vim_test_context::VimTestContext};
#[gpui::test]
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true, "").await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
- cx.simulate_keystrokes(&["T", "e", "s", "t"]);
+ cx.simulate_keystrokes(["T", "e", "s", "t"]);
cx.assert_editor_state("Test|");
cx.simulate_keystroke("escape");
- assert_eq!(cx.mode(), Mode::normal());
+ assert_eq!(cx.mode(), Mode::Normal);
cx.assert_editor_state("Tes|t");
}
}
@@ -1,72 +0,0 @@
-use editor::CursorShape;
-use gpui::keymap::Context;
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Mode {
- Normal(NormalState),
- Insert,
-}
-
-impl Mode {
- pub fn cursor_shape(&self) -> CursorShape {
- match self {
- Mode::Normal(_) => CursorShape::Block,
- Mode::Insert => CursorShape::Bar,
- }
- }
-
- pub fn keymap_context_layer(&self) -> Context {
- let mut context = Context::default();
- context.map.insert(
- "vim_mode".to_string(),
- match self {
- Self::Normal(_) => "normal",
- Self::Insert => "insert",
- }
- .to_string(),
- );
-
- match self {
- Self::Normal(normal_state) => normal_state.set_context(&mut context),
- _ => {}
- }
- context
- }
-
- pub fn normal() -> Mode {
- Mode::Normal(Default::default())
- }
-}
-
-impl Default for Mode {
- fn default() -> Self {
- Self::Normal(Default::default())
- }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum NormalState {
- None,
- GPrefix,
-}
-
-impl NormalState {
- pub fn set_context(&self, context: &mut Context) {
- let submode = match self {
- Self::GPrefix => Some("g"),
- _ => None,
- };
-
- if let Some(submode) = submode {
- context
- .map
- .insert("vim_submode".to_string(), submode.to_string());
- }
- }
-}
-
-impl Default for NormalState {
- fn default() -> Self {
- NormalState::None
- }
-}
@@ -0,0 +1,271 @@
+use editor::{
+ char_kind,
+ display_map::{DisplaySnapshot, ToDisplayPoint},
+ movement, Bias, DisplayPoint,
+};
+use gpui::{actions, impl_actions, MutableAppContext};
+use language::SelectionGoal;
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{
+ normal::normal_motion,
+ state::{Mode, Operator},
+ Vim,
+};
+
+#[derive(Copy, Clone)]
+pub enum Motion {
+ Left,
+ Down,
+ Up,
+ Right,
+ NextWordStart {
+ ignore_punctuation: bool,
+ stop_at_newline: bool,
+ },
+ NextWordEnd {
+ ignore_punctuation: bool,
+ },
+ PreviousWordStart {
+ ignore_punctuation: bool,
+ },
+ StartOfLine,
+ EndOfLine,
+ StartOfDocument,
+ EndOfDocument,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct NextWordStart {
+ #[serde(default)]
+ ignore_punctuation: bool,
+ #[serde(default)]
+ stop_at_newline: bool,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct NextWordEnd {
+ #[serde(default)]
+ ignore_punctuation: bool,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct PreviousWordStart {
+ #[serde(default)]
+ ignore_punctuation: bool,
+}
+
+actions!(
+ vim,
+ [
+ Left,
+ Down,
+ Up,
+ Right,
+ StartOfLine,
+ EndOfLine,
+ StartOfDocument,
+ EndOfDocument
+ ]
+);
+impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
+ cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
+ cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
+ cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
+ cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
+ cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
+ cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
+ motion(Motion::StartOfDocument, cx)
+ });
+ cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
+
+ cx.add_action(
+ |_: &mut Workspace,
+ &NextWordStart {
+ ignore_punctuation,
+ stop_at_newline,
+ }: &NextWordStart,
+ cx: _| {
+ motion(
+ Motion::NextWordStart {
+ ignore_punctuation,
+ stop_at_newline,
+ },
+ cx,
+ )
+ },
+ );
+ cx.add_action(
+ |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
+ motion(Motion::NextWordEnd { ignore_punctuation }, cx)
+ },
+ );
+ cx.add_action(
+ |_: &mut Workspace,
+ &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
+ cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
+ );
+}
+
+fn motion(motion: Motion, cx: &mut MutableAppContext) {
+ Vim::update(cx, |vim, cx| {
+ if let Some(Operator::Namespace(_)) = vim.active_operator() {
+ vim.pop_operator(cx);
+ }
+ });
+ match Vim::read(cx).state.mode {
+ Mode::Normal => normal_motion(motion, cx),
+ Mode::Insert => {
+ // Shouldn't execute a motion in insert mode. Ignoring
+ }
+ }
+}
+
+impl Motion {
+ pub fn move_point(
+ self,
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ goal: SelectionGoal,
+ block_cursor_positioning: bool,
+ ) -> (DisplayPoint, SelectionGoal) {
+ use Motion::*;
+ match self {
+ Left => (left(map, point), SelectionGoal::None),
+ Down => movement::down(map, point, goal),
+ Up => movement::up(map, point, goal),
+ Right => (right(map, point), SelectionGoal::None),
+ NextWordStart {
+ ignore_punctuation,
+ stop_at_newline,
+ } => (
+ next_word_start(map, point, ignore_punctuation, stop_at_newline),
+ SelectionGoal::None,
+ ),
+ NextWordEnd { ignore_punctuation } => (
+ next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
+ SelectionGoal::None,
+ ),
+ PreviousWordStart { ignore_punctuation } => (
+ previous_word_start(map, point, ignore_punctuation),
+ SelectionGoal::None,
+ ),
+ StartOfLine => (start_of_line(map, point), SelectionGoal::None),
+ EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+ StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
+ EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
+ }
+ }
+
+ pub fn line_wise(self) -> bool {
+ use Motion::*;
+ match self {
+ Down | Up | StartOfDocument | EndOfDocument => true,
+ _ => false,
+ }
+ }
+}
+
+fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ *point.column_mut() = point.column().saturating_sub(1);
+ map.clip_point(point, Bias::Left)
+}
+
+fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ *point.column_mut() += 1;
+ map.clip_point(point, Bias::Right)
+}
+
+fn next_word_start(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ ignore_punctuation: bool,
+ stop_at_newline: bool,
+) -> DisplayPoint {
+ let mut crossed_newline = false;
+ movement::find_boundary(map, point, |left, right| {
+ let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+ let at_newline = right == '\n';
+
+ let found = (left_kind != right_kind && !right.is_whitespace())
+ || (at_newline && (crossed_newline || stop_at_newline))
+ || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
+
+ if at_newline {
+ crossed_newline = true;
+ }
+ found
+ })
+}
+
+fn next_word_end(
+ map: &DisplaySnapshot,
+ mut point: DisplayPoint,
+ ignore_punctuation: bool,
+ before_end_character: bool,
+) -> DisplayPoint {
+ *point.column_mut() += 1;
+ point = movement::find_boundary(map, point, |left, right| {
+ let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+ left_kind != right_kind && !left.is_whitespace()
+ });
+ // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
+ // we have backtraced already
+ if before_end_character
+ && !map
+ .chars_at(point)
+ .skip(1)
+ .next()
+ .map(|c| c == '\n')
+ .unwrap_or(true)
+ {
+ *point.column_mut() = point.column().saturating_sub(1);
+ }
+ map.clip_point(point, Bias::Left)
+}
+
+fn previous_word_start(
+ map: &DisplaySnapshot,
+ mut point: DisplayPoint,
+ ignore_punctuation: bool,
+) -> DisplayPoint {
+ // This works even though find_preceding_boundary is called for every character in the line containing
+ // cursor because the newline is checked only once.
+ point = movement::find_preceding_boundary(map, point, |left, right| {
+ let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
+
+ (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
+ });
+ point
+}
+
+fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ map.prev_line_boundary(point.to_point(map)).1
+}
+
+fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
+}
+
+fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let mut new_point = 0usize.to_display_point(map);
+ *new_point.column_mut() = point.column();
+ map.clip_point(new_point, Bias::Left)
+}
+
+fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let mut new_point = map.max_point();
+ *new_point.column_mut() = point.column();
+ map.clip_point(new_point, Bias::Left)
+}
@@ -1,187 +1,100 @@
-mod g_prefix;
-
-use editor::{char_kind, movement, Bias};
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
+use crate::{
+ motion::Motion,
+ state::{Mode, Operator},
+ Vim,
+};
+use editor::Bias;
+use gpui::MutableAppContext;
use language::SelectionGoal;
-use workspace::Workspace;
-
-use crate::{mode::NormalState, Mode, SwitchMode, VimState};
-
-action!(GPrefix);
-action!(MoveLeft);
-action!(MoveDown);
-action!(MoveUp);
-action!(MoveRight);
-action!(MoveToStartOfLine);
-action!(MoveToEndOfLine);
-action!(MoveToEnd);
-action!(MoveToNextWordStart, bool);
-action!(MoveToNextWordEnd, bool);
-action!(MoveToPreviousWordStart, bool);
-
-pub fn init(cx: &mut MutableAppContext) {
- let context = Some("Editor && vim_mode == normal");
- cx.add_bindings(vec![
- Binding::new("i", SwitchMode(Mode::Insert), context),
- Binding::new("g", SwitchMode(Mode::Normal(NormalState::GPrefix)), context),
- Binding::new("h", MoveLeft, context),
- Binding::new("j", MoveDown, context),
- Binding::new("k", MoveUp, context),
- Binding::new("l", MoveRight, context),
- Binding::new("0", MoveToStartOfLine, context),
- Binding::new("shift-$", MoveToEndOfLine, context),
- Binding::new("shift-G", MoveToEnd, context),
- Binding::new("w", MoveToNextWordStart(false), context),
- Binding::new("shift-W", MoveToNextWordStart(true), context),
- Binding::new("e", MoveToNextWordEnd(false), context),
- Binding::new("shift-E", MoveToNextWordEnd(true), context),
- Binding::new("b", MoveToPreviousWordStart(false), context),
- Binding::new("shift-B", MoveToPreviousWordStart(true), context),
- ]);
- g_prefix::init(cx);
-
- cx.add_action(move_left);
- cx.add_action(move_down);
- cx.add_action(move_up);
- cx.add_action(move_right);
- cx.add_action(move_to_start_of_line);
- cx.add_action(move_to_end_of_line);
- cx.add_action(move_to_end);
- cx.add_action(move_to_next_word_start);
- cx.add_action(move_to_next_word_end);
- cx.add_action(move_to_previous_word_start);
-}
-
-fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, mut cursor, _| {
- *cursor.column_mut() = cursor.column().saturating_sub(1);
- (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
- });
- });
- })
-}
-
-fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, movement::down);
- });
- });
-}
-
-fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, movement::up);
- });
- });
-}
-fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, mut cursor, _| {
- *cursor.column_mut() += 1;
- (map.clip_point(cursor, Bias::Right), SelectionGoal::None)
- });
- });
+pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
+ Vim::update(cx, |vim, cx| {
+ match vim.state.operator_stack.pop() {
+ None => move_cursor(vim, motion, cx),
+ Some(Operator::Change) => change_over(vim, motion, cx),
+ Some(Operator::Delete) => delete_over(vim, motion, cx),
+ Some(Operator::Namespace(_)) => {
+ // Can't do anything for a namespace operator. Ignoring
+ }
+ }
+ vim.clear_operator(cx);
});
}
-fn move_to_start_of_line(
- _: &mut Workspace,
- _: &MoveToStartOfLine,
- cx: &mut ViewContext<Workspace>,
-) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, cursor, _| {
- (
- movement::line_beginning(map, cursor, false),
- SelectionGoal::None,
- )
- });
- });
+fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.move_cursors(cx, |map, cursor, goal| {
+ motion.move_point(map, cursor, goal, true)
+ })
});
}
-fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, cursor, _| {
- (
- map.clip_point(movement::line_end(map, cursor, false), Bias::Left),
- SelectionGoal::None,
- )
+fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ // Don't clip at line ends during change operation
+ editor.set_clip_at_line_ends(false, cx);
+ editor.move_selections(cx, |map, selection| {
+ let (head, goal) = motion.move_point(map, selection.head(), selection.goal, false);
+ selection.set_head(head, goal);
+
+ if motion.line_wise() {
+ selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
+ selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
+ }
});
+ editor.set_clip_at_line_ends(true, cx);
+ editor.insert(&"", cx);
});
});
+ vim.switch_mode(Mode::Insert, cx)
}
-fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left));
- });
- });
-}
-
-fn move_to_next_word_start(
- _: &mut Workspace,
- &MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart,
- cx: &mut ViewContext<Workspace>,
-) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, mut cursor, _| {
- let mut crossed_newline = false;
- cursor = movement::find_boundary(map, cursor, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
- let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
- let at_newline = right == '\n';
-
- let found = (left_kind != right_kind && !right.is_whitespace())
- || (at_newline && crossed_newline)
- || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
-
- if at_newline {
- crossed_newline = true;
+fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ // Use goal column to preserve previous position
+ editor.set_clip_at_line_ends(false, cx);
+ editor.move_selections(cx, |map, selection| {
+ let original_head = selection.head();
+ let (head, _) = motion.move_point(map, selection.head(), selection.goal, false);
+ // Set the goal column to the original position in order to fix it up
+ // after the deletion
+ selection.set_head(head, SelectionGoal::Column(original_head.column()));
+
+ if motion.line_wise() {
+ if selection.end.row() == map.max_point().row() {
+ // Delete previous line break since we are at the end of the document
+ if selection.start.row() > 0 {
+ *selection.start.row_mut() = selection.start.row().saturating_sub(1);
+ selection.start = map.clip_point(selection.start, Bias::Left);
+ selection.start =
+ map.next_line_boundary(selection.start.to_point(map)).1;
+ } else {
+ // Selection covers the whole document. Just delete to the start of the
+ // line.
+ selection.start =
+ map.prev_line_boundary(selection.start.to_point(map)).1;
+ }
+ selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
+ } else {
+ // Delete next line break so that we leave the previous line alone
+ selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
+ *selection.end.column_mut() = 0;
+ *selection.end.row_mut() += 1;
+ selection.end = map.clip_point(selection.end, Bias::Left);
}
- found
- });
- (cursor, SelectionGoal::None)
+ }
});
- });
- });
-}
-
-fn move_to_next_word_end(
- _: &mut Workspace,
- &MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd,
- cx: &mut ViewContext<Workspace>,
-) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, mut cursor, _| {
- *cursor.column_mut() += 1;
- cursor = movement::find_boundary(map, cursor, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
- let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
-
- left_kind != right_kind && !left.is_whitespace()
- });
- // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
- // we have backtraced already
- if !map
- .chars_at(cursor)
- .skip(1)
- .next()
- .map(|c| c == '\n')
- .unwrap_or(true)
- {
- *cursor.column_mut() = cursor.column().saturating_sub(1);
+ editor.insert(&"", cx);
+
+ // Fixup cursor position after the deletion
+ editor.set_clip_at_line_ends(true, cx);
+ editor.move_cursors(cx, |map, mut cursor, goal| {
+ if motion.line_wise() {
+ if let SelectionGoal::Column(column) = goal {
+ *cursor.column_mut() = column
+ }
}
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
@@ -189,34 +102,18 @@ fn move_to_next_word_end(
});
}
-fn move_to_previous_word_start(
- _: &mut Workspace,
- &MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart,
- cx: &mut ViewContext<Workspace>,
-) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_cursors(cx, |map, mut cursor, _| {
- // This works even though find_preceding_boundary is called for every character in the line containing
- // cursor because the newline is checked only once.
- cursor = movement::find_preceding_boundary(map, cursor, |left, right| {
- let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
- let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
-
- (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
- });
- (cursor, SelectionGoal::None)
- });
- });
- });
-}
-
#[cfg(test)]
mod test {
use indoc::indoc;
use util::test::marked_text;
- use crate::vim_test_context::VimTestContext;
+ use crate::{
+ state::{
+ Mode::{self, *},
+ Namespace, Operator,
+ },
+ vim_test_context::VimTestContext,
+ };
#[gpui::test]
async fn test_hjkl(cx: &mut gpui::TestAppContext) {
@@ -322,19 +219,22 @@ mod test {
#[gpui::test]
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
- let initial_content = indoc! {"
- The quick
+ let mut cx = VimTestContext::new(cx, true, "").await;
+
+ cx.set_state(
+ indoc! {"
+ The |quick
brown fox jumps
- over the lazy dog"};
- let mut cx = VimTestContext::new(cx, true, initial_content).await;
-
+ over the lazy dog"},
+ Mode::Normal,
+ );
cx.simulate_keystroke("shift-G");
cx.assert_editor_state(indoc! {"
The quick
brown fox jumps
- over the lazy do|g"});
+ over| the lazy dog"});
// Repeat the action doesn't move
cx.simulate_keystroke("shift-G");
@@ -342,7 +242,7 @@ mod test {
The quick
brown fox jumps
- over the lazy do|g"});
+ over| the lazy dog"});
}
#[gpui::test]
@@ -361,7 +261,7 @@ mod test {
}
// Reset and test ignoring punctuation
- cx.simulate_keystrokes(&["g", "g"]);
+ cx.simulate_keystrokes(["g", "g", "0"]);
let (_, cursor_offsets) = marked_text(indoc! {"
The |quick-brown
|
@@ -391,7 +291,7 @@ mod test {
}
// Reset and test ignoring punctuation
- cx.simulate_keystrokes(&["g", "g"]);
+ cx.simulate_keystrokes(["g", "g", "0"]);
let (_, cursor_offsets) = marked_text(indoc! {"
Th|e quick-brow|n
@@ -413,7 +313,7 @@ mod test {
|fox_jumps |over
|the"});
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
- cx.simulate_keystroke("shift-G");
+ cx.simulate_keystrokes(["shift-G", "shift-$"]);
for cursor_offset in cursor_offsets.into_iter().rev() {
cx.simulate_keystroke("b");
@@ -421,7 +321,7 @@ mod test {
}
// Reset and test ignoring punctuation
- cx.simulate_keystroke("shift-G");
+ cx.simulate_keystrokes(["shift-G", "shift-$"]);
let (_, cursor_offsets) = marked_text(indoc! {"
||The |quick-brown
|
@@ -433,4 +333,474 @@ mod test {
cx.assert_newest_selection_head_offset(cursor_offset);
}
}
+
+ #[gpui::test]
+ async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true, "").await;
+
+ // Can abort with escape to get back to normal mode
+ cx.simulate_keystroke("g");
+ assert_eq!(cx.mode(), Normal);
+ assert_eq!(
+ cx.active_operator(),
+ Some(Operator::Namespace(Namespace::G))
+ );
+ cx.simulate_keystroke("escape");
+ assert_eq!(cx.mode(), Normal);
+ assert_eq!(cx.active_operator(), None);
+ }
+
+ #[gpui::test]
+ async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true, "").await;
+
+ cx.set_state(
+ indoc! {"
+ The q|uick
+
+ brown fox jumps
+ over the lazy dog"},
+ Mode::Normal,
+ );
+
+ // Jump to the end to
+ cx.simulate_keystroke("shift-G");
+ cx.assert_editor_state(indoc! {"
+ The quick
+
+ brown fox jumps
+ over |the lazy dog"});
+
+ // Jump to the start
+ cx.simulate_keystrokes(["g", "g"]);
+ cx.assert_editor_state(indoc! {"
+ The q|uick
+
+ brown fox jumps
+ over the lazy dog"});
+ assert_eq!(cx.mode(), Normal);
+ assert_eq!(cx.active_operator(), None);
+
+ // Repeat action doesn't change
+ cx.simulate_keystrokes(["g", "g"]);
+ cx.assert_editor_state(indoc! {"
+ The q|uick
+
+ brown fox jumps
+ over the lazy dog"});
+ assert_eq!(cx.mode(), Normal);
+ assert_eq!(cx.active_operator(), None);
+ }
+
+ #[gpui::test]
+ async fn test_change(cx: &mut gpui::TestAppContext) {
+ fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+ cx.assert_binding(
+ ["c", motion],
+ initial_state,
+ Mode::Normal,
+ state_after,
+ Mode::Insert,
+ );
+ }
+ let cx = &mut VimTestContext::new(cx, true, "").await;
+ assert("h", "Te|st", "T|st", cx);
+ assert("l", "Te|st", "Te|t", cx);
+ assert("w", "|Test", "|", cx);
+ assert("w", "Te|st", "Te|", cx);
+ assert("w", "Te|st Test", "Te| Test", cx);
+ assert("e", "Te|st Test", "Te| Test", cx);
+ assert("b", "Te|st", "|st", cx);
+ assert("b", "Test Te|st", "Test |st", cx);
+ assert(
+ "w",
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ The quick
+ brown |
+ jumps over"},
+ cx,
+ );
+ assert(
+ "shift-W",
+ indoc! {"
+ The quick
+ brown |fox-fox
+ jumps over"},
+ indoc! {"
+ The quick
+ brown |
+ jumps over"},
+ cx,
+ );
+ assert(
+ "k",
+ indoc! {"
+ The quick
+ brown |fox"},
+ indoc! {"
+ |"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |"},
+ cx,
+ );
+ assert(
+ "shift-$",
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ The q|
+ brown fox"},
+ cx,
+ );
+ assert(
+ "0",
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |uick
+ brown fox"},
+ cx,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_delete(cx: &mut gpui::TestAppContext) {
+ fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+ cx.assert_binding(
+ ["d", motion],
+ initial_state,
+ Mode::Normal,
+ state_after,
+ Mode::Normal,
+ );
+ }
+ let cx = &mut VimTestContext::new(cx, true, "").await;
+ assert("h", "Te|st", "T|st", cx);
+ assert("l", "Te|st", "Te|t", cx);
+ assert("w", "|Test", "|", cx);
+ assert("w", "Te|st", "T|e", cx);
+ assert("w", "Te|st Test", "Te|Test", cx);
+ assert("e", "Te|st Test", "Te| Test", cx);
+ assert("b", "Te|st", "|st", cx);
+ assert("b", "Test Te|st", "Test |st", cx);
+ assert(
+ "w",
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ // Trailing space after cursor
+ indoc! {"
+ The quick
+ brown|
+ jumps over"},
+ cx,
+ );
+ assert(
+ "shift-W",
+ indoc! {"
+ The quick
+ brown |fox-fox
+ jumps over"},
+ // Trailing space after cursor
+ indoc! {"
+ The quick
+ brown|
+ jumps over"},
+ cx,
+ );
+ assert(
+ "shift-$",
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ The |q
+ brown fox"},
+ cx,
+ );
+ assert(
+ "0",
+ indoc! {"
+ The q|uick
+ brown fox"},
+ indoc! {"
+ |uick
+ brown fox"},
+ cx,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_linewise_delete(cx: &mut gpui::TestAppContext) {
+ fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+ cx.assert_binding(
+ ["d", motion],
+ initial_state,
+ Mode::Normal,
+ state_after,
+ Mode::Normal,
+ );
+ }
+ let cx = &mut VimTestContext::new(cx, true, "").await;
+ assert(
+ "k",
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ jumps |over"},
+ cx,
+ );
+ assert(
+ "k",
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The qu|ick"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ jumps| over"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over"},
+ indoc! {"
+ The q|uick"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over"},
+ indoc! {"
+ The q|uick"},
+ cx,
+ );
+ cx.assert_binding(
+ ["d", "g", "g"],
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ Mode::Normal,
+ indoc! {"
+ jumps| over
+ the lazy"},
+ Mode::Normal,
+ );
+ cx.assert_binding(
+ ["d", "g", "g"],
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ Mode::Normal,
+ "|",
+ Mode::Normal,
+ );
+ assert(
+ "shift-G",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ The q|uick"},
+ cx,
+ );
+ cx.assert_binding(
+ ["d", "g", "g"],
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over
+ the lazy"},
+ Mode::Normal,
+ indoc! {"
+ brown| fox
+ jumps over
+ the lazy"},
+ Mode::Normal,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_linewise_change(cx: &mut gpui::TestAppContext) {
+ fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
+ cx.assert_binding(
+ ["c", motion],
+ initial_state,
+ Mode::Normal,
+ state_after,
+ Mode::Insert,
+ );
+ }
+ let cx = &mut VimTestContext::new(cx, true, "").await;
+ assert(
+ "k",
+ indoc! {"
+ The quick
+ brown |fox
+ jumps over"},
+ indoc! {"
+ |
+ jumps over"},
+ cx,
+ );
+ assert(
+ "k",
+ indoc! {"
+ The quick
+ brown fox
+ jumps |over"},
+ indoc! {"
+ The quick
+ |"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over"},
+ indoc! {"
+ |
+ jumps over"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over"},
+ indoc! {"
+ The quick
+ |"},
+ cx,
+ );
+ assert(
+ "j",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over"},
+ indoc! {"
+ The quick
+ |"},
+ cx,
+ );
+ assert(
+ "shift-G",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ The quick
+ |"},
+ cx,
+ );
+ assert(
+ "shift-G",
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ indoc! {"
+ The quick
+ |"},
+ cx,
+ );
+ assert(
+ "shift-G",
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ |"},
+ cx,
+ );
+ cx.assert_binding(
+ ["c", "g", "g"],
+ indoc! {"
+ The quick
+ brown| fox
+ jumps over
+ the lazy"},
+ Mode::Normal,
+ indoc! {"
+ |
+ jumps over
+ the lazy"},
+ Mode::Insert,
+ );
+ cx.assert_binding(
+ ["c", "g", "g"],
+ indoc! {"
+ The quick
+ brown fox
+ jumps over
+ the l|azy"},
+ Mode::Normal,
+ "|",
+ Mode::Insert,
+ );
+ cx.assert_binding(
+ ["c", "g", "g"],
+ indoc! {"
+ The q|uick
+ brown fox
+ jumps over
+ the lazy"},
+ Mode::Normal,
+ indoc! {"
+ |
+ brown fox
+ jumps over
+ the lazy"},
+ Mode::Insert,
+ );
+ }
}
@@ -1,82 +0,0 @@
-use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
-use workspace::Workspace;
-
-use crate::{mode::Mode, SwitchMode, VimState};
-
-action!(MoveToStart);
-
-pub fn init(cx: &mut MutableAppContext) {
- let context = Some("Editor && vim_mode == normal && vim_submode == g");
- cx.add_bindings(vec![
- Binding::new("g", MoveToStart, context),
- Binding::new("escape", SwitchMode(Mode::normal()), context),
- ]);
-
- cx.add_action(move_to_start);
-}
-
-fn move_to_start(_: &mut Workspace, _: &MoveToStart, cx: &mut ViewContext<Workspace>) {
- VimState::update_global(cx, |state, cx| {
- state.update_active_editor(cx, |editor, cx| {
- editor.move_to_beginning(&editor::MoveToBeginning, cx);
- });
- state.switch_mode(&SwitchMode(Mode::normal()), cx);
- })
-}
-
-#[cfg(test)]
-mod test {
- use indoc::indoc;
-
- use crate::{
- mode::{Mode, NormalState},
- vim_test_context::VimTestContext,
- };
-
- #[gpui::test]
- async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
- let mut cx = VimTestContext::new(cx, true, "").await;
-
- // Can abort with escape to get back to normal mode
- cx.simulate_keystroke("g");
- assert_eq!(cx.mode(), Mode::Normal(NormalState::GPrefix));
- cx.simulate_keystroke("escape");
- assert_eq!(cx.mode(), Mode::normal());
- }
-
- #[gpui::test]
- async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
- let initial_content = indoc! {"
- The quick
-
- brown fox jumps
- over the lazy dog"};
- let mut cx = VimTestContext::new(cx, true, initial_content).await;
-
- // Jump to the end to
- cx.simulate_keystroke("shift-G");
- cx.assert_editor_state(indoc! {"
- The quick
-
- brown fox jumps
- over the lazy do|g"});
-
- // Jump to the start
- cx.simulate_keystrokes(&["g", "g"]);
- cx.assert_editor_state(indoc! {"
- |The quick
-
- brown fox jumps
- over the lazy dog"});
- assert_eq!(cx.mode(), Mode::normal());
-
- // Repeat action doesn't change
- cx.simulate_keystrokes(&["g", "g"]);
- cx.assert_editor_state(indoc! {"
- |The quick
-
- brown fox jumps
- over the lazy dog"});
- assert_eq!(cx.mode(), Mode::normal());
- }
-}
@@ -0,0 +1,82 @@
+use editor::CursorShape;
+use gpui::keymap::Context;
+use serde::Deserialize;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
+pub enum Mode {
+ Normal,
+ Insert,
+}
+
+impl Default for Mode {
+ fn default() -> Self {
+ Self::Normal
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+pub enum Namespace {
+ G,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
+pub enum Operator {
+ Namespace(Namespace),
+ Change,
+ Delete,
+}
+
+#[derive(Default)]
+pub struct VimState {
+ pub mode: Mode,
+ pub operator_stack: Vec<Operator>,
+}
+
+impl VimState {
+ pub fn cursor_shape(&self) -> CursorShape {
+ match self.mode {
+ Mode::Normal => CursorShape::Block,
+ Mode::Insert => CursorShape::Bar,
+ }
+ }
+
+ pub fn vim_controlled(&self) -> bool {
+ !matches!(self.mode, Mode::Insert)
+ }
+
+ pub fn keymap_context_layer(&self) -> Context {
+ let mut context = Context::default();
+ context.map.insert(
+ "vim_mode".to_string(),
+ match self.mode {
+ Mode::Normal => "normal",
+ Mode::Insert => "insert",
+ }
+ .to_string(),
+ );
+
+ if self.vim_controlled() {
+ context.set.insert("VimControl".to_string());
+ }
+
+ if let Some(operator) = &self.operator_stack.last() {
+ operator.set_context(&mut context);
+ }
+ context
+ }
+}
+
+impl Operator {
+ pub fn set_context(&self, context: &mut Context) {
+ let operator_context = match self {
+ Operator::Namespace(Namespace::G) => "g",
+ Operator::Change => "c",
+ Operator::Delete => "d",
+ }
+ .to_owned();
+
+ context
+ .map
+ .insert("vim_operator".to_string(), operator_context.to_string());
+ }
+}
@@ -1,45 +1,63 @@
mod editor_events;
mod insert;
-mod mode;
+mod motion;
mod normal;
+mod state;
#[cfg(test)]
mod vim_test_context;
use collections::HashMap;
use editor::{CursorShape, Editor};
-use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
+use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle};
+use serde::Deserialize;
-use mode::Mode;
-use workspace::{self, Settings, Workspace};
+use settings::Settings;
+use state::{Mode, Operator, VimState};
+use workspace::{self, Workspace};
-action!(SwitchMode, Mode);
+#[derive(Clone, Deserialize)]
+pub struct SwitchMode(pub Mode);
+
+#[derive(Clone, Deserialize)]
+pub struct PushOperator(pub Operator);
+
+impl_actions!(vim, [SwitchMode, PushOperator]);
pub fn init(cx: &mut MutableAppContext) {
editor_events::init(cx);
insert::init(cx);
- normal::init(cx);
+ motion::init(cx);
- cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| {
- VimState::update_global(cx, |state, cx| state.switch_mode(action, cx))
+ cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
+ Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
});
+ cx.add_action(
+ |_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| {
+ Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
+ },
+ );
cx.observe_global::<Settings, _>(|settings, cx| {
- VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
+ Vim::update(cx, |state, cx| state.set_enabled(settings.vim_mode, cx))
})
.detach();
}
#[derive(Default)]
-pub struct VimState {
+pub struct Vim {
editors: HashMap<usize, WeakViewHandle<Editor>>,
active_editor: Option<WeakViewHandle<Editor>>,
enabled: bool,
- mode: Mode,
+ state: VimState,
}
-impl VimState {
- fn update_global<F, S>(cx: &mut MutableAppContext, update: F) -> S
+impl Vim {
+ fn read(cx: &mut MutableAppContext) -> &Self {
+ cx.default_global()
+ }
+
+ fn update<F, S>(cx: &mut MutableAppContext, update: F) -> S
where
F: FnOnce(&mut Self, &mut MutableAppContext) -> S,
{
@@ -57,33 +75,54 @@ impl VimState {
.map(|ae| ae.update(cx, update))
}
- fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) {
- self.mode = *mode;
+ fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
+ self.state.mode = mode;
+ self.state.operator_stack.clear();
self.sync_editor_options(cx);
}
+ fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
+ self.state.operator_stack.push(operator);
+ self.sync_editor_options(cx);
+ }
+
+ fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
+ let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
+ self.sync_editor_options(cx);
+ popped_operator
+ }
+
+ fn clear_operator(&mut self, cx: &mut MutableAppContext) {
+ self.state.operator_stack.clear();
+ self.sync_editor_options(cx);
+ }
+
+ fn active_operator(&mut self) -> Option<Operator> {
+ self.state.operator_stack.last().copied()
+ }
+
fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
if self.enabled != enabled {
self.enabled = enabled;
- self.mode = Default::default();
+ self.state = Default::default();
if enabled {
- self.mode = Mode::normal();
+ self.state.mode = Mode::Normal;
}
self.sync_editor_options(cx);
}
}
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
- let mode = self.mode;
- let cursor_shape = mode.cursor_shape();
+ let state = &self.state;
+ let cursor_shape = state.cursor_shape();
for editor in self.editors.values() {
if let Some(editor) = editor.upgrade(cx) {
editor.update(cx, |editor, cx| {
if self.enabled {
editor.set_cursor_shape(cursor_shape, cx);
editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
- editor.set_input_enabled(mode == Mode::Insert);
- let context_layer = mode.keymap_context_layer();
+ editor.set_input_enabled(!state.vim_controlled());
+ let context_layer = state.keymap_context_layer();
editor.set_keymap_context_layer::<Self>(context_layer);
} else {
editor.set_cursor_shape(CursorShape::Bar, cx);
@@ -99,12 +138,12 @@ impl VimState {
#[cfg(test)]
mod test {
- use crate::{mode::Mode, vim_test_context::VimTestContext};
+ use crate::{state::Mode, vim_test_context::VimTestContext};
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, false, "").await;
- cx.simulate_keystrokes(&["h", "j", "k", "l"]);
+ cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjkl|");
}
@@ -117,22 +156,22 @@ mod test {
// Editor acts as though vim is disabled
cx.disable_vim();
- cx.simulate_keystrokes(&["h", "j", "k", "l"]);
+ cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjkl|");
// Enabling dynamically sets vim mode again and restores normal mode
cx.enable_vim();
- assert_eq!(cx.mode(), Mode::normal());
- cx.simulate_keystrokes(&["h", "h", "h", "l"]);
+ assert_eq!(cx.mode(), Mode::Normal);
+ cx.simulate_keystrokes(["h", "h", "h", "l"]);
assert_eq!(cx.editor_text(), "hjkl".to_owned());
cx.assert_editor_state("hj|kl");
- cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
+ cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
cx.assert_editor_state("hjTest|kl");
// Disabling and enabling resets to normal mode
assert_eq!(cx.mode(), Mode::Insert);
cx.disable_vim();
cx.enable_vim();
- assert_eq!(cx.mode(), Mode::normal());
+ assert_eq!(cx.mode(), Mode::Normal);
}
}
@@ -6,7 +6,7 @@ use language::{Point, Selection};
use util::test::marked_text;
use workspace::{WorkspaceHandle, WorkspaceParams};
-use crate::*;
+use crate::{state::Operator, *};
pub struct VimTestContext<'a> {
cx: &'a mut gpui::TestAppContext,
@@ -23,7 +23,10 @@ impl<'a> VimTestContext<'a> {
cx.update(|cx| {
editor::init(cx);
crate::init(cx);
+
+ settings::KeymapFile::load("keymaps/vim.json", cx).unwrap();
});
+
let params = cx.update(WorkspaceParams::test);
cx.update(|cx| {
@@ -97,7 +100,12 @@ impl<'a> VimTestContext<'a> {
}
pub fn mode(&mut self) -> Mode {
- self.cx.update(|cx| cx.global::<VimState>().mode)
+ self.cx.read(|cx| cx.global::<Vim>().state.mode)
+ }
+
+ pub fn active_operator(&mut self) -> Option<Operator> {
+ self.cx
+ .read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
}
pub fn editor_text(&mut self) -> String {
@@ -116,12 +124,23 @@ impl<'a> VimTestContext<'a> {
.dispatch_keystroke(self.window_id, keystroke, input, false);
}
- pub fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
+ pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
}
+ pub fn set_state(&mut self, text: &str, mode: Mode) {
+ self.cx
+ .update(|cx| Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)));
+ self.editor.update(self.cx, |editor, cx| {
+ let (unmarked_text, markers) = marked_text(&text);
+ editor.set_text(unmarked_text, cx);
+ let cursor_offset = markers[0];
+ editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
+ })
+ }
+
pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
let actual_head = self.newest_selection().head();
let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
@@ -168,6 +187,21 @@ impl<'a> VimTestContext<'a> {
actual_position_text, expected_position_text
)
}
+
+ pub fn assert_binding<const COUNT: usize>(
+ &mut self,
+ keystrokes: [&str; COUNT],
+ initial_state: &str,
+ initial_mode: Mode,
+ state_after: &str,
+ mode_after: Mode,
+ ) {
+ self.set_state(initial_state, initial_mode);
+ self.simulate_keystrokes(keystrokes);
+ self.assert_editor_state(state_after);
+ assert_eq!(self.mode(), mode_after);
+ assert_eq!(self.active_operator(), None);
+ }
}
impl<'a> Deref for VimTestContext<'a> {
@@ -8,7 +8,7 @@ path = "src/workspace.rs"
doctest = false
[features]
-test-support = ["client/test-support", "project/test-support"]
+test-support = ["client/test-support", "project/test-support", "settings/test-support"]
[dependencies]
client = { path = "../client" }
@@ -17,14 +17,14 @@ collections = { path = "../collections" }
gpui = { path = "../gpui" }
language = { path = "../language" }
project = { path = "../project" }
+settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
anyhow = "1.0.38"
futures = "0.3"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
-schemars = "0.8"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["preserve_order"] }
smallvec = { version = "1.6", features = ["union"] }
@@ -33,3 +33,4 @@ smallvec = { version = "1.6", features = ["union"] }
client = { path = "../client", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
@@ -1,18 +1,19 @@
-use crate::{ItemHandle, Settings, StatusItemView};
+use crate::{ItemHandle, StatusItemView};
use futures::StreamExt;
-use gpui::AppContext;
+use gpui::{actions, AppContext};
use gpui::{
- action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext,
- RenderContext, View, ViewContext,
+ elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext,
+ View, ViewContext,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project};
+use settings::Settings;
use smallvec::SmallVec;
use std::cmp::Reverse;
use std::fmt::Write;
use std::sync::Arc;
-action!(DismissErrorMessage);
+actions!(lsp_status, [DismissErrorMessage]);
pub struct LspStatus {
checking_for_update: Vec<String>,
@@ -1,19 +1,16 @@
-use gpui::{action, keymap::Binding, MutableAppContext};
+#[derive(Clone)]
+pub struct SelectIndex(pub usize);
-action!(Confirm);
-action!(SelectPrev);
-action!(SelectNext);
-action!(SelectFirst);
-action!(SelectLast);
+gpui::actions!(
+ menu,
+ [
+ Cancel,
+ Confirm,
+ SelectPrev,
+ SelectNext,
+ SelectFirst,
+ SelectLast
+ ]
+);
-pub fn init(cx: &mut MutableAppContext) {
- cx.add_bindings([
- Binding::new("up", SelectPrev, Some("menu")),
- Binding::new("ctrl-p", SelectPrev, Some("menu")),
- Binding::new("down", SelectNext, Some("menu")),
- Binding::new("ctrl-n", SelectNext, Some("menu")),
- Binding::new("cmd-up", SelectFirst, Some("menu")),
- Binding::new("cmd-down", SelectLast, Some("menu")),
- Binding::new("enter", Confirm, Some("menu")),
- ]);
-}
+gpui::impl_internal_actions!(menu, [SelectIndex]);
@@ -1,29 +1,59 @@
use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
+use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
+use anyhow::Result;
use collections::{HashMap, VecDeque};
use futures::StreamExt;
use gpui::{
- action,
+ actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
- keymap::Binding,
+ impl_actions, impl_internal_actions,
platform::{CursorStyle, NavigationDirection},
- AppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, RenderContext, Task,
- View, ViewContext, ViewHandle, WeakViewHandle,
+ AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
+ ViewContext, ViewHandle, WeakViewHandle,
};
-use project::{Project, ProjectEntryId, ProjectPath};
+use project::{ProjectEntryId, ProjectPath};
+use serde::Deserialize;
+use settings::Settings;
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
use util::ResultExt;
-action!(Split, SplitDirection);
-action!(ActivateItem, usize);
-action!(ActivatePrevItem);
-action!(ActivateNextItem);
-action!(CloseActiveItem);
-action!(CloseInactiveItems);
-action!(CloseItem, usize);
-action!(GoBack, Option<WeakViewHandle<Pane>>);
-action!(GoForward, Option<WeakViewHandle<Pane>>);
+actions!(
+ pane,
+ [
+ ActivatePrevItem,
+ ActivateNextItem,
+ CloseActiveItem,
+ CloseInactiveItems,
+ ]
+);
+
+#[derive(Clone, Deserialize)]
+pub struct Split(pub SplitDirection);
+
+#[derive(Clone)]
+pub struct CloseItem {
+ pub item_id: usize,
+ pub pane: WeakViewHandle<Pane>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct ActivateItem(pub usize);
+
+#[derive(Clone, Deserialize)]
+pub struct GoBack {
+ #[serde(skip_deserializing)]
+ pub pane: Option<WeakViewHandle<Pane>>,
+}
+
+#[derive(Clone, Deserialize)]
+pub struct GoForward {
+ #[serde(skip_deserializing)]
+ pub pane: Option<WeakViewHandle<Pane>>,
+}
+
+impl_actions!(pane, [Split, GoBack, GoForward]);
+impl_internal_actions!(pane, [CloseItem, ActivateItem]);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@@ -37,14 +67,11 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
pane.activate_next_item(cx);
});
- cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
- pane.close_active_item(cx).detach();
- });
- cx.add_action(|pane: &mut Pane, _: &CloseInactiveItems, cx| {
- pane.close_inactive_items(cx).detach();
- });
- cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
- pane.close_item(action.0, cx).detach();
+ cx.add_async_action(Pane::close_active_item);
+ cx.add_async_action(Pane::close_inactive_items);
+ cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
+ let pane = action.pane.upgrade(cx)?;
+ Some(Pane::close_item(workspace, pane, action.item_id, cx))
});
cx.add_action(|pane: &mut Pane, action: &Split, cx| {
pane.split(action.0, cx);
@@ -53,7 +80,7 @@ pub fn init(cx: &mut MutableAppContext) {
Pane::go_back(
workspace,
action
- .0
+ .pane
.as_ref()
.and_then(|weak_handle| weak_handle.upgrade(cx)),
cx,
@@ -64,26 +91,13 @@ pub fn init(cx: &mut MutableAppContext) {
Pane::go_forward(
workspace,
action
- .0
+ .pane
.as_ref()
.and_then(|weak_handle| weak_handle.upgrade(cx)),
cx,
)
.detach();
});
-
- cx.add_bindings(vec![
- Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
- Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
- Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
- Binding::new("alt-cmd-w", CloseInactiveItems, Some("Pane")),
- Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
- Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
- Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
- Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
- Binding::new("ctrl--", GoBack(None), Some("Pane")),
- Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")),
- ]);
}
pub enum Event {
@@ -96,9 +110,9 @@ pub enum Event {
pub struct Pane {
items: Vec<Box<dyn ItemHandle>>,
active_item_index: usize,
+ autoscroll: bool,
nav_history: Rc<RefCell<NavHistory>>,
toolbar: ViewHandle<Toolbar>,
- project: ModelHandle<Project>,
}
pub struct ItemNavHistory {
@@ -134,13 +148,13 @@ pub struct NavigationEntry {
}
impl Pane {
- pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
items: Vec::new(),
active_item_index: 0,
+ autoscroll: false,
nav_history: Default::default(),
toolbar: cx.add_view(|_| Toolbar::new()),
- project,
}
}
@@ -197,21 +211,14 @@ impl Pane {
.upgrade(cx)
.and_then(|v| pane.index_for_item(v.as_ref()))
{
- if let Some(item) = pane.active_item() {
- pane.nav_history.borrow_mut().set_mode(mode);
- item.deactivated(cx);
- pane.nav_history
- .borrow_mut()
- .set_mode(NavigationMode::Normal);
- }
-
- let prev_active_index = mem::replace(&mut pane.active_item_index, index);
- pane.focus_active_item(cx);
- pane.update_toolbar(cx);
- cx.emit(Event::ActivateItem { local: true });
- cx.notify();
+ let prev_active_item_index = pane.active_item_index;
+ pane.nav_history.borrow_mut().set_mode(mode);
+ pane.activate_item(index, true, cx);
+ pane.nav_history
+ .borrow_mut()
+ .set_mode(NavigationMode::Normal);
- let mut navigated = prev_active_index != pane.active_item_index;
+ let mut navigated = prev_active_item_index != pane.active_item_index;
if let Some(data) = entry.data {
navigated |= pane.active_item()?.navigate(data, cx);
}
@@ -372,10 +379,12 @@ impl Pane {
}
pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
+ use NavigationMode::{GoingBack, GoingForward};
if index < self.items.len() {
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
- if prev_active_item_ix != self.active_item_index
- && prev_active_item_ix < self.items.len()
+ if matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
+ || (prev_active_item_ix != self.active_item_index
+ && prev_active_item_ix < self.items.len())
{
self.items[prev_active_item_ix].deactivated(cx);
cx.emit(Event::ActivateItem { local });
@@ -385,6 +394,7 @@ impl Pane {
self.focus_active_item(cx);
self.activate(cx);
}
+ self.autoscroll = true;
cx.notify();
}
}
@@ -409,162 +419,183 @@ impl Pane {
self.activate_item(index, true, cx);
}
- pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
- if self.items.is_empty() {
- Task::ready(())
+ fn close_active_item(
+ workspace: &mut Workspace,
+ _: &CloseActiveItem,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let pane_handle = workspace.active_pane().clone();
+ let pane = pane_handle.read(cx);
+ if pane.items.is_empty() {
+ None
} else {
- self.close_item(self.items[self.active_item_index].id(), cx)
+ let item_id_to_close = pane.items[pane.active_item_index].id();
+ Some(Self::close_items(
+ workspace,
+ pane_handle,
+ cx,
+ move |item_id| item_id == item_id_to_close,
+ ))
}
}
- pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
- if self.items.is_empty() {
- Task::ready(())
+ pub fn close_inactive_items(
+ workspace: &mut Workspace,
+ _: &CloseInactiveItems,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let pane_handle = workspace.active_pane().clone();
+ let pane = pane_handle.read(cx);
+ if pane.items.is_empty() {
+ None
} else {
- let active_item_id = self.items[self.active_item_index].id();
- self.close_items(cx, move |id| id != active_item_id)
+ let active_item_id = pane.items[pane.active_item_index].id();
+ Some(Self::close_items(workspace, pane_handle, cx, move |id| {
+ id != active_item_id
+ }))
}
}
- pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) -> Task<()> {
- self.close_items(cx, move |view_id| view_id == view_id_to_close)
+ pub fn close_item(
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ item_id_to_close: usize,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Task<Result<()>> {
+ Self::close_items(workspace, pane, cx, move |view_id| {
+ view_id == item_id_to_close
+ })
}
pub fn close_items(
- &mut self,
- cx: &mut ViewContext<Self>,
+ workspace: &mut Workspace,
+ pane: ViewHandle<Pane>,
+ cx: &mut ViewContext<Workspace>,
should_close: impl 'static + Fn(usize) -> bool,
- ) -> Task<()> {
+ ) -> Task<Result<()>> {
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
const DIRTY_MESSAGE: &'static str =
"This file contains unsaved edits. Do you want to save it?";
- let project = self.project.clone();
- cx.spawn(|this, mut cx| async move {
- while let Some(item_to_close_ix) = this.read_with(&cx, |this, _| {
- this.items.iter().position(|item| should_close(item.id()))
+ let project = workspace.project().clone();
+ cx.spawn(|workspace, mut cx| async move {
+ while let Some(item_to_close_ix) = pane.read_with(&cx, |pane, _| {
+ pane.items.iter().position(|item| should_close(item.id()))
}) {
let item =
- this.read_with(&cx, |this, _| this.items[item_to_close_ix].boxed_clone());
- if cx.read(|cx| item.is_dirty(cx)) {
- if cx.read(|cx| item.can_save(cx)) {
- let mut answer = this.update(&mut cx, |this, cx| {
- this.activate_item(item_to_close_ix, true, cx);
+ pane.read_with(&cx, |pane, _| pane.items[item_to_close_ix].boxed_clone());
+
+ let is_last_item_for_entry = workspace.read_with(&cx, |workspace, cx| {
+ let project_entry_id = item.project_entry_id(cx);
+ project_entry_id.is_none()
+ || workspace
+ .items(cx)
+ .filter(|item| item.project_entry_id(cx) == project_entry_id)
+ .count()
+ == 1
+ });
+
+ if is_last_item_for_entry {
+ if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
+ let mut answer = pane.update(&mut cx, |pane, cx| {
+ pane.activate_item(item_to_close_ix, true, cx);
cx.prompt(
PromptLevel::Warning,
- DIRTY_MESSAGE,
- &["Save", "Don't Save", "Cancel"],
+ CONFLICT_MESSAGE,
+ &["Overwrite", "Discard", "Cancel"],
)
});
match answer.next().await {
Some(0) => {
- if cx
- .update(|cx| item.save(project.clone(), cx))
- .await
- .log_err()
- .is_none()
- {
- break;
- }
+ cx.update(|cx| item.save(project.clone(), cx)).await?;
+ }
+ Some(1) => {
+ cx.update(|cx| item.reload(project.clone(), cx)).await?;
}
- Some(1) => {}
_ => break,
}
- } else if cx.read(|cx| item.can_save_as(cx)) {
- let mut answer = this.update(&mut cx, |this, cx| {
- this.activate_item(item_to_close_ix, true, cx);
- cx.prompt(
- PromptLevel::Warning,
- DIRTY_MESSAGE,
- &["Save", "Don't Save", "Cancel"],
- )
- });
+ } else if cx.read(|cx| item.is_dirty(cx)) {
+ if cx.read(|cx| item.can_save(cx)) {
+ let mut answer = pane.update(&mut cx, |pane, cx| {
+ pane.activate_item(item_to_close_ix, true, cx);
+ cx.prompt(
+ PromptLevel::Warning,
+ DIRTY_MESSAGE,
+ &["Save", "Don't Save", "Cancel"],
+ )
+ });
- match answer.next().await {
- Some(0) => {
- let start_abs_path = project
- .read_with(&cx, |project, cx| {
- let worktree = project.visible_worktrees(cx).next()?;
- Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
- })
- .unwrap_or(Path::new("").into());
-
- let mut abs_path =
- cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
- if let Some(abs_path) = abs_path.next().await.flatten() {
- if cx
- .update(|cx| item.save_as(project.clone(), abs_path, cx))
- .await
- .log_err()
- .is_none()
- {
+ match answer.next().await {
+ Some(0) => {
+ cx.update(|cx| item.save(project.clone(), cx)).await?;
+ }
+ Some(1) => {}
+ _ => break,
+ }
+ } else if cx.read(|cx| item.can_save_as(cx)) {
+ let mut answer = pane.update(&mut cx, |pane, cx| {
+ pane.activate_item(item_to_close_ix, true, cx);
+ cx.prompt(
+ PromptLevel::Warning,
+ DIRTY_MESSAGE,
+ &["Save", "Don't Save", "Cancel"],
+ )
+ });
+
+ match answer.next().await {
+ Some(0) => {
+ let start_abs_path = project
+ .read_with(&cx, |project, cx| {
+ let worktree = project.visible_worktrees(cx).next()?;
+ Some(
+ worktree
+ .read(cx)
+ .as_local()?
+ .abs_path()
+ .to_path_buf(),
+ )
+ })
+ .unwrap_or(Path::new("").into());
+
+ let mut abs_path =
+ cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
+ if let Some(abs_path) = abs_path.next().await.flatten() {
+ cx.update(|cx| item.save_as(project.clone(), abs_path, cx))
+ .await?;
+ } else {
break;
}
- } else {
- break;
}
- }
- Some(1) => {}
- _ => break,
- }
- }
- } else if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
- let mut answer = this.update(&mut cx, |this, cx| {
- this.activate_item(item_to_close_ix, true, cx);
- cx.prompt(
- PromptLevel::Warning,
- CONFLICT_MESSAGE,
- &["Overwrite", "Discard", "Cancel"],
- )
- });
-
- match answer.next().await {
- Some(0) => {
- if cx
- .update(|cx| item.save(project.clone(), cx))
- .await
- .log_err()
- .is_none()
- {
- break;
+ Some(1) => {}
+ _ => break,
}
}
- Some(1) => {
- if cx
- .update(|cx| item.reload(project.clone(), cx))
- .await
- .log_err()
- .is_none()
- {
- break;
- }
- }
- _ => break,
}
}
- this.update(&mut cx, |this, cx| {
- if let Some(item_ix) = this.items.iter().position(|i| i.id() == item.id()) {
- if item_ix == this.active_item_index {
- if item_ix + 1 < this.items.len() {
- this.activate_next_item(cx);
+ pane.update(&mut cx, |pane, cx| {
+ if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
+ if item_ix == pane.active_item_index {
+ if item_ix + 1 < pane.items.len() {
+ pane.activate_next_item(cx);
} else if item_ix > 0 {
- this.activate_prev_item(cx);
+ pane.activate_prev_item(cx);
}
}
- let item = this.items.remove(item_ix);
- if this.items.is_empty() {
+ let item = pane.items.remove(item_ix);
+ if pane.items.is_empty() {
item.deactivated(cx);
+ pane.update_toolbar(cx);
cx.emit(Event::Remove);
}
- if item_ix < this.active_item_index {
- this.active_item_index -= 1;
+ if item_ix < pane.active_item_index {
+ pane.active_item_index -= 1;
}
- let mut nav_history = this.nav_history.borrow_mut();
+ let mut nav_history = pane.nav_history.borrow_mut();
if let Some(path) = item.project_path(cx) {
nav_history.paths_by_item.insert(item.id(), path);
} else {
@@ -574,7 +605,8 @@ impl Pane {
});
}
- this.update(&mut cx, |_, cx| cx.notify());
+ pane.update(&mut cx, |_, cx| cx.notify());
+ Ok(())
})
}
@@ -602,12 +634,18 @@ impl Pane {
});
}
- fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+ fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
enum Tabs {}
+ let pane = cx.handle();
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
- let mut row = Flex::row();
+ let autoscroll = if mem::take(&mut self.autoscroll) {
+ Some(self.active_item_index)
+ } else {
+ None
+ };
+ let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
for (ix, item) in self.items.iter().enumerate() {
let is_active = ix == self.active_item_index;
@@ -697,8 +735,14 @@ impl Pane {
)
.with_padding(Padding::uniform(4.))
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(move |cx| {
- cx.dispatch_action(CloseItem(item_id))
+ .on_click({
+ let pane = pane.clone();
+ move |cx| {
+ cx.dispatch_action(CloseItem {
+ item_id,
+ pane: pane.clone(),
+ })
+ }
})
.named("close-tab-icon")
} else {
@@ -763,8 +807,8 @@ impl View for Pane {
.on_navigate_mouse_down(move |direction, cx| {
let this = this.clone();
match direction {
- NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))),
- NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))),
+ NavigationDirection::Back => cx.dispatch_action(GoBack { pane: Some(this) }),
+ NavigationDirection::Forward => cx.dispatch_action(GoForward { pane: Some(this) }),
}
true
@@ -860,10 +904,11 @@ impl NavHistory {
#[cfg(test)]
mod tests {
- use crate::WorkspaceParams;
-
use super::*;
- use gpui::TestAppContext;
+ use crate::WorkspaceParams;
+ use gpui::{ModelHandle, TestAppContext, ViewContext};
+ use project::Project;
+ use std::sync::atomic::AtomicUsize;
#[gpui::test]
async fn test_close_items(cx: &mut TestAppContext) {
@@ -873,7 +918,7 @@ mod tests {
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
let item1 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
- item.has_conflict = true;
+ item.is_dirty = true;
item
});
let item2 = cx.add_view(window_id, |_| {
@@ -884,15 +929,11 @@ mod tests {
});
let item3 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
+ item.is_dirty = true;
item.has_conflict = true;
item
});
let item4 = cx.add_view(window_id, |_| {
- let mut item = TestItem::new();
- item.is_dirty = true;
- item
- });
- let item5 = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.is_dirty = true;
item.can_save = false;
@@ -903,26 +944,26 @@ mod tests {
workspace.add_item(Box::new(item2.clone()), cx);
workspace.add_item(Box::new(item3.clone()), cx);
workspace.add_item(Box::new(item4.clone()), cx);
- workspace.add_item(Box::new(item5.clone()), cx);
workspace.active_pane().clone()
});
- let close_items = pane.update(cx, |pane, cx| {
- pane.activate_item(1, true, cx);
- assert_eq!(pane.active_item().unwrap().id(), item2.id());
+ let close_items = workspace.update(cx, |workspace, cx| {
+ pane.update(cx, |pane, cx| {
+ pane.activate_item(1, true, cx);
+ assert_eq!(pane.active_item().unwrap().id(), item2.id());
+ });
let item1_id = item1.id();
let item3_id = item3.id();
let item4_id = item4.id();
- let item5_id = item5.id();
- pane.close_items(cx, move |id| {
- [item1_id, item3_id, item4_id, item5_id].contains(&id)
+ Pane::close_items(workspace, pane.clone(), cx, move |id| {
+ [item1_id, item3_id, item4_id].contains(&id)
})
});
cx.foreground().run_until_parked();
pane.read_with(cx, |pane, _| {
- assert_eq!(pane.items.len(), 5);
+ assert_eq!(pane.items.len(), 4);
assert_eq!(pane.active_item().unwrap().id(), item1.id());
});
@@ -932,7 +973,7 @@ mod tests {
assert_eq!(item1.read(cx).save_count, 1);
assert_eq!(item1.read(cx).save_as_count, 0);
assert_eq!(item1.read(cx).reload_count, 0);
- assert_eq!(pane.items.len(), 4);
+ assert_eq!(pane.items.len(), 3);
assert_eq!(pane.active_item().unwrap().id(), item3.id());
});
@@ -942,33 +983,67 @@ mod tests {
assert_eq!(item3.read(cx).save_count, 0);
assert_eq!(item3.read(cx).save_as_count, 0);
assert_eq!(item3.read(cx).reload_count, 1);
- assert_eq!(pane.items.len(), 3);
+ assert_eq!(pane.items.len(), 2);
assert_eq!(pane.active_item().unwrap().id(), item4.id());
});
cx.simulate_prompt_answer(window_id, 0);
cx.foreground().run_until_parked();
+ cx.simulate_new_path_selection(|_| Some(Default::default()));
+ close_items.await.unwrap();
pane.read_with(cx, |pane, cx| {
- assert_eq!(item4.read(cx).save_count, 1);
- assert_eq!(item4.read(cx).save_as_count, 0);
+ assert_eq!(item4.read(cx).save_count, 0);
+ assert_eq!(item4.read(cx).save_as_count, 1);
assert_eq!(item4.read(cx).reload_count, 0);
- assert_eq!(pane.items.len(), 2);
- assert_eq!(pane.active_item().unwrap().id(), item5.id());
+ assert_eq!(pane.items.len(), 1);
+ assert_eq!(pane.active_item().unwrap().id(), item2.id());
});
+ }
- cx.simulate_prompt_answer(window_id, 0);
+ #[gpui::test]
+ async fn test_prompting_only_on_last_item_for_entry(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+
+ let params = cx.update(WorkspaceParams::test);
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
+ let item = cx.add_view(window_id, |_| {
+ let mut item = TestItem::new();
+ item.is_dirty = true;
+ item.project_entry_id = Some(ProjectEntryId::new(&AtomicUsize::new(1)));
+ item
+ });
+
+ let (left_pane, right_pane) = workspace.update(cx, |workspace, cx| {
+ workspace.add_item(Box::new(item.clone()), cx);
+ let left_pane = workspace.active_pane().clone();
+ let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
+ (left_pane, right_pane)
+ });
+
+ workspace
+ .update(cx, |workspace, cx| {
+ let item = right_pane.read(cx).active_item().unwrap();
+ Pane::close_item(workspace, right_pane.clone(), item.id(), cx)
+ })
+ .await
+ .unwrap();
+ workspace.read_with(cx, |workspace, _| {
+ assert_eq!(workspace.panes(), [left_pane.clone()]);
+ });
+
+ let close_item = workspace.update(cx, |workspace, cx| {
+ let item = left_pane.read(cx).active_item().unwrap();
+ Pane::close_item(workspace, left_pane.clone(), item.id(), cx)
+ });
cx.foreground().run_until_parked();
- cx.simulate_new_path_selection(|_| Some(Default::default()));
- close_items.await;
- pane.read_with(cx, |pane, cx| {
- assert_eq!(item5.read(cx).save_count, 0);
- assert_eq!(item5.read(cx).save_as_count, 1);
- assert_eq!(item5.read(cx).reload_count, 0);
- assert_eq!(pane.items.len(), 1);
- assert_eq!(pane.active_item().unwrap().id(), item2.id());
+ cx.simulate_prompt_answer(window_id, 0);
+ close_item.await.unwrap();
+ left_pane.read_with(cx, |pane, _| {
+ assert_eq!(pane.items.len(), 0);
});
}
+ #[derive(Clone)]
struct TestItem {
save_count: usize,
save_as_count: usize,
@@ -976,6 +1051,7 @@ mod tests {
is_dirty: bool,
has_conflict: bool,
can_save: bool,
+ project_entry_id: Option<ProjectEntryId>,
}
impl TestItem {
@@ -987,6 +1063,7 @@ mod tests {
is_dirty: false,
has_conflict: false,
can_save: true,
+ project_entry_id: None,
}
}
}
@@ -1015,11 +1092,18 @@ mod tests {
}
fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
- None
+ self.project_entry_id
}
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
+ fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ Some(self.clone())
+ }
+
fn is_dirty(&self, _: &AppContext) -> bool {
self.is_dirty
}
@@ -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,
@@ -1,325 +0,0 @@
-use anyhow::Result;
-use futures::{stream, SinkExt, StreamExt as _};
-use gpui::{
- executor,
- font_cache::{FamilyId, FontCache},
-};
-use language::Language;
-use postage::{prelude::Stream, watch};
-use project::Fs;
-use schemars::{schema_for, JsonSchema};
-use serde::Deserialize;
-use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
-use theme::{Theme, ThemeRegistry};
-use util::ResultExt;
-
-#[derive(Clone)]
-pub struct Settings {
- pub buffer_font_family: FamilyId,
- pub buffer_font_size: f32,
- pub vim_mode: bool,
- pub tab_size: usize,
- pub soft_wrap: SoftWrap,
- pub preferred_line_length: u32,
- pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
- pub theme: Arc<Theme>,
-}
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-pub struct LanguageOverride {
- pub tab_size: Option<usize>,
- pub soft_wrap: Option<SoftWrap>,
- pub preferred_line_length: Option<u32>,
-}
-
-#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
- None,
- EditorWidth,
- PreferredLineLength,
-}
-
-#[derive(Clone)]
-pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
-
-#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
-struct SettingsFileContent {
- #[serde(default)]
- buffer_font_family: Option<String>,
- #[serde(default)]
- buffer_font_size: Option<f32>,
- #[serde(default)]
- vim_mode: Option<bool>,
- #[serde(flatten)]
- editor: LanguageOverride,
- #[serde(default)]
- language_overrides: HashMap<Arc<str>, LanguageOverride>,
- #[serde(default)]
- theme: Option<String>,
-}
-
-impl SettingsFile {
- pub async fn new(
- fs: Arc<dyn Fs>,
- executor: &executor::Background,
- path: impl Into<Arc<Path>>,
- ) -> Self {
- let path = path.into();
- let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
- let mut events = fs.watch(&path, Duration::from_millis(500)).await;
- let (mut tx, rx) = watch::channel_with(settings);
- executor
- .spawn(async move {
- while events.next().await.is_some() {
- if let Some(settings) = Self::load(fs.clone(), &path).await {
- if tx.send(settings).await.is_err() {
- break;
- }
- }
- }
- })
- .detach();
- Self(rx)
- }
-
- async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
- if fs.is_file(&path).await {
- fs.load(&path)
- .await
- .log_err()
- .and_then(|data| serde_json::from_str(&data).log_err())
- } else {
- Some(SettingsFileContent::default())
- }
- }
-}
-
-impl Settings {
- pub fn file_json_schema() -> serde_json::Value {
- serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
- }
-
- pub fn from_files(
- defaults: Self,
- sources: Vec<SettingsFile>,
- theme_registry: Arc<ThemeRegistry>,
- font_cache: Arc<FontCache>,
- ) -> impl futures::stream::Stream<Item = Self> {
- stream::select_all(sources.iter().enumerate().map(|(i, source)| {
- let mut rx = source.0.clone();
- // Consume the initial item from all of the constituent file watches but one.
- // This way, the stream will yield exactly one item for the files' initial
- // state, and won't return any more items until the files change.
- if i > 0 {
- rx.try_recv().ok();
- }
- rx
- }))
- .map(move |_| {
- let mut settings = defaults.clone();
- for source in &sources {
- settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
- }
- settings
- })
- }
-
- pub fn new(
- buffer_font_family: &str,
- font_cache: &FontCache,
- theme: Arc<Theme>,
- ) -> Result<Self> {
- Ok(Self {
- buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
- buffer_font_size: 15.,
- vim_mode: false,
- tab_size: 4,
- soft_wrap: SoftWrap::None,
- preferred_line_length: 80,
- language_overrides: Default::default(),
- theme,
- })
- }
-
- pub fn with_overrides(
- mut self,
- language_name: impl Into<Arc<str>>,
- overrides: LanguageOverride,
- ) -> Self {
- self.language_overrides
- .insert(language_name.into(), overrides);
- self
- }
-
- pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
- language
- .and_then(|language| self.language_overrides.get(language.name().as_ref()))
- .and_then(|settings| settings.tab_size)
- .unwrap_or(self.tab_size)
- }
-
- pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
- language
- .and_then(|language| self.language_overrides.get(language.name().as_ref()))
- .and_then(|settings| settings.soft_wrap)
- .unwrap_or(self.soft_wrap)
- }
-
- pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
- language
- .and_then(|language| self.language_overrides.get(language.name().as_ref()))
- .and_then(|settings| settings.preferred_line_length)
- .unwrap_or(self.preferred_line_length)
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn test(cx: &gpui::AppContext) -> Settings {
- Settings {
- buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
- buffer_font_size: 14.,
- vim_mode: false,
- tab_size: 4,
- soft_wrap: SoftWrap::None,
- preferred_line_length: 80,
- language_overrides: Default::default(),
- theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
- }
- }
-
- fn merge(
- &mut self,
- data: &SettingsFileContent,
- theme_registry: &ThemeRegistry,
- font_cache: &FontCache,
- ) {
- if let Some(value) = &data.buffer_font_family {
- if let Some(id) = font_cache.load_family(&[value]).log_err() {
- self.buffer_font_family = id;
- }
- }
- if let Some(value) = &data.theme {
- if let Some(theme) = theme_registry.get(value).log_err() {
- self.theme = theme;
- }
- }
-
- merge(&mut self.buffer_font_size, data.buffer_font_size);
- merge(&mut self.vim_mode, data.vim_mode);
- merge(&mut self.soft_wrap, data.editor.soft_wrap);
- merge(&mut self.tab_size, data.editor.tab_size);
- merge(
- &mut self.preferred_line_length,
- data.editor.preferred_line_length,
- );
-
- for (language_name, settings) in &data.language_overrides {
- let target = self
- .language_overrides
- .entry(language_name.clone())
- .or_default();
-
- merge_option(&mut target.tab_size, settings.tab_size);
- merge_option(&mut target.soft_wrap, settings.soft_wrap);
- merge_option(
- &mut target.preferred_line_length,
- settings.preferred_line_length,
- );
- }
- }
-}
-
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
- if let Some(value) = value {
- *target = value;
- }
-}
-
-fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
- if value.is_some() {
- *target = value;
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use project::FakeFs;
-
- #[gpui::test]
- async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
- let executor = cx.background();
- let fs = FakeFs::new(executor.clone());
-
- fs.save(
- "/settings1.json".as_ref(),
- &r#"
- {
- "buffer_font_size": 24,
- "soft_wrap": "editor_width",
- "language_overrides": {
- "Markdown": {
- "preferred_line_length": 100,
- "soft_wrap": "preferred_line_length"
- }
- }
- }
- "#
- .into(),
- )
- .await
- .unwrap();
-
- let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
- let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
- let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
-
- let mut settings_rx = Settings::from_files(
- cx.read(Settings::test),
- vec![source1, source2, source3],
- ThemeRegistry::new((), cx.font_cache()),
- cx.font_cache(),
- );
-
- let settings = settings_rx.next().await.unwrap();
- let md_settings = settings.language_overrides.get("Markdown").unwrap();
- assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
- assert_eq!(settings.buffer_font_size, 24.0);
- assert_eq!(settings.tab_size, 4);
- assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
- assert_eq!(md_settings.preferred_line_length, Some(100));
-
- fs.save(
- "/settings2.json".as_ref(),
- &r#"
- {
- "tab_size": 2,
- "soft_wrap": "none",
- "language_overrides": {
- "Markdown": {
- "preferred_line_length": 120
- }
- }
- }
- "#
- .into(),
- )
- .await
- .unwrap();
-
- let settings = settings_rx.next().await.unwrap();
- let md_settings = settings.language_overrides.get("Markdown").unwrap();
- assert_eq!(settings.soft_wrap, SoftWrap::None);
- assert_eq!(settings.buffer_font_size, 24.0);
- assert_eq!(settings.tab_size, 2);
- assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
- assert_eq!(md_settings.preferred_line_length, Some(120));
-
- fs.remove_file("/settings2.json".as_ref(), Default::default())
- .await
- .unwrap();
-
- let settings = settings_rx.next().await.unwrap();
- assert_eq!(settings.tab_size, 4);
- }
-}
@@ -1,5 +1,6 @@
use super::Workspace;
-use gpui::{action, elements::*, platform::CursorStyle, AnyViewHandle, RenderContext};
+use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext};
+use serde::Deserialize;
use std::{cell::RefCell, rc::Rc};
use theme::Theme;
@@ -10,7 +11,7 @@ pub struct Sidebar {
width: Rc<RefCell<f32>>,
}
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Deserialize)]
pub enum Side {
Left,
Right,
@@ -21,10 +22,15 @@ struct Item {
view: AnyViewHandle,
}
-action!(ToggleSidebarItem, SidebarItemId);
-action!(ToggleSidebarItemFocus, SidebarItemId);
+#[derive(Clone, Deserialize)]
+pub struct ToggleSidebarItem(pub SidebarItemId);
-#[derive(Clone)]
+#[derive(Clone, Deserialize)]
+pub struct ToggleSidebarItemFocus(pub SidebarItemId);
+
+impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
+
+#[derive(Clone, Deserialize)]
pub struct SidebarItemId {
pub side: Side,
pub item_index: usize,
@@ -1,4 +1,5 @@
-use crate::{ItemHandle, Pane, Settings};
+use crate::{ItemHandle, Pane};
+use settings::Settings;
use gpui::{
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
View, ViewContext, ViewHandle,
@@ -1,8 +1,9 @@
-use crate::{ItemHandle, Settings};
+use crate::ItemHandle;
use gpui::{
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
View, ViewContext, ViewHandle,
};
+use settings::Settings;
pub trait ToolbarItemView: View {
fn set_active_pane_item(
@@ -2,7 +2,6 @@ pub mod lsp_status;
pub mod menu;
pub mod pane;
pub mod pane_group;
-pub mod settings;
pub mod sidebar;
mod status_bar;
mod toolbar;
@@ -14,16 +13,16 @@ use client::{
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet};
use gpui::{
- action,
+ actions,
color::Color,
elements::*,
- geometry::{vector::vec2f, PathBuilder},
- json::{self, to_string_pretty, ToJson},
- keymap::Binding,
+ geometry::{rect::RectF, vector::vec2f, PathBuilder},
+ impl_internal_actions,
+ json::{self, ToJson},
platform::{CursorStyle, WindowOptions},
- AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity,
- ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
- View, ViewContext, ViewHandle, WeakViewHandle,
+ AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
+ ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
+ ViewContext, ViewHandle, WeakViewHandle,
};
use language::LanguageRegistry;
use log::error;
@@ -31,8 +30,8 @@ pub use pane::*;
pub use pane_group::*;
use postage::prelude::Stream;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
-pub use settings::Settings;
-use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
+use settings::Settings;
+use sidebar::{Side, Sidebar, ToggleSidebarItem, ToggleSidebarItemFocus};
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use std::{
@@ -70,32 +69,56 @@ type FollowableItemBuilders = HashMap<
),
>;
-action!(Open, Arc<AppState>);
-action!(OpenNew, Arc<AppState>);
-action!(OpenPaths, OpenParams);
-action!(ToggleShare);
-action!(ToggleFollow, PeerId);
-action!(FollowNextCollaborator);
-action!(Unfollow);
-action!(JoinProject, JoinProjectParams);
-action!(Save);
-action!(DebugElements);
-action!(ActivatePreviousPane);
-action!(ActivateNextPane);
+actions!(
+ workspace,
+ [
+ ToggleShare,
+ Unfollow,
+ Save,
+ ActivatePreviousPane,
+ ActivateNextPane,
+ FollowNextCollaborator,
+ ]
+);
+
+#[derive(Clone)]
+pub struct Open(pub Arc<AppState>);
+
+#[derive(Clone)]
+pub struct OpenNew(pub Arc<AppState>);
+
+#[derive(Clone)]
+pub struct OpenPaths {
+ pub paths: Vec<PathBuf>,
+ pub app_state: Arc<AppState>,
+}
+
+#[derive(Clone)]
+pub struct ToggleFollow(pub PeerId);
+
+#[derive(Clone)]
+pub struct JoinProject {
+ pub project_id: u64,
+ pub app_state: Arc<AppState>,
+}
+
+impl_internal_actions!(
+ workspace,
+ [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
+);
pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
pane::init(cx);
- menu::init(cx);
cx.add_global_action(open);
cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
- open_paths(&action.0.paths, &action.0.app_state, cx).detach();
+ open_paths(&action.paths, &action.app_state, cx).detach();
});
cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
open_new(&action.0, cx)
});
cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
- join_project(action.0.project_id, &action.0.app_state, cx).detach();
+ join_project(action.project_id, &action.app_state, cx).detach();
});
cx.add_action(Workspace::toggle_share);
@@ -112,7 +135,6 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
workspace.save_active_item(cx).detach_and_log_err(cx);
},
);
- cx.add_action(Workspace::debug_elements);
cx.add_action(Workspace::toggle_sidebar_item);
cx.add_action(Workspace::toggle_sidebar_item_focus);
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
@@ -121,29 +143,6 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
workspace.activate_next_pane(cx)
});
- cx.add_bindings(vec![
- Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None),
- Binding::new("cmd-s", Save, None),
- Binding::new("cmd-alt-i", DebugElements, None),
- Binding::new("cmd-k cmd-left", ActivatePreviousPane, None),
- Binding::new("cmd-k cmd-right", ActivateNextPane, None),
- Binding::new(
- "cmd-shift-!",
- ToggleSidebarItem(SidebarItemId {
- side: Side::Left,
- item_index: 0,
- }),
- None,
- ),
- Binding::new(
- "cmd-1",
- ToggleSidebarItemFocus(SidebarItemId {
- side: Side::Left,
- item_index: 0,
- }),
- None,
- ),
- ]);
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
@@ -188,18 +187,6 @@ pub struct AppState {
fn(ModelHandle<Project>, &Arc<AppState>, &mut ViewContext<Workspace>) -> Workspace,
}
-#[derive(Clone)]
-pub struct OpenParams {
- pub paths: Vec<PathBuf>,
- pub app_state: Arc<AppState>,
-}
-
-#[derive(Clone)]
-pub struct JoinProjectParams {
- pub project_id: u64,
- pub app_state: Arc<AppState>,
-}
-
pub trait Item: View {
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@@ -386,6 +373,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
-> Task<Result<()>>;
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
+ fn on_release(
+ &self,
+ cx: &mut MutableAppContext,
+ callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+ ) -> gpui::Subscription;
}
pub trait WeakItemHandle {
@@ -421,17 +413,17 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
Box::new(self.clone())
}
- fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
+ fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
self.update(cx, |item, cx| {
- cx.add_option_view(|cx| item.clone_on_split(cx))
+ item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
})
- .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
}
- fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
+ fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
self.update(cx, |item, cx| {
- item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
+ cx.add_option_view(|cx| item.clone_on_split(cx))
})
+ .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
}
fn added_to_pane(
@@ -494,8 +486,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
if T::should_close_item_on_event(event) {
- pane.update(cx, |pane, cx| pane.close_item(item.id(), cx))
- .detach();
+ Pane::close_item(workspace, pane, item.id(), cx).detach_and_log_err(cx);
return;
}
@@ -523,6 +514,30 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.update(cx, |this, cx| this.navigate(data, cx))
}
+ fn id(&self) -> usize {
+ self.id()
+ }
+
+ fn to_any(&self) -> AnyViewHandle {
+ self.into()
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.read(cx).is_dirty(cx)
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.read(cx).has_conflict(cx)
+ }
+
+ fn can_save(&self, cx: &AppContext) -> bool {
+ self.read(cx).can_save(cx)
+ }
+
+ fn can_save_as(&self, cx: &AppContext) -> bool {
+ self.read(cx).can_save_as(cx)
+ }
+
fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
self.update(cx, |item, cx| item.save(project, cx))
}
@@ -544,30 +559,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.update(cx, |item, cx| item.reload(project, cx))
}
- fn is_dirty(&self, cx: &AppContext) -> bool {
- self.read(cx).is_dirty(cx)
- }
-
- fn has_conflict(&self, cx: &AppContext) -> bool {
- self.read(cx).has_conflict(cx)
- }
-
- fn id(&self) -> usize {
- self.id()
- }
-
- fn to_any(&self) -> AnyViewHandle {
- self.into()
- }
-
- fn can_save(&self, cx: &AppContext) -> bool {
- self.read(cx).can_save(cx)
- }
-
- fn can_save_as(&self, cx: &AppContext) -> bool {
- self.read(cx).can_save_as(cx)
- }
-
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
self.read(cx).act_as_type(type_id, self, cx)
}
@@ -581,6 +572,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
None
}
}
+
+ fn on_release(
+ &self,
+ cx: &mut MutableAppContext,
+ callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+ ) -> gpui::Subscription {
+ cx.observe_release(self, move |_, cx| callback(cx))
+ }
}
impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
@@ -611,6 +610,7 @@ pub struct WorkspaceParams {
pub client: Arc<Client>,
pub fs: Arc<dyn Fs>,
pub languages: Arc<LanguageRegistry>,
+ pub themes: Arc<ThemeRegistry>,
pub user_store: ModelHandle<UserStore>,
pub channel_list: ModelHandle<ChannelList>,
}
@@ -640,6 +640,7 @@ impl WorkspaceParams {
channel_list: cx
.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
client,
+ themes: ThemeRegistry::new((), cx.font_cache().clone()),
fs,
languages,
user_store,
@@ -658,6 +659,7 @@ impl WorkspaceParams {
),
client: app_state.client.clone(),
fs: app_state.fs.clone(),
+ themes: app_state.themes.clone(),
languages: app_state.languages.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
@@ -675,6 +677,7 @@ pub struct Workspace {
user_store: ModelHandle<client::UserStore>,
remote_entity_subscription: Option<Subscription>,
fs: Arc<dyn Fs>,
+ themes: Arc<ThemeRegistry>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
left_sidebar: Sidebar,
@@ -735,7 +738,7 @@ impl Workspace {
})
.detach();
- let pane = cx.add_view(|cx| Pane::new(params.project.clone(), cx));
+ let pane = cx.add_view(|cx| Pane::new(cx));
let pane_id = pane.id();
cx.observe(&pane, move |me, _, cx| {
let active_entry = me.active_project_path(cx);
@@ -783,6 +786,7 @@ impl Workspace {
remote_entity_subscription: None,
user_store: params.user_store.clone(),
fs: params.fs.clone(),
+ themes: params.themes.clone(),
left_sidebar: Sidebar::new(Side::Left),
right_sidebar: Sidebar::new(Side::Right),
project: params.project.clone(),
@@ -815,6 +819,10 @@ impl Workspace {
&self.project
}
+ pub fn themes(&self) -> Arc<ThemeRegistry> {
+ self.themes.clone()
+ }
+
pub fn worktrees<'a>(
&self,
cx: &'a AppContext,
@@ -1050,24 +1058,8 @@ impl Workspace {
cx.notify();
}
- pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext<Self>) {
- match to_string_pretty(&cx.debug_elements()) {
- Ok(json) => {
- let kib = json.len() as f32 / 1024.;
- cx.as_mut().write_to_clipboard(ClipboardItem::new(json));
- log::info!(
- "copied {:.1} KiB of element debug JSON to the clipboard",
- kib
- );
- }
- Err(error) => {
- log::error!("error debugging elements: {}", error);
- }
- };
- }
-
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
- let pane = cx.add_view(|cx| Pane::new(self.project.clone(), cx));
+ let pane = cx.add_view(|cx| Pane::new(cx));
let pane_id = pane.id();
cx.observe(&pane, move |me, _, cx| {
let active_entry = me.active_project_path(cx);
@@ -2067,7 +2059,8 @@ impl Element for AvatarRibbon {
fn dispatch_event(
&mut self,
_: &gpui::Event,
- _: gpui::geometry::rect::RectF,
+ _: RectF,
+ _: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
@@ -2090,9 +2083,9 @@ impl Element for AvatarRibbon {
}
}
-impl std::fmt::Debug for OpenParams {
+impl std::fmt::Debug for OpenPaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("OpenParams")
+ f.debug_struct("OpenPaths")
.field("paths", &self.paths)
.finish()
}
@@ -2107,7 +2100,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
});
cx.spawn(|mut cx| async move {
if let Some(paths) = paths.recv().await.flatten() {
- cx.update(|cx| cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })));
+ cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
}
})
.detach();
@@ -2119,7 +2112,10 @@ pub fn open_paths(
abs_paths: &[PathBuf],
app_state: &Arc<AppState>,
cx: &mut MutableAppContext,
-) -> Task<ViewHandle<Workspace>> {
+) -> Task<(
+ ViewHandle<Workspace>,
+ Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
+)> {
log::info!("open paths {:?}", abs_paths);
// Open paths in existing workspace if possible
@@ -2156,8 +2152,8 @@ pub fn open_paths(
let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
cx.spawn(|_| async move {
- task.await;
- workspace
+ let items = task.await;
+ (workspace, items)
})
}
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.24.0"
+version = "0.29.0"
[lib]
name = "zed"
@@ -15,10 +15,13 @@ name = "Zed"
path = "src/main.rs"
[dependencies]
+assets = { path = "../assets" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
chat_panel = { path = "../chat_panel" }
+cli = { path = "../cli" }
collections = { path = "../collections" }
+command_palette = { path = "../command_palette" }
client = { path = "../client" }
clock = { path = "../clock" }
contacts_panel = { path = "../contacts_panel" }
@@ -38,6 +41,7 @@ project = { path = "../project" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
rpc = { path = "../rpc" }
+settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
theme = { path = "../theme" }
@@ -49,7 +53,6 @@ anyhow = "1.0.38"
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-recursion = "0.3"
async-trait = "0.1"
-crossbeam-channel = "0.5.0"
ctor = "0.1.20"
dirs = "3.0"
easy-parallel = "3.1.0"
@@ -61,7 +64,7 @@ image = "0.23"
indexmap = "1.6.2"
lazy_static = "1.4.0"
libc = "0.2"
-log = "0.4"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
log-panics = { version = "2.0", features = ["with-backtrace"] }
num_cpus = "1.13.0"
parking_lot = "0.11.1"
@@ -98,6 +101,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
env_logger = "0.8"
@@ -1,413 +0,0 @@
-[text]
-base = { family = "Zed Sans", size = 14 }
-
-[workspace]
-background = "$surface.0"
-pane_divider = { width = 1, color = "$border.0" }
-leader_border_opacity = 0.7
-leader_border_width = 2.0
-
-[workspace.titlebar]
-height = 32
-border = { width = 1, bottom = true, color = "$border.0" }
-title = "$text.0"
-avatar_width = 18
-avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
-avatar_ribbon = { background = "#ff0000", height = 3, width = 12 }
-outdated_warning = { extends = "$text.2", size = 13 }
-share_icon_color = "$text.2.color"
-share_icon_active_color = "$text.0.color"
-
-[workspace.titlebar.sign_in_prompt]
-extends = "$text.2"
-size = 13
-underline = true
-padding = { right = 8 }
-
-[workspace.titlebar.hovered_sign_in_prompt]
-extends = "$workspace.titlebar.sign_in_prompt"
-color = "$text.1.color"
-
-[workspace.titlebar.offline_icon]
-padding = { right = 4 }
-width = 16
-color = "$text.2.color"
-
-[workspace.tab]
-height = 34
-text = "$text.2"
-padding = { left = 12, right = 12 }
-icon_width = 8
-spacing = 10
-icon_close = "$text.2.color"
-icon_close_active = "$text.0.color"
-icon_dirty = "$status.info"
-icon_conflict = "$status.warn"
-border = { left = true, bottom = true, width = 1, color = "$border.0", overlay = true }
-
-[workspace.active_tab]
-extends = "$workspace.tab"
-border.bottom = false
-background = "$surface.1"
-text = "$text.0"
-
-[workspace.sidebar]
-width = 30
-border = { right = true, width = 1, color = "$border.0" }
-
-[workspace.sidebar.resize_handle]
-padding = { left = 1 }
-background = "$border.0"
-
-[workspace.sidebar.item]
-icon_color = "$text.2.color"
-icon_size = 18
-height = "$workspace.tab.height"
-
-[workspace.sidebar.active_item]
-extends = "$workspace.sidebar.item"
-icon_color = "$text.0.color"
-
-[workspace.left_sidebar]
-extends = "$workspace.sidebar"
-border = { width = 1, color = "$border.0", right = true }
-
-[workspace.right_sidebar]
-extends = "$workspace.sidebar"
-border = { width = 1, color = "$border.0", left = true }
-
-[workspace.status_bar]
-padding = { left = 6, right = 6 }
-height = 24
-item_spacing = 8
-cursor_position = "$text.2"
-diagnostic_message = "$text.2"
-lsp_message = "$text.2"
-auto_update_progress_message = "$text.2"
-auto_update_done_message = "$text.2"
-
-[workspace.toolbar]
-background = "$surface.1"
-border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false }
-height = 34
-item_spacing = 8
-padding = { left = 16, right = 8, top = 4, bottom = 4 }
-
-[breadcrumbs]
-extends = "$text.1"
-padding = { left = 6 }
-
-[panel]
-padding = { top = 12, left = 12, bottom = 12, right = 12 }
-
-[chat_panel]
-extends = "$panel"
-channel_name = { extends = "$text.0", weight = "bold" }
-channel_name_hash = { text = "$text.2", padding.right = 8 }
-
-[chat_panel.message]
-body = "$text.1"
-sender = { extends = "$text.0", weight = "bold", margin.right = 8 }
-timestamp = "$text.2"
-padding.bottom = 6
-
-[chat_panel.pending_message]
-extends = "$chat_panel.message"
-body = { color = "$text.3.color" }
-sender = { color = "$text.3.color" }
-timestamp = { color = "$text.3.color" }
-
-[chat_panel.channel_select.item]
-padding = 4
-name = "$text.1"
-hash = { extends = "$text.2", margin.right = 8 }
-
-[chat_panel.channel_select.hovered_item]
-extends = "$chat_panel.channel_select.item"
-background = "$state.hover"
-corner_radius = 6
-
-[chat_panel.channel_select.active_item]
-extends = "$chat_panel.channel_select.item"
-name = "$text.0"
-
-[chat_panel.channel_select.hovered_active_item]
-extends = "$chat_panel.channel_select.hovered_item"
-name = "$text.0"
-
-[chat_panel.channel_select.header]
-extends = "$chat_panel.channel_select.active_item"
-padding.bottom = 4
-padding.left = 0
-
-[chat_panel.channel_select.menu]
-padding = 4
-corner_radius = 6
-border = { color = "$border.0", width = 1 }
-background = "$surface.0"
-shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
-
-[chat_panel.input_editor]
-background = "$surface.1"
-corner_radius = 6
-padding = { left = 8, right = 8, top = 7, bottom = 7 }
-text = "$text.0"
-placeholder_text = "$text.2"
-selection = "$selection.host"
-border = { width = 1, color = "$border.0" }
-
-[chat_panel.sign_in_prompt]
-extends = "$text.0"
-underline = true
-
-[chat_panel.hovered_sign_in_prompt]
-extends = "$chat_panel.sign_in_prompt"
-color = "$text.1.color"
-
-[contacts_panel]
-extends = "$panel"
-host_row_height = 28
-host_avatar = { corner_radius = 10, width = 18 }
-host_username = { extends = "$text.0", padding.left = 8 }
-tree_branch_width = 1
-tree_branch_color = "$surface.2"
-
-[contacts_panel.project]
-height = 24
-padding = { left = 8 }
-guest_avatar = { corner_radius = 8, width = 14 }
-guest_avatar_spacing = 4
-
-[contacts_panel.project.name]
-extends = "$text.1"
-margin = { right = 6 }
-
-[contacts_panel.unshared_project]
-extends = "$contacts_panel.project"
-
-[contacts_panel.hovered_unshared_project]
-extends = "$contacts_panel.unshared_project"
-background = "$state.hover"
-corner_radius = 6
-
-[contacts_panel.shared_project]
-extends = "$contacts_panel.project"
-name.color = "$text.0.color"
-
-[contacts_panel.hovered_shared_project]
-extends = "$contacts_panel.shared_project"
-background = "$state.hover"
-corner_radius = 6
-
-[project_panel]
-extends = "$panel"
-padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
-
-[project_panel.entry]
-text = "$text.1"
-height = 22
-icon_color = "$text.3.color"
-icon_size = 8
-icon_spacing = 8
-
-[project_panel.hovered_entry]
-extends = "$project_panel.entry"
-background = "$state.hover"
-
-[project_panel.selected_entry]
-extends = "$project_panel.entry"
-text = { extends = "$text.0" }
-
-[project_panel.hovered_selected_entry]
-extends = "$project_panel.hovered_entry"
-text = { extends = "$text.0" }
-
-[selector]
-background = "$surface.0"
-padding = 8
-margin = { top = 52, bottom = 52 }
-corner_radius = 6
-shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
-border = { width = 1, color = "$border.0" }
-
-[selector.input_editor]
-background = "$surface.1"
-corner_radius = 6
-padding = { left = 16, right = 16, top = 7, bottom = 7 }
-text = "$text.0"
-placeholder_text = "$text.2"
-selection = "$selection.host"
-border = { width = 1, color = "$border.0" }
-
-[selector.empty]
-text = "$text.2"
-padding = { left = 16, right = 16, top = 8, bottom = 4 }
-
-[selector.item]
-text = "$text.1"
-highlight_text = { extends = "$text.base", color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
-padding = { left = 16, right = 16, top = 4, bottom = 4 }
-corner_radius = 6
-
-[selector.active_item]
-extends = "$selector.item"
-background = "$state.hover"
-text = "$text.0"
-
-[editor]
-text_color = "$text.1.color"
-background = "$surface.1"
-gutter_background = "$surface.1"
-gutter_padding_factor = 2.5
-active_line_background = "$state.active_line"
-highlighted_line_background = "$state.highlighted_line"
-rename_fade = 0.6
-unnecessary_code_fade = 0.5
-document_highlight_read_background = "#99999920"
-document_highlight_write_background = "#99999916"
-diff_background_deleted = "$state.deleted_line"
-diff_background_inserted = "$state.inserted_line"
-line_number = "$text.2.color"
-line_number_active = "$text.0.color"
-selection = "$selection.host"
-guest_selections = "$selection.guests"
-error_color = "$status.bad"
-code_actions_indicator = "$text.3.color"
-
-[editor.diagnostic_path_header]
-background = "$state.active_line"
-filename = { extends = "$text.0", size = 14 }
-path = { extends = "$text.2", size = 14, margin.left = 12 }
-text_scale_factor = 0.857
-
-[editor.diagnostic_header]
-background = "$editor.background"
-border = { width = 1, top = true, bottom = true, color = "$border.1" }
-code = { extends = "$text.2", size = 14, margin.left = 10 }
-icon_width_factor = 1.5
-text_scale_factor = 0.857
-
-[editor.diagnostic_header.message]
-text = { extends = "$text.1", size = 14 }
-highlight_text = { extends = "$text.0", size = 14, weight = "bold" }
-
-[editor.error_diagnostic]
-header.border = { width = 1, top = true, color = "$border.0" }
-text_scale_factor = 0.857
-
-[editor.error_diagnostic.message]
-text = { extends = "$text.1", size = 14, color = "$status.bad" }
-highlight_text = { extends = "$text.1", size = 14, color = "$status.bad", weight = "bold" }
-
-[editor.warning_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$status.warn"
-message.highlight_text.color = "$status.warn"
-
-[editor.information_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$status.info"
-message.highlight_text.color = "$status.info"
-
-[editor.hint_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$status.info"
-message.highlight_text.color = "$status.info"
-
-[editor.invalid_error_diagnostic]
-extends = "$editor.error_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.invalid_warning_diagnostic]
-extends = "$editor.warning_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.invalid_information_diagnostic]
-extends = "$editor.information_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.invalid_hint_diagnostic]
-extends = "$editor.hint_diagnostic"
-message.text.color = "$text.3.color"
-message.highlight_text.color = "$text.3.color"
-
-[editor.autocomplete]
-background = "$surface.2"
-border = { width = 2, color = "$border.1" }
-corner_radius = 6
-padding = 6
-match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
-margin.left = -14
-
-[editor.autocomplete.item]
-padding = { left = 6, right = 6, top = 2, bottom = 2 }
-corner_radius = 6
-
-[editor.autocomplete.selected_item]
-extends = "$editor.autocomplete.item"
-background = "$state.selected"
-
-[editor.autocomplete.hovered_item]
-extends = "$editor.autocomplete.item"
-background = "$state.hover"
-
-[project_diagnostics]
-background = "$surface.1"
-empty_message = { extends = "$text.0", size = 18 }
-status_bar_item = { extends = "$text.2", margin.right = 10 }
-tab_icon_width = 13
-tab_icon_spacing = 4
-tab_summary_spacing = 10
-
-[search]
-match_background = "$state.highlighted_line"
-results_status = { extends = "$text.0", size = 18 }
-tab_icon_width = 14
-tab_icon_spacing = 4
-
-[search.option_button]
-extends = "$text.1"
-padding = { left = 6, right = 6, top = 1, bottom = 1 }
-corner_radius = 6
-background = "$surface.1"
-border = { width = 1, color = "$border.0" }
-margin.left = 1
-margin.right = 1
-
-[search.option_button_group]
-padding = { left = 2, right = 2 }
-
-[search.active_option_button]
-extends = "$search.option_button"
-background = "$surface.2"
-
-[search.hovered_option_button]
-extends = "$search.option_button"
-background = "$surface.2"
-
-[search.active_hovered_option_button]
-extends = "$search.option_button"
-background = "$surface.2"
-
-[search.match_index]
-extends = "$text.2"
-padding = 6
-
-[search.editor]
-min_width = 200
-max_width = 500
-background = "$surface.0"
-corner_radius = 6
-padding = { left = 14, right = 14, top = 3, bottom = 3 }
-margin = { right = 5 }
-text = "$text.0"
-placeholder_text = "$text.2"
-selection = "$selection.host"
-border = { width = 1, color = "$border.0" }
-
-[search.invalid_editor]
-extends = "$search.editor"
-border = { width = 1, color = "$status.bad" }
@@ -1,67 +0,0 @@
-extends = "_base"
-
-[surface]
-0 = "#222222"
-1 = "#0f0b0c"
-2 = "#131415"
-
-[border]
-0 = "#000000B2"
-1 = "#FFFFFF20"
-
-[text]
-0 = { extends = "$text.base", color = "#ffffff" }
-1 = { extends = "$text.base", color = "#b3b3b3" }
-2 = { extends = "$text.base", color = "#7b7d80" }
-3 = { extends = "$text.base", color = "#66686A" }
-
-[shadow]
-0 = "#00000052"
-
-[selection]
-host = { selection = "#3B57BC55", cursor = "$text.0.color" }
-guests = [
- { selection = "#FDF35133", cursor = "#FDF351" },
- { selection = "#4EACAD33", cursor = "#4EACAD" },
- { selection = "#D0453B33", cursor = "#D0453B" },
- { selection = "#3B874B33", cursor = "#3B874B" },
- { selection = "#BD7CB433", cursor = "#BD7CB4" },
- { selection = "#EE823133", cursor = "#EE8231" },
- { selection = "#5A2B9233", cursor = "#5A2B92" },
-]
-
-[status]
-good = "#4fac63"
-info = "#3c5dd4"
-warn = "#faca50"
-bad = "#b7372e"
-
-[state]
-active_line = "#161313"
-highlighted_line = "#faca5033"
-deleted_line = "#dd000036"
-inserted_line = "#00dd0036"
-hover = "#00000033"
-selected = "#00000088"
-
-[editor.syntax]
-keyword = { color = "#0086c0", weight = "bold" }
-function = "#dcdcaa"
-string = "#cb8f77"
-type = "#4ec9b0"
-number = "#b5cea8"
-comment = "#6a9955"
-property = "#4e94ce"
-variant = "#4fc1ff"
-constant = "#9cdcfe"
-title = { color = "#9cdcfe", weight = "bold" }
-emphasis = "#4ec9b0"
-"emphasis.strong" = { color = "#4ec9b0", weight = "bold" }
-link_uri = { color = "#6a9955", underline = true }
-link_text = { color = "#cb8f77", italic = true }
-list_marker = "#4e94ce"
-
-[workspace.disconnected_overlay]
-extends = "$text.base"
-color = "#ffffff"
-background = "#000000aa"
@@ -1,67 +0,0 @@
-extends = "_base"
-
-[surface]
-0 = "#283340"
-1 = "#1C2733"
-2 = "#1C2733"
-
-[border]
-0 = "#1B222B"
-1 = "#FFFFFF20"
-
-[text]
-0 = { extends = "$text.base", color = "#FFFFFF" }
-1 = { extends = "$text.base", color = "#CDD1E2" }
-2 = { extends = "$text.base", color = "#9BA8BE" }
-3 = { extends = "$text.base", color = "#6E7483" }
-
-[shadow]
-0 = "#00000052"
-
-[selection]
-host = { selection = "#3B57BC55", cursor = "$text.0.color" }
-guests = [
- { selection = "#FDF35133", cursor = "#FDF351" },
- { selection = "#4EACAD33", cursor = "#4EACAD" },
- { selection = "#D0453B33", cursor = "#D0453B" },
- { selection = "#3B874B33", cursor = "#3B874B" },
- { selection = "#BD7CB433", cursor = "#BD7CB4" },
- { selection = "#EE823133", cursor = "#EE8231" },
- { selection = "#5A2B9233", cursor = "#5A2B92" },
-]
-
-[status]
-good = "#4fac63"
-info = "#3c5dd4"
-warn = "#faca50"
-bad = "#b7372e"
-
-[state]
-active_line = "#00000022"
-highlighted_line = "#faca5033"
-deleted_line = "#dd000036"
-inserted_line = "#00dd0036"
-hover = "#00000033"
-selected = "#00000088"
-
-[editor.syntax]
-keyword = { color = "#0086c0", weight = "bold" }
-function = "#dcdcaa"
-string = "#cb8f77"
-type = "#4ec9b0"
-number = "#b5cea8"
-comment = "#6a9955"
-property = "#4e94ce"
-variant = "#4fc1ff"
-constant = "#9cdcfe"
-title = { color = "#9cdcfe", weight = "bold" }
-emphasis = "#4ec9b0"
-"emphasis.strong" = { color = "#4ec9b0", weight = "bold" }
-link_uri = { color = "#6a9955", underline = true }
-link_text = { color = "#cb8f77", italic = true }
-list_marker = "#4e94ce"
-
-[workspace.disconnected_overlay]
-extends = "$text.base"
-color = "#ffffff"
-background = "#000000aa"
@@ -1,67 +0,0 @@
-extends = "_base"
-
-[surface]
-0 = "#EAEAEB"
-1 = "#FAFAFA"
-2 = "#FFFFFF"
-
-[border]
-0 = "#DDDDDC"
-1 = "#0000000F"
-
-[text]
-0 = { extends = "$text.base", color = "#000000" }
-1 = { extends = "$text.base", color = "#29292B" }
-2 = { extends = "$text.base", color = "#7E7E83" }
-3 = { extends = "$text.base", color = "#939393" }
-
-[shadow]
-0 = "#0000000D"
-
-[selection]
-host = { selection = "#3B57BC55", cursor = "$text.0.color" }
-guests = [
- { selection = "#D0453B33", cursor = "#D0453B" },
- { selection = "#3B874B33", cursor = "#3B874B" },
- { selection = "#BD7CB433", cursor = "#BD7CB4" },
- { selection = "#EE823133", cursor = "#EE8231" },
- { selection = "#5A2B9233", cursor = "#5A2B92" },
- { selection = "#FDF35133", cursor = "#FDF351" },
- { selection = "#4EACAD33", cursor = "#4EACAD" },
-]
-
-[status]
-good = "#4fac63"
-info = "#3c5dd4"
-warn = "#faca50"
-bad = "#b7372e"
-
-[state]
-active_line = "#00000008"
-highlighted_line = "#faca5033"
-deleted_line = "#dd000036"
-inserted_line = "#00dd0036"
-hover = "#0000000D"
-selected = "#0000001c"
-
-[editor.syntax]
-keyword = { color = "#0000fa", weight = "bold" }
-function = "#795e26"
-string = "#a82121"
-type = "#267f29"
-number = "#b5cea8"
-comment = "#6a9955"
-property = "#4e94ce"
-variant = "#4fc1ff"
-constant = "#5a9ccc"
-title = { color = "#5a9ccc", weight = "bold" }
-emphasis = "#267f29"
-"emphasis.strong" = { color = "#267f29", weight = "bold" }
-link_uri = { color = "#6a9955", underline = true }
-link_text = { color = "#a82121", italic = true }
-list_marker = "#4e94ce"
-
-[workspace.disconnected_overlay]
-extends = "$text.base"
-color = "#ffffff"
-background = "#000000cc"
@@ -47,6 +47,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
tree_sitter_typescript::language_typescript(),
Some(Arc::new(typescript::TypeScriptLspAdapter)),
),
+ (
+ "javascript",
+ tree_sitter_typescript::language_tsx(),
+ Some(Arc::new(typescript::TypeScriptLspAdapter)),
+ ),
] {
languages.add(Arc::new(language(name, grammar, lsp_adapter)));
}
@@ -0,0 +1,5 @@
+("(" @open ")" @close)
+("[" @open "]" @close)
+("{" @open "}" @close)
+("<" @open ">" @close)
+("\"" @open "\"" @close)
@@ -0,0 +1,12 @@
+name = "JavaScript"
+path_suffixes = ["js", "jsx"]
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+ { start = "{", end = "}", close = true, newline = true },
+ { start = "[", end = "]", close = true, newline = true },
+ { start = "(", end = ")", close = true, newline = true },
+ { start = "<", end = ">", close = false, newline = true },
+ { start = "\"", end = "\"", close = true, newline = false },
+ { start = "/*", end = " */", close = true, newline = false },
+]
@@ -0,0 +1,219 @@
+; Variables
+
+(identifier) @variable
+
+; Properties
+
+(property_identifier) @property
+
+; Function and method calls
+
+(call_expression
+ function: (identifier) @function)
+
+(call_expression
+ function: (member_expression
+ property: (property_identifier) @function.method))
+
+; Function and method definitions
+
+(function
+ name: (identifier) @function)
+(function_declaration
+ name: (identifier) @function)
+(method_definition
+ name: (property_identifier) @function.method)
+
+(pair
+ key: (property_identifier) @function.method
+ value: [(function) (arrow_function)])
+
+(assignment_expression
+ left: (member_expression
+ property: (property_identifier) @function.method)
+ right: [(function) (arrow_function)])
+
+(variable_declarator
+ name: (identifier) @function
+ value: [(function) (arrow_function)])
+
+(assignment_expression
+ left: (identifier) @function
+ right: [(function) (arrow_function)])
+
+; Special identifiers
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+([
+ (identifier)
+ (shorthand_property_identifier)
+ (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^[A-Z_][A-Z\\d_]+$"))
+
+; Literals
+
+(this) @variable.builtin
+(super) @variable.builtin
+
+[
+ (true)
+ (false)
+ (null)
+ (undefined)
+] @constant.builtin
+
+(comment) @comment
+
+[
+ (string)
+ (template_string)
+] @string
+
+(regex) @string.special
+(number) @number
+
+; Tokens
+
+(template_substitution
+ "${" @punctuation.special
+ "}" @punctuation.special) @embedded
+
+[
+ ";"
+ "?."
+ "."
+ ","
+] @punctuation.delimiter
+
+[
+ "-"
+ "--"
+ "-="
+ "+"
+ "++"
+ "+="
+ "*"
+ "*="
+ "**"
+ "**="
+ "/"
+ "/="
+ "%"
+ "%="
+ "<"
+ "<="
+ "<<"
+ "<<="
+ "="
+ "=="
+ "==="
+ "!"
+ "!="
+ "!=="
+ "=>"
+ ">"
+ ">="
+ ">>"
+ ">>="
+ ">>>"
+ ">>>="
+ "~"
+ "^"
+ "&"
+ "|"
+ "^="
+ "&="
+ "|="
+ "&&"
+ "||"
+ "??"
+ "&&="
+ "||="
+ "??="
+] @operator
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "as"
+ "async"
+ "await"
+ "break"
+ "case"
+ "catch"
+ "class"
+ "const"
+ "continue"
+ "debugger"
+ "default"
+ "delete"
+ "do"
+ "else"
+ "export"
+ "extends"
+ "finally"
+ "for"
+ "from"
+ "function"
+ "get"
+ "if"
+ "import"
+ "in"
+ "instanceof"
+ "let"
+ "new"
+ "of"
+ "return"
+ "set"
+ "static"
+ "switch"
+ "target"
+ "throw"
+ "try"
+ "typeof"
+ "var"
+ "void"
+ "while"
+ "with"
+ "yield"
+] @keyword
+
+; Types
+
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(type_arguments
+ "<" @punctuation.bracket
+ ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+ "declare"
+ "enum"
+ "export"
+ "implements"
+ "interface"
+ "keyof"
+ "namespace"
+ "private"
+ "protected"
+ "public"
+ "type"
+ "readonly"
+ "override"
+] @keyword
@@ -0,0 +1,15 @@
+[
+ (call_expression)
+ (assignment_expression)
+ (member_expression)
+ (lexical_declaration)
+ (variable_declaration)
+ (assignment_expression)
+ (if_statement)
+ (for_statement)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent
@@ -0,0 +1,55 @@
+(internal_module
+ "namespace" @context
+ name: (_) @name) @item
+
+(enum_declaration
+ "enum" @context
+ name: (_) @name) @item
+
+(function_declaration
+ "async"? @context
+ "function" @context
+ name: (_) @name
+ parameters: (formal_parameters
+ "(" @context
+ ")" @context)) @item
+
+(interface_declaration
+ "interface" @context
+ name: (_) @name) @item
+
+(program
+ (lexical_declaration
+ ["let" "const"] @context
+ (variable_declarator
+ name: (_) @name) @item))
+
+(class_declaration
+ "class" @context
+ name: (_) @name) @item
+
+(method_definition
+ [
+ "get"
+ "set"
+ "async"
+ "*"
+ "readonly"
+ "static"
+ (override_modifier)
+ (accessibility_modifier)
+ ]* @context
+ name: (_) @name
+ parameters: (formal_parameters
+ "(" @context
+ ")" @context)) @item
+
+(public_field_definition
+ [
+ "declare"
+ "readonly"
+ "abstract"
+ "static"
+ (accessibility_modifier)
+ ]* @context
+ name: (_) @name) @item
@@ -1,5 +1,5 @@
name = "TSX"
-path_suffixes = ["tsx", "js"]
+path_suffixes = ["tsx"]
line_comment = "// "
autoclose_before = ";:.,=}])>"
brackets = [
@@ -2,30 +2,38 @@
#![allow(non_snake_case)]
use anyhow::{anyhow, Context, Result};
+use assets::Assets;
+use cli::{
+ ipc::{self, IpcSender},
+ CliRequest, CliResponse, IpcHandshake,
+};
use client::{self, http, ChannelList, UserStore};
use fs::OpenOptions;
-use futures::{channel::oneshot, StreamExt};
-use gpui::{App, AssetSource, Task};
+use futures::{
+ channel::{mpsc, oneshot},
+ FutureExt, SinkExt, StreamExt,
+};
+use gpui::{App, AssetSource, AsyncAppContext, Task};
use log::LevelFilter;
use parking_lot::Mutex;
use project::Fs;
+use settings::{self, KeymapFile, Settings, SettingsFileContent};
use smol::process::Command;
-use std::{env, fs, path::PathBuf, sync::Arc};
+use std::{env, fs, path::PathBuf, sync::Arc, thread, time::Duration};
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
use util::ResultExt;
-use workspace::{
- self,
- settings::{self, SettingsFile},
- AppState, OpenNew, OpenParams, OpenPaths, Settings,
-};
+use workspace::{self, AppState, OpenNew, OpenPaths};
use zed::{
- self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
+ self, build_window_options, build_workspace,
+ fs::RealFs,
+ languages, menus,
+ settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
};
fn main() {
init_logger();
- let app = gpui::App::new(Assets).unwrap();
+ let mut app = gpui::App::new(Assets).unwrap();
load_embedded_fonts(&app);
let fs = Arc::new(RealFs);
@@ -46,8 +54,37 @@ fn main() {
soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
..Default::default()
},
+ )
+ .with_overrides(
+ "Rust",
+ settings::LanguageOverride {
+ tab_size: Some(4),
+ ..Default::default()
+ },
+ )
+ .with_overrides(
+ "JavaScript",
+ settings::LanguageOverride {
+ tab_size: Some(2),
+ ..Default::default()
+ },
+ )
+ .with_overrides(
+ "TypeScript",
+ settings::LanguageOverride {
+ tab_size: Some(2),
+ ..Default::default()
+ },
+ )
+ .with_overrides(
+ "TSX",
+ settings::LanguageOverride {
+ tab_size: Some(2),
+ ..Default::default()
+ },
);
- let settings_file = load_settings_file(&app, fs.clone());
+
+ let config_files = load_config_files(&app, fs.clone());
let login_shell_env_loaded = if stdout_is_a_pty() {
Task::ready(())
@@ -57,6 +94,18 @@ fn main() {
})
};
+ let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
+ app.on_open_urls(move |urls, _| {
+ if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
+ if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
+ cli_connections_tx
+ .unbounded_send(cli_connection)
+ .map_err(|_| anyhow!("no listener for cli connections"))
+ .log_err();
+ };
+ }
+ });
+
app.run(move |cx| {
let http = http::client();
let client = client::Client::new(http.clone());
@@ -69,6 +118,7 @@ fn main() {
project::Project::init(&client);
client::Channel::init(&client);
client::init(client.clone(), cx);
+ command_palette::init(cx);
workspace::init(&client, cx);
editor::init(cx);
go_to_line::init(cx);
@@ -97,13 +147,16 @@ fn main() {
})
.detach_and_log_err(cx);
- let settings_file = cx.background().block(settings_file).unwrap();
- let mut settings_rx = Settings::from_files(
+ let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
+ let mut settings_rx = settings_from_files(
default_settings,
vec![settings_file],
themes.clone(),
cx.font_cache().clone(),
);
+
+ cx.spawn(|cx| watch_keymap_file(keymap_file, cx)).detach();
+
let settings = cx.background().block(settings_rx.next()).unwrap();
cx.spawn(|mut cx| async move {
while let Some(settings) = settings_rx.next().await {
@@ -130,20 +183,32 @@ fn main() {
build_workspace,
});
journal::init(app_state.clone(), cx);
+ theme_selector::init(cx);
zed::init(&app_state, cx);
- theme_selector::init(app_state.themes.clone(), cx);
cx.set_menus(menus::menus(&app_state.clone()));
if stdout_is_a_pty() {
cx.platform().activate(true);
- }
-
- let paths = collect_path_args();
- if paths.is_empty() {
- cx.dispatch_global_action(OpenNew(app_state.clone()));
+ let paths = collect_path_args();
+ if paths.is_empty() {
+ cx.dispatch_global_action(OpenNew(app_state.clone()));
+ } else {
+ cx.dispatch_global_action(OpenPaths { paths, app_state });
+ }
} else {
- cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
+ if let Ok(Some(connection)) = cli_connections_rx.try_next() {
+ cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
+ .detach();
+ } else {
+ cx.dispatch_global_action(OpenNew(app_state.clone()));
+ }
+ cx.spawn(|cx| async move {
+ while let Some(connection) = cli_connections_rx.next().await {
+ handle_cli_connection(connection, app_state.clone(), cx.clone()).await;
+ }
+ })
+ .detach();
}
});
}
@@ -239,15 +304,137 @@ fn load_embedded_fonts(app: &App) {
.unwrap();
}
-fn load_settings_file(app: &App, fs: Arc<dyn Fs>) -> oneshot::Receiver<SettingsFile> {
+fn load_config_files(
+ app: &App,
+ fs: Arc<dyn Fs>,
+) -> oneshot::Receiver<(
+ WatchedJsonFile<SettingsFileContent>,
+ WatchedJsonFile<KeymapFile>,
+)> {
let executor = app.background();
let (tx, rx) = oneshot::channel();
executor
.clone()
.spawn(async move {
- let file = SettingsFile::new(fs, &executor, zed::SETTINGS_PATH.clone()).await;
- tx.send(file).ok()
+ let settings_file =
+ WatchedJsonFile::new(fs.clone(), &executor, zed::SETTINGS_PATH.clone()).await;
+ let keymap_file = WatchedJsonFile::new(fs, &executor, zed::KEYMAP_PATH.clone()).await;
+ tx.send((settings_file, keymap_file)).ok()
})
.detach();
rx
}
+
+fn connect_to_cli(
+ server_name: &str,
+) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
+ let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
+ .context("error connecting to cli")?;
+ let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
+ let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
+
+ handshake_tx
+ .send(IpcHandshake {
+ requests: request_tx,
+ responses: response_rx,
+ })
+ .context("error sending ipc handshake")?;
+
+ let (mut async_request_tx, async_request_rx) =
+ futures::channel::mpsc::channel::<CliRequest>(16);
+ thread::spawn(move || {
+ while let Ok(cli_request) = request_rx.recv() {
+ if smol::block_on(async_request_tx.send(cli_request)).is_err() {
+ break;
+ }
+ }
+ Ok::<_, anyhow::Error>(())
+ });
+
+ Ok((async_request_rx, response_tx))
+}
+
+async fn handle_cli_connection(
+ (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
+ app_state: Arc<AppState>,
+ mut cx: AsyncAppContext,
+) {
+ if let Some(request) = requests.next().await {
+ match request {
+ CliRequest::Open { paths, wait } => {
+ let (workspace, items) = cx
+ .update(|cx| workspace::open_paths(&paths, &app_state, cx))
+ .await;
+
+ let mut errored = false;
+ let mut futures = Vec::new();
+ cx.update(|cx| {
+ for (item, path) in items.into_iter().zip(&paths) {
+ match item {
+ Some(Ok(item)) => {
+ let released = oneshot::channel();
+ item.on_release(
+ cx,
+ Box::new(move |_| {
+ let _ = released.0.send(());
+ }),
+ )
+ .detach();
+ futures.push(released.1);
+ }
+ Some(Err(err)) => {
+ responses
+ .send(CliResponse::Stderr {
+ message: format!("error opening {:?}: {}", path, err),
+ })
+ .log_err();
+ errored = true;
+ }
+ None => {}
+ }
+ }
+ });
+
+ if wait {
+ let background = cx.background();
+ let wait = async move {
+ if paths.is_empty() {
+ let (done_tx, done_rx) = oneshot::channel();
+ let _subscription = cx.update(|cx| {
+ cx.observe_release(&workspace, move |_, _| {
+ let _ = done_tx.send(());
+ })
+ });
+ drop(workspace);
+ let _ = done_rx.await;
+ } else {
+ let _ = futures::future::try_join_all(futures).await;
+ };
+ }
+ .fuse();
+ futures::pin_mut!(wait);
+
+ loop {
+ // Repeatedly check if CLI is still open to avoid wasting resources
+ // waiting for files or workspaces to close.
+ let mut timer = background.timer(Duration::from_secs(1)).fuse();
+ futures::select_biased! {
+ _ = wait => break,
+ _ = timer => {
+ if responses.send(CliResponse::Ping).is_err() {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ responses
+ .send(CliResponse::Exit {
+ status: if errored { 1 } else { 0 },
+ })
+ .log_err();
+ }
+ }
+ }
+}
@@ -16,7 +16,13 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
MenuItem::Action {
name: "Check for Updates",
keystroke: None,
- action: Box::new(super::CheckForUpdates),
+ action: Box::new(auto_update::Check),
+ },
+ MenuItem::Separator,
+ MenuItem::Action {
+ name: "Install CLI",
+ keystroke: None,
+ action: Box::new(super::InstallCommandLineInterface),
},
MenuItem::Separator,
MenuItem::Action {
@@ -0,0 +1,171 @@
+use futures::{stream, StreamExt};
+use gpui::{executor, AsyncAppContext, FontCache};
+use postage::sink::Sink as _;
+use postage::{prelude::Stream, watch};
+use project::Fs;
+use serde::Deserialize;
+use settings::{KeymapFile, Settings, SettingsFileContent};
+use std::{path::Path, sync::Arc, time::Duration};
+use theme::ThemeRegistry;
+use util::ResultExt;
+
+#[derive(Clone)]
+pub struct WatchedJsonFile<T>(watch::Receiver<T>);
+
+impl<T> WatchedJsonFile<T>
+where
+ T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
+{
+ pub async fn new(
+ fs: Arc<dyn Fs>,
+ executor: &executor::Background,
+ path: impl Into<Arc<Path>>,
+ ) -> Self {
+ let path = path.into();
+ let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
+ let mut events = fs.watch(&path, Duration::from_millis(500)).await;
+ let (mut tx, rx) = watch::channel_with(settings);
+ executor
+ .spawn(async move {
+ while events.next().await.is_some() {
+ if let Some(settings) = Self::load(fs.clone(), &path).await {
+ if tx.send(settings).await.is_err() {
+ break;
+ }
+ }
+ }
+ })
+ .detach();
+ Self(rx)
+ }
+
+ async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
+ if fs.is_file(&path).await {
+ fs.load(&path)
+ .await
+ .log_err()
+ .and_then(|data| serde_json::from_str(&data).log_err())
+ } else {
+ Some(T::default())
+ }
+ }
+}
+
+pub fn settings_from_files(
+ defaults: Settings,
+ sources: Vec<WatchedJsonFile<SettingsFileContent>>,
+ theme_registry: Arc<ThemeRegistry>,
+ font_cache: Arc<FontCache>,
+) -> impl futures::stream::Stream<Item = Settings> {
+ stream::select_all(sources.iter().enumerate().map(|(i, source)| {
+ let mut rx = source.0.clone();
+ // Consume the initial item from all of the constituent file watches but one.
+ // This way, the stream will yield exactly one item for the files' initial
+ // state, and won't return any more items until the files change.
+ if i > 0 {
+ rx.try_recv().ok();
+ }
+ rx
+ }))
+ .map(move |_| {
+ let mut settings = defaults.clone();
+ for source in &sources {
+ settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
+ }
+ settings
+ })
+}
+
+pub async fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFile>, mut cx: AsyncAppContext) {
+ while let Some(content) = file.0.recv().await {
+ cx.update(|cx| {
+ cx.clear_bindings();
+ settings::KeymapFile::load_defaults(cx);
+ content.add(cx).log_err();
+ });
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use project::FakeFs;
+ use settings::SoftWrap;
+
+ #[gpui::test]
+ async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
+ let executor = cx.background();
+ let fs = FakeFs::new(executor.clone());
+
+ fs.save(
+ "/settings1.json".as_ref(),
+ &r#"
+ {
+ "buffer_font_size": 24,
+ "soft_wrap": "editor_width",
+ "language_overrides": {
+ "Markdown": {
+ "preferred_line_length": 100,
+ "soft_wrap": "preferred_line_length"
+ }
+ }
+ }
+ "#
+ .into(),
+ )
+ .await
+ .unwrap();
+
+ let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
+ let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
+ let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
+
+ let mut settings_rx = settings_from_files(
+ cx.read(Settings::test),
+ vec![source1, source2, source3],
+ ThemeRegistry::new((), cx.font_cache()),
+ cx.font_cache(),
+ );
+
+ let settings = settings_rx.next().await.unwrap();
+ let md_settings = settings.language_overrides.get("Markdown").unwrap();
+ assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
+ assert_eq!(settings.buffer_font_size, 24.0);
+ assert_eq!(settings.tab_size, 4);
+ assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+ assert_eq!(md_settings.preferred_line_length, Some(100));
+
+ fs.save(
+ "/settings2.json".as_ref(),
+ &r#"
+ {
+ "tab_size": 2,
+ "soft_wrap": "none",
+ "language_overrides": {
+ "Markdown": {
+ "preferred_line_length": 120
+ }
+ }
+ }
+ "#
+ .into(),
+ )
+ .await
+ .unwrap();
+
+ let settings = settings_rx.next().await.unwrap();
+ let md_settings = settings.language_overrides.get("Markdown").unwrap();
+ assert_eq!(settings.soft_wrap, SoftWrap::None);
+ assert_eq!(settings.buffer_font_size, 24.0);
+ assert_eq!(settings.tab_size, 2);
+ assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
+ assert_eq!(md_settings.preferred_line_length, Some(120));
+
+ fs.remove_file("/settings2.json".as_ref(), Default::default())
+ .await
+ .unwrap();
+
+ let settings = settings_rx.next().await.unwrap();
+ assert_eq!(settings.tab_size, 4);
+ }
+}
@@ -1,11 +1,12 @@
-use crate::{assets::Assets, build_window_options, build_workspace, AppState};
+use crate::{build_window_options, build_workspace, AppState};
+use assets::Assets;
use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
use gpui::MutableAppContext;
use language::LanguageRegistry;
use project::fs::FakeFs;
+use settings::Settings;
use std::sync::Arc;
use theme::ThemeRegistry;
-use workspace::Settings;
#[cfg(test)]
#[ctor::ctor]
@@ -1,21 +1,22 @@
-pub mod assets;
pub mod languages;
pub mod menus;
+pub mod settings_file;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+use anyhow::{anyhow, Context, Result};
use breadcrumbs::Breadcrumbs;
use chat_panel::ChatPanel;
pub use client;
pub use contacts_panel;
use contacts_panel::ContactsPanel;
pub use editor;
+use editor::Editor;
use gpui::{
- action,
+ actions,
geometry::vector::vec2f,
- keymap::Binding,
platform::{WindowBounds, WindowOptions},
- ModelHandle, ViewContext,
+ AsyncAppContext, ModelHandle, ViewContext,
};
use lazy_static::lazy_static;
pub use lsp;
@@ -23,15 +24,28 @@ use project::Project;
pub use project::{self, fs};
use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar};
-use std::{path::PathBuf, sync::Arc};
+use serde_json::to_string_pretty;
+use settings::Settings;
+use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use util::ResultExt;
pub use workspace;
-use workspace::{AppState, Settings, Workspace, WorkspaceParams};
-
-action!(About);
-action!(Quit);
-action!(OpenSettings);
-action!(AdjustBufferFontSize, f32);
-action!(CheckForUpdates);
+use workspace::{AppState, Workspace, WorkspaceParams};
+
+actions!(
+ zed,
+ [
+ About,
+ Quit,
+ DebugElements,
+ OpenSettings,
+ IncreaseBufferFontSize,
+ DecreaseBufferFontSize,
+ InstallCommandLineInterface,
+ ]
+);
const MIN_FONT_SIZE: f32 = 6.0;
@@ -40,21 +54,27 @@ lazy_static! {
.expect("failed to determine home directory")
.join(".zed");
pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
+ pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json");
}
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
cx.add_global_action(quit);
- cx.add_global_action(|_: &CheckForUpdates, cx| auto_update::check(cx));
- cx.add_global_action({
- move |action: &AdjustBufferFontSize, cx| {
- cx.update_global::<Settings, _, _>(|settings, cx| {
- settings.buffer_font_size =
- (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE);
- cx.refresh_windows();
- });
- }
+ cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
+ cx.update_global::<Settings, _, _>(|settings, cx| {
+ settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
+ cx.refresh_windows();
+ });
+ });
+ cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
+ cx.update_global::<Settings, _, _>(|settings, cx| {
+ settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
+ cx.refresh_windows();
+ });
+ });
+ cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
+ cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
+ .detach_and_log_err(cx);
});
-
cx.add_action({
let app_state = app_state.clone();
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
@@ -93,14 +113,32 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
.detach_and_log_err(cx);
}
});
+ cx.add_action(
+ |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+ let content = to_string_pretty(&cx.debug_elements()).unwrap();
+ let project = workspace.project().clone();
+ let json_language = project.read(cx).languages().get_language("JSON").unwrap();
+ if project.read(cx).is_remote() {
+ cx.propagate_action();
+ } else if let Some(buffer) = project
+ .update(cx, |project, cx| {
+ project.create_buffer(&content, Some(json_language), cx)
+ })
+ .log_err()
+ {
+ workspace.add_item(
+ Box::new(
+ cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
+ ),
+ cx,
+ );
+ }
+ },
+ );
workspace::lsp_status::init(cx);
- cx.add_bindings(vec![
- Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
- Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
- Binding::new("cmd-,", OpenSettings, None),
- ])
+ settings::KeymapFile::load_defaults(cx);
}
pub fn build_workspace(
@@ -131,19 +169,23 @@ pub fn build_workspace(
client: app_state.client.clone(),
fs: app_state.fs.clone(),
languages: app_state.languages.clone(),
+ themes: app_state.themes.clone(),
user_store: app_state.user_store.clone(),
channel_list: app_state.channel_list.clone(),
};
let mut workspace = Workspace::new(&workspace_params, cx);
let project = workspace.project().clone();
+ let theme_names = app_state.themes.list().collect();
+ let language_names = app_state.languages.language_names();
+
project.update(cx, |project, _| {
project.set_language_server_settings(serde_json::json!({
"json": {
"schemas": [
{
"fileMatch": "**/.zed/settings.json",
- "schema": Settings::file_json_schema(),
+ "schema": Settings::file_json_schema(theme_names, language_names),
}
]
}
@@ -199,11 +241,58 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
cx.platform().quit();
}
+async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
+ let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
+ let link_path = Path::new("/usr/local/bin/zed");
+ let bin_dir_path = link_path.parent().unwrap();
+
+ // Don't re-create symlink if it points to the same CLI binary.
+ if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
+ return Ok(());
+ }
+
+ // If the symlink is not there or is outdated, first try replacing it
+ // without escalating.
+ smol::fs::remove_file(link_path).await.log_err();
+ if smol::fs::unix::symlink(&cli_path, link_path)
+ .await
+ .log_err()
+ .is_some()
+ {
+ return Ok(());
+ }
+
+ // The symlink could not be created, so use osascript with admin privileges
+ // to create it.
+ let status = smol::process::Command::new("osascript")
+ .args([
+ "-e",
+ &format!(
+ "do shell script \" \
+ mkdir -p \'{}\' && \
+ ln -sf \'{}\' \'{}\' \
+ \" with administrator privileges",
+ bin_dir_path.to_string_lossy(),
+ cli_path.to_string_lossy(),
+ link_path.to_string_lossy(),
+ ),
+ ])
+ .stdout(smol::process::Stdio::inherit())
+ .stderr(smol::process::Stdio::inherit())
+ .output()
+ .await?
+ .status;
+ if status.success() {
+ Ok(())
+ } else {
+ Err(anyhow!("error running osascript"))
+ }
+}
+
#[cfg(test)]
mod tests {
- use crate::assets::Assets;
-
use super::*;
+ use assets::Assets;
use editor::{DisplayPoint, Editor};
use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
use project::{Fs, ProjectPath};
@@ -567,7 +656,7 @@ mod tests {
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
// Create a new untitled buffer
- cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
+ cx.dispatch_action(window_id, OpenNew(app_state.clone()));
let editor = workspace.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
@@ -580,7 +669,7 @@ mod tests {
assert!(!editor.is_dirty(cx));
assert_eq!(editor.title(cx), "untitled");
assert!(Arc::ptr_eq(
- editor.language(cx).unwrap(),
+ editor.language_at(0, cx).unwrap(),
&languages::PLAIN_TEXT
));
editor.handle_input(&editor::Input("hi".into()), cx);
@@ -604,7 +693,7 @@ mod tests {
editor.read_with(cx, |editor, cx| {
assert!(!editor.is_dirty(cx));
assert_eq!(editor.title(cx), "the-new-name.rs");
- assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust");
+ assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
});
// Edit the file and save it again. This time, there is no filename prompt.
@@ -622,7 +711,7 @@ mod tests {
// Open the same newly-created file in another pane item. The new editor should reuse
// the same buffer.
- cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
+ cx.dispatch_action(window_id, OpenNew(app_state.clone()));
workspace
.update(cx, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
@@ -659,7 +748,7 @@ mod tests {
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
// Create a new untitled buffer
- cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
+ cx.dispatch_action(window_id, OpenNew(app_state.clone()));
let editor = workspace.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
@@ -670,7 +759,7 @@ mod tests {
editor.update(cx, |editor, cx| {
assert!(Arc::ptr_eq(
- editor.language(cx).unwrap(),
+ editor.language_at(0, cx).unwrap(),
&languages::PLAIN_TEXT
));
editor.handle_input(&editor::Input("hi".into()), cx);
@@ -684,7 +773,7 @@ mod tests {
// The buffer is not dirty anymore and the language is assigned based on the path.
editor.read_with(cx, |editor, cx| {
assert!(!editor.is_dirty(cx));
- assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust")
+ assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
});
}
@@ -729,32 +818,47 @@ mod tests {
.update(cx, |w, cx| w.open_path(file1.clone(), cx))
.await
.unwrap();
- cx.read(|cx| {
- assert_eq!(
- pane_1.read(cx).active_item().unwrap().project_path(cx),
- Some(file1.clone())
- );
+
+ let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
+ let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+ assert_eq!(editor.project_path(cx), Some(file1.clone()));
+ let buffer = editor.update(cx, |editor, cx| {
+ editor.insert("dirt", cx);
+ editor.buffer().downgrade()
+ });
+ (editor.downgrade(), buffer)
});
- cx.dispatch_action(
- window_id,
- vec![pane_1.id()],
- pane::Split(SplitDirection::Right),
- );
- cx.update(|cx| {
+ cx.dispatch_action(window_id, pane::Split(SplitDirection::Right));
+ let editor_2 = cx.update(|cx| {
let pane_2 = workspace.read(cx).active_pane().clone();
assert_ne!(pane_1, pane_2);
let pane2_item = pane_2.read(cx).active_item().unwrap();
assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
- cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem);
+ pane2_item.downcast::<Editor>().unwrap().downgrade()
});
+ cx.dispatch_action(window_id, workspace::CloseActiveItem);
+
cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, _| {
assert_eq!(workspace.panes().len(), 1);
assert_eq!(workspace.active_pane(), &pane_1);
});
+
+ cx.dispatch_action(window_id, workspace::CloseActiveItem);
+ cx.foreground().run_until_parked();
+ cx.simulate_prompt_answer(window_id, 1);
+ cx.foreground().run_until_parked();
+
+ workspace.read_with(cx, |workspace, cx| {
+ assert!(workspace.active_item(cx).is_none());
+ });
+
+ cx.assert_dropped(editor_1);
+ cx.assert_dropped(editor_2);
+ cx.assert_dropped(buffer);
}
#[gpui::test]
@@ -882,11 +986,10 @@ mod tests {
.update(cx, |workspace, cx| {
let editor3_id = editor3.id();
drop(editor3);
- workspace
- .active_pane()
- .update(cx, |pane, cx| pane.close_item(editor3_id, cx))
+ Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
})
- .await;
+ .await
+ .unwrap();
workspace
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
.await;
@@ -900,11 +1003,10 @@ mod tests {
.update(cx, |workspace, cx| {
let editor2_id = editor2.id();
drop(editor2);
- workspace
- .active_pane()
- .update(cx, |pane, cx| pane.close_item(editor2_id, cx))
+ Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
})
- .await;
+ .await
+ .unwrap();
app_state
.fs
.as_fake()
@@ -992,7 +1094,8 @@ mod tests {
lazy_static::lazy_static! {
static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
static ref FONTS: Vec<Arc<Vec<u8>>> = vec![
- Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into()
+ Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap().to_vec().into(),
+ Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap().to_vec().into(),
];
}
@@ -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
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+set -e
+
+cd styles
+npm install
+npm run build
@@ -4,24 +4,33 @@ set -e
export ZED_BUNDLE=true
-# Install cargo-bundle 0.5.0 if it's not already installed
+echo "Installing cargo bundle"
cargo install cargo-bundle --version 0.5.0
# Deal with versions of macOS that don't include libstdc++ headers
export CXXFLAGS="-stdlib=libc++"
-# Build the app bundle for x86_64
-pushd crates/zed > /dev/null
-cargo bundle --release --target x86_64-apple-darwin
-popd > /dev/null
+echo "Compiling binaries"
+cargo build --release --package zed --target aarch64-apple-darwin
+cargo build --release --package zed --target x86_64-apple-darwin
+cargo build --release --package cli --target aarch64-apple-darwin
+cargo build --release --package cli --target x86_64-apple-darwin
-# Build the binary for aarch64 (Apple M1)
-cargo build --release --target aarch64-apple-darwin
+echo "Creating application bundle"
+(cd crates/zed && cargo bundle --release --target x86_64-apple-darwin)
-# Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries
-lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
+echo "Creating fat binaries"
+lipo \
+ -create \
+ target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/Zed \
+ -output \
+ target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
+lipo \
+ -create \
+ target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/cli \
+ -output \
+ target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/cli
-# Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now.
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
echo "Signing bundle with Apple-issued certificate"
security create-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain || echo ""
@@ -39,7 +48,6 @@ else
codesign --force --deep --sign - target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v
fi
-# Create a DMG
echo "Creating DMG"
mkdir -p target/release
hdiutil create -volname Zed -srcfolder target/x86_64-apple-darwin/release/bundle/osx -ov -format UDZO target/release/Zed.dmg
@@ -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 -
@@ -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
@@ -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)"
@@ -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"
]
}
@@ -0,0 +1 @@
+node_modules/
@@ -0,0 +1,1155 @@
+{
+ "color": {
+ "neutral": {
+ "0": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "25": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ },
+ "50": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "75": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ },
+ "100": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "125": {
+ "value": "#dcdcdc",
+ "step": 125,
+ "type": "color"
+ },
+ "150": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "175": {
+ "value": "#cdcdcd",
+ "step": 175,
+ "type": "color"
+ },
+ "200": {
+ "value": "#c6c6c6",
+ "step": 200,
+ "type": "color"
+ },
+ "225": {
+ "value": "#bfbfbf",
+ "step": 225,
+ "type": "color"
+ },
+ "250": {
+ "value": "#b8b8b8",
+ "step": 250,
+ "type": "color"
+ },
+ "275": {
+ "value": "#b1b1b1",
+ "step": 275,
+ "type": "color"
+ },
+ "300": {
+ "value": "#aaaaaa",
+ "step": 300,
+ "type": "color"
+ },
+ "325": {
+ "value": "#a3a3a3",
+ "step": 325,
+ "type": "color"
+ },
+ "350": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "375": {
+ "value": "#959595",
+ "step": 375,
+ "type": "color"
+ },
+ "400": {
+ "value": "#8e8e8e",
+ "step": 400,
+ "type": "color"
+ },
+ "425": {
+ "value": "#878787",
+ "step": 425,
+ "type": "color"
+ },
+ "450": {
+ "value": "#808080",
+ "step": 450,
+ "type": "color"
+ },
+ "475": {
+ "value": "#787878",
+ "step": 475,
+ "type": "color"
+ },
+ "500": {
+ "value": "#717171",
+ "step": 500,
+ "type": "color"
+ },
+ "525": {
+ "value": "#6a6a6a",
+ "step": 525,
+ "type": "color"
+ },
+ "550": {
+ "value": "#636363",
+ "step": 550,
+ "type": "color"
+ },
+ "575": {
+ "value": "#5c5c5c",
+ "step": 575,
+ "type": "color"
+ },
+ "600": {
+ "value": "#555555",
+ "step": 600,
+ "type": "color"
+ },
+ "625": {
+ "value": "#4e4e4e",
+ "step": 625,
+ "type": "color"
+ },
+ "650": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "675": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ },
+ "700": {
+ "value": "#393939",
+ "step": 700,
+ "type": "color"
+ },
+ "725": {
+ "value": "#323232",
+ "step": 725,
+ "type": "color"
+ },
+ "750": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "775": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "800": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "825": {
+ "value": "#151515",
+ "step": 825,
+ "type": "color"
+ },
+ "850": {
+ "value": "#0e0e0e",
+ "step": 850,
+ "type": "color"
+ },
+ "875": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ },
+ "900": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "rose": {
+ "0": {
+ "value": "#feecef",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fcc5cf",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#fa9fae",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f8788e",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f5526e",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#f0284a",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#cd1434",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#97142a",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#64101e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#330a11",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "red": {
+ "0": {
+ "value": "#feecec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f9a0a0",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f57b7b",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#c91818",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#951515",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#631111",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#330a0a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "orange": {
+ "0": {
+ "value": "#fef3ec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fcd6bd",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#fab98e",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f99d5f",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f9812e",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#ee670a",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#bb550e",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#8b4210",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#5d2f0e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#331b0a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "amber": {
+ "0": {
+ "value": "#fef7ec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fce2ba",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f9ce89",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f6a724",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#de900c",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#b0740f",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#845910",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#5a3e0e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#33240a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "yellow": {
+ "0": {
+ "value": "#fef9ec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f9da82",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f8cc4d",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#d3a20b",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#a8820e",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#7e630f",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#58460e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#33290a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "lime": {
+ "0": {
+ "value": "#f7feec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#dffab5",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#c7f57f",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#aeef4b",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#96e818",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#79ba16",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#639714",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#4e7412",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#38530f",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#23330a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "green": {
+ "0": {
+ "value": "#ecfef2",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#84f2ab",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#54e989",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#27dd69",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#20b456",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#157338",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#105328",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a3319",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "emerald": {
+ "0": {
+ "value": "#ecfef8",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b0fae1",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#74f6cb",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#39f0b3",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#12d796",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#10a977",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#118a62",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#106c4e",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0d4f3a",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a3326",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "teal": {
+ "0": {
+ "value": "#ecfefc",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b1faf2",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#76f5e7",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#3eeeda",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#14a898",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#138a7d",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#116c62",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0e4f48",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a332f",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "cyan": {
+ "0": {
+ "value": "#ecfcfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b2f3fb",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#78eaf9",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#3de2f8",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#07d5f1",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#09aac0",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#0c8a9a",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#0e6a75",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0d4c53",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a2f33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "sky": {
+ "0": {
+ "value": "#ecf8fe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b9e5fb",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#86d3f8",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#53c1f5",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#20b0f2",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#1096d3",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#1179a8",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#115c7f",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0e4158",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a2633",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "blue": {
+ "0": {
+ "value": "#ecf3fe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#9ec1fa",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#76a8f8",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#4f8ff7",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#135acd",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#134697",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#103063",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a1a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "indigo": {
+ "0": {
+ "value": "#ececfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#cdcdfc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#aeaff9",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#9091f6",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#7274f3",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#1b1edc",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#1819a1",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#121269",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a0a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "violet": {
+ "0": {
+ "value": "#f1ecfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#daccfc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#c3acfb",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#ac8cf9",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#966cf7",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#7741f2",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#5316e0",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#3f15a3",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#2b116a",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#160a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "purple": {
+ "0": {
+ "value": "#f5ecfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#e4cbfc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#d2a9fb",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#c188f9",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#b066f8",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#993bf3",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#7b14dd",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#5c14a1",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#3e1169",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#1f0a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "fuschia": {
+ "0": {
+ "value": "#fdecfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#f8c5fb",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f19ff6",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#e87af0",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#de57e8",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#d430e0",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#b31fbc",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#87198e",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#5c1260",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#310a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "pink": {
+ "0": {
+ "value": "#feecf5",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fbc6e1",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f8a1cc",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f47db8",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#e8318c",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#c71a71",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#941756",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#63113b",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#330a1f",
+ "step": 900,
+ "type": "color"
+ }
+ }
+ },
+ "text": {
+ "family": {
+ "sans": {
+ "value": "Zed Sans",
+ "type": "fontFamily"
+ },
+ "mono": {
+ "value": "Zed Mono",
+ "type": "fontFamily"
+ }
+ },
+ "weight": {
+ "thin": {
+ "value": "thin",
+ "type": "fontWeight"
+ },
+ "extra_light": {
+ "value": "extra_light",
+ "type": "fontWeight"
+ },
+ "light": {
+ "value": "light",
+ "type": "fontWeight"
+ },
+ "normal": {
+ "value": "normal",
+ "type": "fontWeight"
+ },
+ "medium": {
+ "value": "medium",
+ "type": "fontWeight"
+ },
+ "semibold": {
+ "value": "semibold",
+ "type": "fontWeight"
+ },
+ "bold": {
+ "value": "bold",
+ "type": "fontWeight"
+ },
+ "extra_bold": {
+ "value": "extra_bold",
+ "type": "fontWeight"
+ },
+ "black": {
+ "value": "black",
+ "type": "fontWeight"
+ }
+ }
+ },
+ "size": {
+ "3xs": {
+ "value": 8,
+ "type": "fontSize"
+ },
+ "2xs": {
+ "value": 10,
+ "type": "fontSize"
+ },
+ "xs": {
+ "value": 12,
+ "type": "fontSize"
+ },
+ "sm": {
+ "value": 14,
+ "type": "fontSize"
+ },
+ "md": {
+ "value": 16,
+ "type": "fontSize"
+ },
+ "lg": {
+ "value": 18,
+ "type": "fontSize"
+ },
+ "xl": {
+ "value": 20,
+ "type": "fontSize"
+ }
+ }
+}
@@ -0,0 +1,681 @@
+{
+ "meta": {
+ "themeName": "dark"
+ },
+ "text": {
+ "primary": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#808080",
+ "step": 450,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#4f8ff7",
+ "step": 400,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "error": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "info": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "icon": {
+ "primary": {
+ "value": "#c6c6c6",
+ "step": 200,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#555555",
+ "step": 600,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#393939",
+ "step": 700,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "error": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f6a724",
+ "step": 400,
+ "type": "color"
+ },
+ "info": {
+ "value": "#135acd",
+ "step": 600,
+ "type": "color"
+ }
+ },
+ "background": {
+ "100": {
+ "base": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#323232",
+ "step": 725,
+ "type": "color"
+ },
+ "active": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ }
+ },
+ "300": {
+ "base": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "active": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ }
+ },
+ "500": {
+ "base": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#ffffff14",
+ "step": 0,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#151515",
+ "step": 825,
+ "type": "color"
+ }
+ },
+ "on300": {
+ "base": {
+ "value": "#0e0e0e80",
+ "step": 850,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ }
+ },
+ "on500": {
+ "base": {
+ "value": "#0e0e0e",
+ "step": 850,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "active": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ }
+ },
+ "ok": {
+ "base": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "active": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ }
+ },
+ "error": {
+ "base": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "active": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "warning": {
+ "base": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "active": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ }
+ },
+ "info": {
+ "base": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "active": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ }
+ },
+ "border": {
+ "primary": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#20b456",
+ "step": 500,
+ "type": "color"
+ },
+ "error": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#de900c",
+ "step": 500,
+ "type": "color"
+ },
+ "info": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "editor": {
+ "background": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "indent_guide": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ },
+ "indent_guide_active": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "line": {
+ "active": {
+ "value": "#ffffff12",
+ "step": 0,
+ "type": "color"
+ },
+ "highlighted": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "inserted": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "deleted": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "modified": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "highlight": {
+ "selection": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "occurrence": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "activeOccurrence": {
+ "value": "#ffffff29",
+ "step": 0,
+ "type": "color"
+ },
+ "matchingBracket": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "match": {
+ "value": "#3f15a380",
+ "step": 700,
+ "type": "color"
+ },
+ "activeMatch": {
+ "value": "#5316e0b3",
+ "step": 600,
+ "type": "color"
+ },
+ "related": {
+ "value": "#151515",
+ "step": 825,
+ "type": "color"
+ }
+ },
+ "gutter": {
+ "primary": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ }
+ }
+ },
+ "syntax": {
+ "primary": {
+ "value": "#d5d5d5",
+ "type": "color"
+ },
+ "comment": {
+ "value": "#aaaaaa",
+ "type": "color"
+ },
+ "keyword": {
+ "value": "#4f8ff7",
+ "type": "color"
+ },
+ "function": {
+ "value": "#f9da82",
+ "type": "color"
+ },
+ "type": {
+ "value": "#3eeeda",
+ "type": "color"
+ },
+ "variant": {
+ "value": "#53c1f5",
+ "type": "color"
+ },
+ "property": {
+ "value": "#4f8ff7",
+ "type": "color"
+ },
+ "enum": {
+ "value": "#ee670a",
+ "type": "color"
+ },
+ "operator": {
+ "value": "#ee670a",
+ "type": "color"
+ },
+ "string": {
+ "value": "#f99d5f",
+ "type": "color"
+ },
+ "number": {
+ "value": "#aeef4b",
+ "type": "color"
+ },
+ "boolean": {
+ "value": "#aeef4b",
+ "type": "color"
+ }
+ },
+ "player": {
+ "1": {
+ "baseColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#2472f2cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "2": {
+ "baseColor": {
+ "value": "#79ba16",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#79ba16",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#79ba163d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#79ba16cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "3": {
+ "baseColor": {
+ "value": "#d430e0",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#d430e0",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#d430e03d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#d430e0cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "4": {
+ "baseColor": {
+ "value": "#ee670a",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#ee670a",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#ee670a3d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#ee670acc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "5": {
+ "baseColor": {
+ "value": "#993bf3",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#993bf3",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#993bf33d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#993bf3cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "6": {
+ "baseColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#16d6c13d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#16d6c1cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "7": {
+ "baseColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#ef59a33d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#ef59a3cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "8": {
+ "baseColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#f7bf173d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#f7bf17cc",
+ "step": 400,
+ "type": "color"
+ }
+ }
+ },
+ "shadowAlpha": {
+ "value": 0.32,
+ "type": "number"
+ }
+}
@@ -0,0 +1,681 @@
+{
+ "meta": {
+ "themeName": "light"
+ },
+ "text": {
+ "primary": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#636363",
+ "step": 550,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#808080",
+ "step": 450,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#20b456",
+ "step": 500,
+ "type": "color"
+ },
+ "error": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#d3a20b",
+ "step": 500,
+ "type": "color"
+ },
+ "info": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "icon": {
+ "primary": {
+ "value": "#393939",
+ "step": 700,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#717171",
+ "step": 500,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#aaaaaa",
+ "step": 300,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "error": {
+ "value": "#c91818",
+ "step": 600,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "info": {
+ "value": "#135acd",
+ "step": 600,
+ "type": "color"
+ }
+ },
+ "background": {
+ "100": {
+ "base": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "300": {
+ "base": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ },
+ "active": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ }
+ },
+ "500": {
+ "base": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#00000008",
+ "step": 900,
+ "type": "color"
+ },
+ "active": {
+ "value": "#0000000f",
+ "step": 900,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ }
+ },
+ "on300": {
+ "base": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "on500": {
+ "base": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ }
+ },
+ "ok": {
+ "base": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "error": {
+ "base": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "warning": {
+ "base": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "info": {
+ "base": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ }
+ }
+ },
+ "border": {
+ "primary": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "active": {
+ "value": "#b8b8b8",
+ "step": 250,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#84f2ab",
+ "step": 200,
+ "type": "color"
+ },
+ "error": {
+ "value": "#f9a0a0",
+ "step": 200,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f9da82",
+ "step": 200,
+ "type": "color"
+ },
+ "info": {
+ "value": "#9ec1fa",
+ "step": 200,
+ "type": "color"
+ }
+ },
+ "editor": {
+ "background": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "indent_guide": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "indent_guide_active": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "line": {
+ "active": {
+ "value": "#0000000f",
+ "step": 900,
+ "type": "color"
+ },
+ "highlighted": {
+ "value": "#0000001f",
+ "step": 900,
+ "type": "color"
+ },
+ "inserted": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "deleted": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "modified": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "highlight": {
+ "selection": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "occurrence": {
+ "value": "#0000000f",
+ "step": 900,
+ "type": "color"
+ },
+ "activeOccurrence": {
+ "value": "#00000029",
+ "step": 900,
+ "type": "color"
+ },
+ "matchingBracket": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "match": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "activeMatch": {
+ "value": "#f9da82",
+ "step": 200,
+ "type": "color"
+ },
+ "related": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ }
+ },
+ "gutter": {
+ "primary": {
+ "value": "#aaaaaa",
+ "step": 300,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ }
+ }
+ },
+ "syntax": {
+ "primary": {
+ "value": "#1c1c1c",
+ "type": "color"
+ },
+ "comment": {
+ "value": "#717171",
+ "type": "color"
+ },
+ "keyword": {
+ "value": "#1819a1",
+ "type": "color"
+ },
+ "function": {
+ "value": "#bb550e",
+ "type": "color"
+ },
+ "type": {
+ "value": "#a8820e",
+ "type": "color"
+ },
+ "variant": {
+ "value": "#97142a",
+ "type": "color"
+ },
+ "property": {
+ "value": "#106c4e",
+ "type": "color"
+ },
+ "enum": {
+ "value": "#eb2d2d",
+ "type": "color"
+ },
+ "operator": {
+ "value": "#eb2d2d",
+ "type": "color"
+ },
+ "string": {
+ "value": "#eb2d2d",
+ "type": "color"
+ },
+ "number": {
+ "value": "#484bed",
+ "type": "color"
+ },
+ "boolean": {
+ "value": "#eb2d2d",
+ "type": "color"
+ }
+ },
+ "player": {
+ "1": {
+ "baseColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#2472f2cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "2": {
+ "baseColor": {
+ "value": "#12d796",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#12d796",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#12d7963d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#12d796cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "3": {
+ "baseColor": {
+ "value": "#de57e8",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#de57e8",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#de57e83d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#de57e8cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "4": {
+ "baseColor": {
+ "value": "#f9812e",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#f9812e",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#f9812e3d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#f9812ecc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "5": {
+ "baseColor": {
+ "value": "#b066f8",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#b066f8",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#b066f83d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#b066f8cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "6": {
+ "baseColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#16d6c13d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#16d6c1cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "7": {
+ "baseColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#ef59a33d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#ef59a3cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "8": {
+ "baseColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#f7bf173d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#f7bf17cc",
+ "step": 400,
+ "type": "color"
+ }
+ }
+ },
+ "shadowAlpha": {
+ "value": 0.12,
+ "type": "number"
+ }
+}
@@ -0,0 +1,2519 @@
+{
+ "core": {
+ "color": {
+ "neutral": {
+ "0": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "25": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ },
+ "50": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "75": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ },
+ "100": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "125": {
+ "value": "#dcdcdc",
+ "step": 125,
+ "type": "color"
+ },
+ "150": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "175": {
+ "value": "#cdcdcd",
+ "step": 175,
+ "type": "color"
+ },
+ "200": {
+ "value": "#c6c6c6",
+ "step": 200,
+ "type": "color"
+ },
+ "225": {
+ "value": "#bfbfbf",
+ "step": 225,
+ "type": "color"
+ },
+ "250": {
+ "value": "#b8b8b8",
+ "step": 250,
+ "type": "color"
+ },
+ "275": {
+ "value": "#b1b1b1",
+ "step": 275,
+ "type": "color"
+ },
+ "300": {
+ "value": "#aaaaaa",
+ "step": 300,
+ "type": "color"
+ },
+ "325": {
+ "value": "#a3a3a3",
+ "step": 325,
+ "type": "color"
+ },
+ "350": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "375": {
+ "value": "#959595",
+ "step": 375,
+ "type": "color"
+ },
+ "400": {
+ "value": "#8e8e8e",
+ "step": 400,
+ "type": "color"
+ },
+ "425": {
+ "value": "#878787",
+ "step": 425,
+ "type": "color"
+ },
+ "450": {
+ "value": "#808080",
+ "step": 450,
+ "type": "color"
+ },
+ "475": {
+ "value": "#787878",
+ "step": 475,
+ "type": "color"
+ },
+ "500": {
+ "value": "#717171",
+ "step": 500,
+ "type": "color"
+ },
+ "525": {
+ "value": "#6a6a6a",
+ "step": 525,
+ "type": "color"
+ },
+ "550": {
+ "value": "#636363",
+ "step": 550,
+ "type": "color"
+ },
+ "575": {
+ "value": "#5c5c5c",
+ "step": 575,
+ "type": "color"
+ },
+ "600": {
+ "value": "#555555",
+ "step": 600,
+ "type": "color"
+ },
+ "625": {
+ "value": "#4e4e4e",
+ "step": 625,
+ "type": "color"
+ },
+ "650": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "675": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ },
+ "700": {
+ "value": "#393939",
+ "step": 700,
+ "type": "color"
+ },
+ "725": {
+ "value": "#323232",
+ "step": 725,
+ "type": "color"
+ },
+ "750": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "775": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "800": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "825": {
+ "value": "#151515",
+ "step": 825,
+ "type": "color"
+ },
+ "850": {
+ "value": "#0e0e0e",
+ "step": 850,
+ "type": "color"
+ },
+ "875": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ },
+ "900": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "rose": {
+ "0": {
+ "value": "#feecef",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fcc5cf",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#fa9fae",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f8788e",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f5526e",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#f0284a",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#cd1434",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#97142a",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#64101e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#330a11",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "red": {
+ "0": {
+ "value": "#feecec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f9a0a0",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f57b7b",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#c91818",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#951515",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#631111",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#330a0a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "orange": {
+ "0": {
+ "value": "#fef3ec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fcd6bd",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#fab98e",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f99d5f",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f9812e",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#ee670a",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#bb550e",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#8b4210",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#5d2f0e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#331b0a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "amber": {
+ "0": {
+ "value": "#fef7ec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fce2ba",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f9ce89",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f6a724",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#de900c",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#b0740f",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#845910",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#5a3e0e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#33240a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "yellow": {
+ "0": {
+ "value": "#fef9ec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f9da82",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f8cc4d",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#d3a20b",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#a8820e",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#7e630f",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#58460e",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#33290a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "lime": {
+ "0": {
+ "value": "#f7feec",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#dffab5",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#c7f57f",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#aeef4b",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#96e818",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#79ba16",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#639714",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#4e7412",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#38530f",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#23330a",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "green": {
+ "0": {
+ "value": "#ecfef2",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#84f2ab",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#54e989",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#27dd69",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#20b456",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#157338",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#105328",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a3319",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "emerald": {
+ "0": {
+ "value": "#ecfef8",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b0fae1",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#74f6cb",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#39f0b3",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#12d796",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#10a977",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#118a62",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#106c4e",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0d4f3a",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a3326",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "teal": {
+ "0": {
+ "value": "#ecfefc",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b1faf2",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#76f5e7",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#3eeeda",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#14a898",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#138a7d",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#116c62",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0e4f48",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a332f",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "cyan": {
+ "0": {
+ "value": "#ecfcfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b2f3fb",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#78eaf9",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#3de2f8",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#07d5f1",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#09aac0",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#0c8a9a",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#0e6a75",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0d4c53",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a2f33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "sky": {
+ "0": {
+ "value": "#ecf8fe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#b9e5fb",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#86d3f8",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#53c1f5",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#20b0f2",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#1096d3",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#1179a8",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#115c7f",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#0e4158",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a2633",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "blue": {
+ "0": {
+ "value": "#ecf3fe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#9ec1fa",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#76a8f8",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#4f8ff7",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#135acd",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#134697",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#103063",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a1a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "indigo": {
+ "0": {
+ "value": "#ececfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#cdcdfc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#aeaff9",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#9091f6",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#7274f3",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#1b1edc",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#1819a1",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#121269",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#0a0a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "violet": {
+ "0": {
+ "value": "#f1ecfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#daccfc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#c3acfb",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#ac8cf9",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#966cf7",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#7741f2",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#5316e0",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#3f15a3",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#2b116a",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#160a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "purple": {
+ "0": {
+ "value": "#f5ecfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#e4cbfc",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#d2a9fb",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#c188f9",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#b066f8",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#993bf3",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#7b14dd",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#5c14a1",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#3e1169",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#1f0a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "fuschia": {
+ "0": {
+ "value": "#fdecfe",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#f8c5fb",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f19ff6",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#e87af0",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#de57e8",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#d430e0",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#b31fbc",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#87198e",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#5c1260",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#310a33",
+ "step": 900,
+ "type": "color"
+ }
+ },
+ "pink": {
+ "0": {
+ "value": "#feecf5",
+ "step": 0,
+ "type": "color"
+ },
+ "100": {
+ "value": "#fbc6e1",
+ "step": 100,
+ "type": "color"
+ },
+ "200": {
+ "value": "#f8a1cc",
+ "step": 200,
+ "type": "color"
+ },
+ "300": {
+ "value": "#f47db8",
+ "step": 300,
+ "type": "color"
+ },
+ "400": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "500": {
+ "value": "#e8318c",
+ "step": 500,
+ "type": "color"
+ },
+ "600": {
+ "value": "#c71a71",
+ "step": 600,
+ "type": "color"
+ },
+ "700": {
+ "value": "#941756",
+ "step": 700,
+ "type": "color"
+ },
+ "800": {
+ "value": "#63113b",
+ "step": 800,
+ "type": "color"
+ },
+ "900": {
+ "value": "#330a1f",
+ "step": 900,
+ "type": "color"
+ }
+ }
+ },
+ "text": {
+ "family": {
+ "sans": {
+ "value": "Zed Sans",
+ "type": "fontFamily"
+ },
+ "mono": {
+ "value": "Zed Mono",
+ "type": "fontFamily"
+ }
+ },
+ "weight": {
+ "thin": {
+ "value": "thin",
+ "type": "fontWeight"
+ },
+ "extra_light": {
+ "value": "extra_light",
+ "type": "fontWeight"
+ },
+ "light": {
+ "value": "light",
+ "type": "fontWeight"
+ },
+ "normal": {
+ "value": "normal",
+ "type": "fontWeight"
+ },
+ "medium": {
+ "value": "medium",
+ "type": "fontWeight"
+ },
+ "semibold": {
+ "value": "semibold",
+ "type": "fontWeight"
+ },
+ "bold": {
+ "value": "bold",
+ "type": "fontWeight"
+ },
+ "extra_bold": {
+ "value": "extra_bold",
+ "type": "fontWeight"
+ },
+ "black": {
+ "value": "black",
+ "type": "fontWeight"
+ }
+ }
+ },
+ "size": {
+ "3xs": {
+ "value": 8,
+ "type": "fontSize"
+ },
+ "2xs": {
+ "value": 10,
+ "type": "fontSize"
+ },
+ "xs": {
+ "value": 12,
+ "type": "fontSize"
+ },
+ "sm": {
+ "value": 14,
+ "type": "fontSize"
+ },
+ "md": {
+ "value": 16,
+ "type": "fontSize"
+ },
+ "lg": {
+ "value": 18,
+ "type": "fontSize"
+ },
+ "xl": {
+ "value": 20,
+ "type": "fontSize"
+ }
+ }
+ },
+ "dark": {
+ "meta": {
+ "themeName": "dark"
+ },
+ "text": {
+ "primary": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#808080",
+ "step": 450,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#4f8ff7",
+ "step": 400,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "error": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "info": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "icon": {
+ "primary": {
+ "value": "#c6c6c6",
+ "step": 200,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#555555",
+ "step": 600,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#393939",
+ "step": 700,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "error": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f6a724",
+ "step": 400,
+ "type": "color"
+ },
+ "info": {
+ "value": "#135acd",
+ "step": 600,
+ "type": "color"
+ }
+ },
+ "background": {
+ "100": {
+ "base": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#323232",
+ "step": 725,
+ "type": "color"
+ },
+ "active": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ }
+ },
+ "300": {
+ "base": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "active": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ }
+ },
+ "500": {
+ "base": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#ffffff14",
+ "step": 0,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#151515",
+ "step": 825,
+ "type": "color"
+ }
+ },
+ "on300": {
+ "base": {
+ "value": "#0e0e0e80",
+ "step": 850,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ }
+ },
+ "on500": {
+ "base": {
+ "value": "#0e0e0e",
+ "step": 850,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ },
+ "active": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#1c1c1c",
+ "step": 800,
+ "type": "color"
+ }
+ },
+ "ok": {
+ "base": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "active": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ }
+ },
+ "error": {
+ "base": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "active": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "warning": {
+ "base": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "active": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f7bb57",
+ "step": 300,
+ "type": "color"
+ }
+ },
+ "info": {
+ "base": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "active": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ }
+ },
+ "border": {
+ "primary": {
+ "value": "#070707",
+ "step": 875,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#20b456",
+ "step": 500,
+ "type": "color"
+ },
+ "error": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#de900c",
+ "step": 500,
+ "type": "color"
+ },
+ "info": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "editor": {
+ "background": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "indent_guide": {
+ "value": "#404040",
+ "step": 675,
+ "type": "color"
+ },
+ "indent_guide_active": {
+ "value": "#232323",
+ "step": 775,
+ "type": "color"
+ },
+ "line": {
+ "active": {
+ "value": "#ffffff12",
+ "step": 0,
+ "type": "color"
+ },
+ "highlighted": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "inserted": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "deleted": {
+ "value": "#f15656",
+ "step": 400,
+ "type": "color"
+ },
+ "modified": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "highlight": {
+ "selection": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "occurrence": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "activeOccurrence": {
+ "value": "#ffffff29",
+ "step": 0,
+ "type": "color"
+ },
+ "matchingBracket": {
+ "value": "#ffffff1f",
+ "step": 0,
+ "type": "color"
+ },
+ "match": {
+ "value": "#3f15a380",
+ "step": 700,
+ "type": "color"
+ },
+ "activeMatch": {
+ "value": "#5316e0b3",
+ "step": 600,
+ "type": "color"
+ },
+ "related": {
+ "value": "#151515",
+ "step": 825,
+ "type": "color"
+ }
+ },
+ "gutter": {
+ "primary": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ }
+ }
+ },
+ "syntax": {
+ "primary": {
+ "value": "#d5d5d5",
+ "type": "color"
+ },
+ "comment": {
+ "value": "#aaaaaa",
+ "type": "color"
+ },
+ "keyword": {
+ "value": "#4f8ff7",
+ "type": "color"
+ },
+ "function": {
+ "value": "#f9da82",
+ "type": "color"
+ },
+ "type": {
+ "value": "#3eeeda",
+ "type": "color"
+ },
+ "variant": {
+ "value": "#53c1f5",
+ "type": "color"
+ },
+ "property": {
+ "value": "#4f8ff7",
+ "type": "color"
+ },
+ "enum": {
+ "value": "#ee670a",
+ "type": "color"
+ },
+ "operator": {
+ "value": "#ee670a",
+ "type": "color"
+ },
+ "string": {
+ "value": "#f99d5f",
+ "type": "color"
+ },
+ "number": {
+ "value": "#aeef4b",
+ "type": "color"
+ },
+ "boolean": {
+ "value": "#aeef4b",
+ "type": "color"
+ }
+ },
+ "player": {
+ "1": {
+ "baseColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#2472f2cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "2": {
+ "baseColor": {
+ "value": "#79ba16",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#79ba16",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#79ba163d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#79ba16cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "3": {
+ "baseColor": {
+ "value": "#d430e0",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#d430e0",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#d430e03d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#d430e0cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "4": {
+ "baseColor": {
+ "value": "#ee670a",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#ee670a",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#ee670a3d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#ee670acc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "5": {
+ "baseColor": {
+ "value": "#993bf3",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#993bf3",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#993bf33d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#993bf3cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "6": {
+ "baseColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#16d6c13d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#16d6c1cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "7": {
+ "baseColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#ef59a33d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#ef59a3cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "8": {
+ "baseColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#f7bf173d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#f7bf17cc",
+ "step": 400,
+ "type": "color"
+ }
+ }
+ },
+ "shadowAlpha": {
+ "value": 0.32,
+ "type": "number"
+ }
+ },
+ "light": {
+ "meta": {
+ "themeName": "light"
+ },
+ "text": {
+ "primary": {
+ "value": "#2b2b2b",
+ "step": 750,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#474747",
+ "step": 650,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#636363",
+ "step": 550,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#808080",
+ "step": 450,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#20b456",
+ "step": 500,
+ "type": "color"
+ },
+ "error": {
+ "value": "#eb2d2d",
+ "step": 500,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#d3a20b",
+ "step": 500,
+ "type": "color"
+ },
+ "info": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "icon": {
+ "primary": {
+ "value": "#393939",
+ "step": 700,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#717171",
+ "step": 500,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#9c9c9c",
+ "step": 350,
+ "type": "color"
+ },
+ "placeholder": {
+ "value": "#aaaaaa",
+ "step": 300,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ },
+ "feature": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#1b9447",
+ "step": 600,
+ "type": "color"
+ },
+ "error": {
+ "value": "#c91818",
+ "step": 600,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "info": {
+ "value": "#135acd",
+ "step": 600,
+ "type": "color"
+ }
+ },
+ "background": {
+ "100": {
+ "base": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "300": {
+ "base": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ },
+ "active": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#eaeaea",
+ "step": 75,
+ "type": "color"
+ }
+ },
+ "500": {
+ "base": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#00000008",
+ "step": 900,
+ "type": "color"
+ },
+ "active": {
+ "value": "#0000000f",
+ "step": 900,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ }
+ },
+ "on300": {
+ "base": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "on500": {
+ "base": {
+ "value": "#f1f1f1",
+ "step": 50,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ },
+ "active": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#f8f8f8",
+ "step": 25,
+ "type": "color"
+ }
+ },
+ "ok": {
+ "base": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "error": {
+ "base": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "warning": {
+ "base": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "info": {
+ "base": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "hovered": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "active": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ }
+ }
+ },
+ "border": {
+ "primary": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "secondary": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "muted": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "focused": {
+ "value": "#484bed",
+ "step": 500,
+ "type": "color"
+ },
+ "active": {
+ "value": "#b8b8b8",
+ "step": 250,
+ "type": "color"
+ },
+ "ok": {
+ "value": "#84f2ab",
+ "step": 200,
+ "type": "color"
+ },
+ "error": {
+ "value": "#f9a0a0",
+ "step": 200,
+ "type": "color"
+ },
+ "warning": {
+ "value": "#f9da82",
+ "step": 200,
+ "type": "color"
+ },
+ "info": {
+ "value": "#9ec1fa",
+ "step": 200,
+ "type": "color"
+ }
+ },
+ "editor": {
+ "background": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "indent_guide": {
+ "value": "#e3e3e3",
+ "step": 100,
+ "type": "color"
+ },
+ "indent_guide_active": {
+ "value": "#d5d5d5",
+ "step": 150,
+ "type": "color"
+ },
+ "line": {
+ "active": {
+ "value": "#0000000f",
+ "step": 900,
+ "type": "color"
+ },
+ "highlighted": {
+ "value": "#0000001f",
+ "step": 900,
+ "type": "color"
+ },
+ "inserted": {
+ "value": "#b7f9ce",
+ "step": 100,
+ "type": "color"
+ },
+ "deleted": {
+ "value": "#fcc6c6",
+ "step": 100,
+ "type": "color"
+ },
+ "modified": {
+ "value": "#c5dafc",
+ "step": 100,
+ "type": "color"
+ }
+ },
+ "highlight": {
+ "selection": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "occurrence": {
+ "value": "#0000000f",
+ "step": 900,
+ "type": "color"
+ },
+ "activeOccurrence": {
+ "value": "#00000029",
+ "step": 900,
+ "type": "color"
+ },
+ "matchingBracket": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ },
+ "match": {
+ "value": "#fce9b7",
+ "step": 100,
+ "type": "color"
+ },
+ "activeMatch": {
+ "value": "#f9da82",
+ "step": 200,
+ "type": "color"
+ },
+ "related": {
+ "value": "#ffffff",
+ "step": 0,
+ "type": "color"
+ }
+ },
+ "gutter": {
+ "primary": {
+ "value": "#aaaaaa",
+ "step": 300,
+ "type": "color"
+ },
+ "active": {
+ "value": "#000000",
+ "step": 900,
+ "type": "color"
+ }
+ }
+ },
+ "syntax": {
+ "primary": {
+ "value": "#1c1c1c",
+ "type": "color"
+ },
+ "comment": {
+ "value": "#717171",
+ "type": "color"
+ },
+ "keyword": {
+ "value": "#1819a1",
+ "type": "color"
+ },
+ "function": {
+ "value": "#bb550e",
+ "type": "color"
+ },
+ "type": {
+ "value": "#a8820e",
+ "type": "color"
+ },
+ "variant": {
+ "value": "#97142a",
+ "type": "color"
+ },
+ "property": {
+ "value": "#106c4e",
+ "type": "color"
+ },
+ "enum": {
+ "value": "#eb2d2d",
+ "type": "color"
+ },
+ "operator": {
+ "value": "#eb2d2d",
+ "type": "color"
+ },
+ "string": {
+ "value": "#eb2d2d",
+ "type": "color"
+ },
+ "number": {
+ "value": "#484bed",
+ "type": "color"
+ },
+ "boolean": {
+ "value": "#eb2d2d",
+ "type": "color"
+ }
+ },
+ "player": {
+ "1": {
+ "baseColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#2472f2",
+ "step": 500,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#2472f23d",
+ "step": 500,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#2472f2cc",
+ "step": 500,
+ "type": "color"
+ }
+ },
+ "2": {
+ "baseColor": {
+ "value": "#12d796",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#12d796",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#12d7963d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#12d796cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "3": {
+ "baseColor": {
+ "value": "#de57e8",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#de57e8",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#de57e83d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#de57e8cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "4": {
+ "baseColor": {
+ "value": "#f9812e",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#f9812e",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#f9812e3d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#f9812ecc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "5": {
+ "baseColor": {
+ "value": "#b066f8",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#b066f8",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#b066f83d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#b066f8cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "6": {
+ "baseColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#16d6c1",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#16d6c13d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#16d6c1cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "7": {
+ "baseColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#ef59a3",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#ef59a33d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#ef59a3cc",
+ "step": 400,
+ "type": "color"
+ }
+ },
+ "8": {
+ "baseColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "cursorColor": {
+ "value": "#f7bf17",
+ "step": 400,
+ "type": "color"
+ },
+ "selectionColor": {
+ "value": "#f7bf173d",
+ "step": 400,
+ "type": "color"
+ },
+ "borderColor": {
+ "value": "#f7bf17cc",
+ "step": 400,
+ "type": "color"
+ }
+ }
+ },
+ "shadowAlpha": {
+ "value": 0.12,
+ "type": "number"
+ }
+ }
+}
@@ -0,0 +1,8 @@
+{
+ "watch": [
+ "./**/*"
+ ],
+ "ext": "ts",
+ "ignore": [],
+ "exec": "ts-node src/buildThemes.ts"
+}
@@ -0,0 +1,2321 @@
+{
+ "name": "styles",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "styles",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@types/chroma-js": "^2.1.3",
+ "@types/node": "^17.0.23",
+ "case-anything": "^2.1.10",
+ "chroma-js": "^2.4.2",
+ "nodemon": "^2.0.15",
+ "ts-node": "^10.7.0"
+ }
+ },
+ "node_modules/@cspotcode/source-map-consumer": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
+ "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
+ "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+ "dependencies": {
+ "@cspotcode/source-map-consumer": "0.8.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+ "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "dependencies": {
+ "defer-to-connect": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
+ "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
+ "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
+ "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
+ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+ },
+ "node_modules/@types/chroma-js": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
+ "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+ },
+ "node_modules/@types/node": {
+ "version": "17.0.23",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
+ "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "node_modules/acorn": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
+ "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-align": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+ "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+ "dependencies": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/boxen": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
+ "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
+ "dependencies": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.1.0",
+ "cli-boxes": "^2.2.1",
+ "string-width": "^4.2.2",
+ "type-fest": "^0.20.2",
+ "widest-line": "^3.1.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+ "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacheable-request/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cacheable-request/node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/case-anything": {
+ "version": "2.1.10",
+ "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
+ "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chroma-js": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+ "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+ },
+ "node_modules/ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
+ },
+ "node_modules/cli-boxes": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
+ "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+ "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "node_modules/configstore": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+ "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+ },
+ "node_modules/crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+ "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/escape-goat": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/global-dirs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+ "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+ "dependencies": {
+ "ini": "2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/got": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+ "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "dependencies": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-yarn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
+ },
+ "node_modules/import-lazy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/ini": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+ "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+ "dependencies": {
+ "ci-info": "^2.0.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-installed-globally": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+ "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+ "dependencies": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-npm": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
+ "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+ },
+ "node_modules/is-yarn-global": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
+ },
+ "node_modules/keyv": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+ "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+ "dependencies": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "node_modules/latest-version": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+ "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+ "dependencies": {
+ "package-json": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/nodemon": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz",
+ "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.0.4",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5",
+ "update-notifier": "^5.1.0"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
+ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+ "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+ "dependencies": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/package-json/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prepend-http": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/pupa": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
+ "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
+ "dependencies": {
+ "escape-goat": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/registry-auth-token": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
+ "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
+ "dependencies": {
+ "rc": "^1.2.8"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/registry-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+ "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
+ "dependencies": {
+ "rc": "^1.2.8"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/responselike": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+ "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+ "dependencies": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/semver-diff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+ "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+ "dependencies": {
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/semver-diff/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-readable-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+ "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "dependencies": {
+ "nopt": "~1.0.10"
+ },
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
+ "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.7.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.0",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+ "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+ },
+ "node_modules/unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-notifier": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
+ "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
+ "dependencies": {
+ "boxen": "^5.0.0",
+ "chalk": "^4.1.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.4.0",
+ "is-npm": "^5.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.1.0",
+ "pupa": "^2.1.1",
+ "semver": "^7.3.4",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/yeoman/update-notifier?sponsor=1"
+ }
+ },
+ "node_modules/update-notifier/node_modules/semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/url-parse-lax": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+ "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+ "dependencies": {
+ "prepend-http": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
+ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
+ },
+ "node_modules/widest-line": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+ "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+ "dependencies": {
+ "string-width": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ },
+ "dependencies": {
+ "@cspotcode/source-map-consumer": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
+ "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
+ },
+ "@cspotcode/source-map-support": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
+ "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+ "requires": {
+ "@cspotcode/source-map-consumer": "0.8.0"
+ }
+ },
+ "@sindresorhus/is": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
+ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
+ },
+ "@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
+ "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "requires": {
+ "defer-to-connect": "^1.0.1"
+ }
+ },
+ "@tsconfig/node10": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
+ "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
+ },
+ "@tsconfig/node12": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
+ "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
+ },
+ "@tsconfig/node14": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
+ "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
+ },
+ "@tsconfig/node16": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
+ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+ },
+ "@types/chroma-js": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
+ "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+ },
+ "@types/node": {
+ "version": "17.0.23",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
+ "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "acorn": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
+ "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
+ },
+ "acorn-walk": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
+ },
+ "ansi-align": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+ "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+ "requires": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
+ },
+ "boxen": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
+ "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
+ "requires": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.1.0",
+ "cli-boxes": "^2.2.1",
+ "string-width": "^4.2.2",
+ "type-fest": "^0.20.2",
+ "widest-line": "^3.1.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "cacheable-request": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
+ "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "requires": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
+ }
+ }
+ },
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
+ },
+ "case-anything": {
+ "version": "2.1.10",
+ "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
+ "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ=="
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "chroma-js": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+ "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+ },
+ "ci-info": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
+ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
+ },
+ "cli-boxes": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
+ "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw=="
+ },
+ "clone-response": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
+ "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "configstore": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
+ "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
+ "requires": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ }
+ },
+ "create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+ },
+ "crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
+ },
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "decompress-response": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+ "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+ },
+ "defer-to-connect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
+ "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
+ },
+ "dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "requires": {
+ "is-obj": "^2.0.0"
+ }
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "escape-goat": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q=="
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "optional": true
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "global-dirs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+ "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+ "requires": {
+ "ini": "2.0.0"
+ }
+ },
+ "got": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
+ "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "requires": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "has-yarn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
+ "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw=="
+ },
+ "http-cache-semantics": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
+ "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
+ },
+ "ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
+ },
+ "import-lazy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM="
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
+ },
+ "ini": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+ "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-ci": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
+ "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
+ "requires": {
+ "ci-info": "^2.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-installed-globally": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+ "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+ "requires": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ }
+ },
+ "is-npm": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
+ "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA=="
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
+ },
+ "is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+ },
+ "is-yarn-global": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
+ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
+ },
+ "json-buffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
+ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
+ },
+ "keyv": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
+ "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
+ "requires": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "latest-version": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+ "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
+ "requires": {
+ "package-json": "^6.3.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "requires": {
+ "semver": "^6.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+ },
+ "mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "nodemon": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz",
+ "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==",
+ "requires": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.0.4",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5",
+ "update-notifier": "^5.1.0"
+ }
+ },
+ "nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+ },
+ "normalize-url": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
+ "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-cancelable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
+ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
+ },
+ "package-json": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
+ "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
+ "requires": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+ }
+ }
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ },
+ "prepend-http": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
+ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
+ },
+ "pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "pupa": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
+ "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
+ "requires": {
+ "escape-goat": "^2.0.0"
+ }
+ },
+ "rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ }
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "registry-auth-token": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
+ "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "registry-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
+ "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "responselike": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
+ "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
+ "requires": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
+ "semver-diff": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
+ "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
+ "requires": {
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+ }
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "to-readable-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
+ "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "requires": {
+ "nopt": "~1.0.10"
+ }
+ },
+ "ts-node": {
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
+ "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
+ "requires": {
+ "@cspotcode/source-map-support": "0.7.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.0",
+ "yn": "3.1.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="
+ },
+ "typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "requires": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "typescript": {
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+ "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+ "peer": true
+ },
+ "undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+ },
+ "unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "requires": {
+ "crypto-random-string": "^2.0.0"
+ }
+ },
+ "update-notifier": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
+ "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
+ "requires": {
+ "boxen": "^5.0.0",
+ "chalk": "^4.1.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.4.0",
+ "is-npm": "^5.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.1.0",
+ "pupa": "^2.1.1",
+ "semver": "^7.3.4",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
+ "url-parse-lax": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
+ "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
+ "requires": {
+ "prepend-http": "^2.0.0"
+ }
+ },
+ "v8-compile-cache-lib": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
+ "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
+ },
+ "widest-line": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
+ "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
+ "requires": {
+ "string-width": "^4.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "requires": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
+ }
+ }
+}
@@ -0,0 +1,22 @@
+{
+ "name": "styles",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "npm run build-themes && npm run build-tokens",
+ "build-themes": "ts-node ./src/buildThemes.ts",
+ "build-tokens": "ts-node ./src/buildTokens.ts",
+ "watch": "nodemon"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@types/chroma-js": "^2.1.3",
+ "@types/node": "^17.0.23",
+ "case-anything": "^2.1.10",
+ "chroma-js": "^2.4.2",
+ "ts-node": "^10.7.0",
+ "nodemon": "^2.0.15"
+ }
+}
@@ -0,0 +1,17 @@
+import * as fs from "fs";
+import * as path from "path";
+import app from "./styleTree/app";
+import dark from "./themes/dark";
+import light from "./themes/light";
+import snakeCase from "./utils/snakeCase";
+
+const themes = [dark, light];
+for (let theme of themes) {
+ let styleTree = snakeCase(app(theme));
+ let styleTreeJSON = JSON.stringify(styleTree, null, 2);
+ let outPath = path.resolve(
+ `${__dirname}/../../assets/themes/${theme.name}.json`
+ );
+ fs.writeFileSync(outPath, styleTreeJSON);
+ console.log(`- ${outPath} created`);
+}
@@ -0,0 +1,110 @@
+import * as fs from "fs";
+import * as path from "path";
+import dark from "./themes/dark";
+import light from "./themes/light";
+import Theme from "./themes/theme";
+import { colors, fontFamilies, fontSizes, fontWeights } from "./tokens";
+
+// Organize theme tokens
+function themeTokens(theme: Theme) {
+ return {
+ meta: {
+ themeName: theme.name,
+ },
+ text: theme.textColor,
+ icon: theme.iconColor,
+ background: theme.backgroundColor,
+ border: theme.borderColor,
+ editor: theme.editor,
+ syntax: {
+ primary: {
+ value: theme.syntax.primary.color.value,
+ type: "color",
+ },
+ comment: {
+ value: theme.syntax.comment.color.value,
+ type: "color",
+ },
+ keyword: {
+ value: theme.syntax.keyword.color.value,
+ type: "color",
+ },
+ function: {
+ value: theme.syntax.function.color.value,
+ type: "color",
+ },
+ type: {
+ value: theme.syntax.type.color.value,
+ type: "color",
+ },
+ variant: {
+ value: theme.syntax.variant.color.value,
+ type: "color",
+ },
+ property: {
+ value: theme.syntax.property.color.value,
+ type: "color",
+ },
+ enum: {
+ value: theme.syntax.enum.color.value,
+ type: "color",
+ },
+ operator: {
+ value: theme.syntax.operator.color.value,
+ type: "color",
+ },
+ string: {
+ value: theme.syntax.string.color.value,
+ type: "color",
+ },
+ number: {
+ value: theme.syntax.number.color.value,
+ type: "color",
+ },
+ boolean: {
+ value: theme.syntax.boolean.color.value,
+ type: "color",
+ },
+ },
+ player: theme.player,
+ shadowAlpha: theme.shadowAlpha,
+ };
+}
+
+// Organize core tokens
+const coreTokens = {
+ color: {
+ ...colors,
+ },
+ text: {
+ family: fontFamilies,
+ weight: fontWeights,
+ },
+ size: fontSizes,
+};
+
+const combinedTokens: any = {};
+
+const distPath = path.resolve(`${__dirname}/../dist`);
+
+// Add core tokens to the combined tokens and write `core.json`.
+// We write `core.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
+const corePath = path.join(distPath, "core.json");
+fs.writeFileSync(corePath, JSON.stringify(coreTokens, null, 2));
+console.log(`- ${corePath} created`);
+combinedTokens.core = coreTokens;
+
+// Add each theme to the combined tokens and write ${theme}.json.
+// We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
+let themes = [dark, light];
+themes.forEach((theme) => {
+ const themePath = `${distPath}/${theme.name}.json`
+ fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2));
+ console.log(`- ${themePath} created`);
+ combinedTokens[theme.name] = themeTokens(theme);
+});
+
+// Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app.
+const combinedPath = path.resolve(`${distPath}/tokens.json`);
+fs.writeFileSync(combinedPath, JSON.stringify(combinedTokens, null, 2));
+console.log(`- ${combinedPath} created`);
@@ -0,0 +1,45 @@
+import Theme from "../themes/theme";
+import chatPanel from "./chatPanel";
+import { text } from "./components";
+import contactsPanel from "./contactsPanel";
+import commandPalette from "./commandPalette";
+import editor from "./editor";
+import projectPanel from "./projectPanel";
+import search from "./search";
+import selectorModal from "./selectorModal";
+import workspace from "./workspace";
+
+export const panel = {
+ padding: { top: 12, left: 12, bottom: 12, right: 12 },
+};
+
+export default function app(theme: Theme): Object {
+ return {
+ selector: selectorModal(theme),
+ workspace: workspace(theme),
+ editor: editor(theme),
+ projectDiagnostics: {
+ tabIconSpacing: 4,
+ tabIconWidth: 13,
+ tabSummarySpacing: 10,
+ emptyMessage: text(theme, "sans", "primary", { size: "lg" }),
+ statusBarItem: {
+ ...text(theme, "sans", "muted"),
+ margin: {
+ right: 10,
+ },
+ },
+ },
+ commandPalette: commandPalette(theme),
+ projectPanel: projectPanel(theme),
+ chatPanel: chatPanel(theme),
+ contactsPanel: contactsPanel(theme),
+ search: search(theme),
+ breadcrumbs: {
+ ...text(theme, "sans", "secondary"),
+ padding: {
+ left: 6,
+ },
+ }
+ };
+}
@@ -0,0 +1,108 @@
+import Theme from "../themes/theme";
+import { panel } from "./app";
+import {
+ backgroundColor,
+ border,
+ player,
+ shadow,
+ text,
+ TextColor
+} from "./components";
+
+export default function chatPanel(theme: Theme) {
+ function channelSelectItem(
+ theme: Theme,
+ textColor: TextColor,
+ hovered: boolean
+ ) {
+ return {
+ name: text(theme, "sans", textColor),
+ padding: 4,
+ hash: {
+ ...text(theme, "sans", "muted"),
+ margin: {
+ right: 8,
+ },
+ },
+ background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
+ cornerRadius: hovered ? 6 : 0,
+ };
+ }
+
+ const message = {
+ body: text(theme, "sans", "secondary"),
+ timestamp: text(theme, "sans", "muted", { size: "sm" }),
+ padding: {
+ bottom: 6,
+ },
+ sender: {
+ ...text(theme, "sans", "primary", { weight: "bold" }),
+ margin: {
+ right: 8,
+ },
+ },
+ };
+
+ return {
+ ...panel,
+ channelName: text(theme, "sans", "primary", { weight: "bold" }),
+ channelNameHash: {
+ ...text(theme, "sans", "muted"),
+ padding: {
+ right: 8,
+ },
+ },
+ channelSelect: {
+ header: {
+ ...channelSelectItem(theme, "primary", false),
+ padding: {
+ bottom: 4,
+ left: 0,
+ },
+ },
+ item: channelSelectItem(theme, "secondary", false),
+ hoveredItem: channelSelectItem(theme, "secondary", true),
+ activeItem: channelSelectItem(theme, "primary", false),
+ hoveredActiveItem: channelSelectItem(theme, "primary", true),
+ menu: {
+ background: backgroundColor(theme, 500),
+ cornerRadius: 6,
+ padding: 4,
+ border: border(theme, "primary"),
+ shadow: shadow(theme),
+ },
+ },
+ signInPrompt: text(theme, "sans", "secondary", { underline: true }),
+ hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
+ message,
+ pendingMessage: {
+ ...message,
+ body: {
+ ...message.body,
+ color: theme.textColor.muted.value,
+ },
+ sender: {
+ ...message.sender,
+ color: theme.textColor.muted.value,
+ },
+ timestamp: {
+ ...message.timestamp,
+ color: theme.textColor.muted.value,
+ },
+ },
+ inputEditor: {
+ background: backgroundColor(theme, 500),
+ cornerRadius: 6,
+ text: text(theme, "mono", "primary"),
+ placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
+ selection: player(theme, 1).selection,
+ border: border(theme, "secondary"),
+ padding: {
+ bottom: 7,
+ left: 8,
+ right: 8,
+ top: 7,
+ },
+ },
+ };
+}
@@ -0,0 +1,23 @@
+import Theme from "../themes/theme";
+import { text, backgroundColor, border } from "./components";
+
+export default function commandPalette(theme: Theme) {
+ return {
+ keystrokeSpacing: 8,
+ key: {
+ text: text(theme, "mono", "secondary", { size: "xs" }),
+ cornerRadius: 4,
+ background: backgroundColor(theme, "on300"),
+ border: border(theme, "secondary"),
+ padding: {
+ top: 2,
+ bottom: 2,
+ left: 8,
+ right: 8,
+ },
+ margin: {
+ left: 2
+ },
+ }
+ }
+}
@@ -0,0 +1,93 @@
+import chroma from "chroma-js";
+import Theme, { BackgroundColorSet } from "../themes/theme";
+import { fontFamilies, fontSizes, FontWeight } from "../tokens";
+import { Color } from "../utils/color";
+
+export type TextColor = keyof Theme["textColor"];
+export function text(
+ theme: Theme,
+ fontFamily: keyof typeof fontFamilies,
+ color: TextColor,
+ properties?: {
+ size?: keyof typeof fontSizes;
+ weight?: FontWeight;
+ underline?: boolean;
+ }
+) {
+ let size = fontSizes[properties?.size || "sm"].value;
+ return {
+ family: fontFamilies[fontFamily].value,
+ color: theme.textColor[color].value,
+ ...properties,
+ size,
+ };
+}
+export function textColor(theme: Theme, color: TextColor) {
+ return theme.textColor[color].value;
+}
+
+export type BorderColor = keyof Theme["borderColor"];
+export interface BorderOptions {
+ width?: number;
+ top?: boolean;
+ bottom?: boolean;
+ left?: boolean;
+ right?: boolean;
+ overlay?: boolean;
+}
+export function border(
+ theme: Theme,
+ color: BorderColor,
+ options?: BorderOptions
+) {
+ return {
+ color: borderColor(theme, color),
+ width: 1,
+ ...options,
+ };
+}
+export function borderColor(theme: Theme, color: BorderColor) {
+ return theme.borderColor[color].value;
+}
+
+export type IconColor = keyof Theme["iconColor"];
+export function iconColor(theme: Theme, color: IconColor) {
+ return theme.iconColor[color].value;
+}
+
+export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
+export interface Player {
+ selection: {
+ cursor: Color;
+ selection: Color;
+ };
+}
+export function player(
+ theme: Theme,
+ playerNumber: PlayerIndex,
+): Player {
+ return {
+ selection: {
+ cursor: theme.player[playerNumber].cursorColor.value,
+ selection: theme.player[playerNumber].selectionColor.value,
+ },
+ };
+}
+
+export type BackgroundColor = keyof Theme["backgroundColor"];
+export type BackgroundState = keyof BackgroundColorSet;
+export function backgroundColor(
+ theme: Theme,
+ name: BackgroundColor,
+ state?: BackgroundState,
+): Color {
+ return theme.backgroundColor[name][state || "base"].value;
+}
+
+export function shadow(theme: Theme) {
+ return {
+ blur: 16,
+ color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
+ offset: [0, 2],
+ };
+}
@@ -0,0 +1,62 @@
+import Theme from "../themes/theme";
+import { panel } from "./app";
+import { backgroundColor, borderColor, text } from "./components";
+
+export default function(theme: Theme) {
+ const project = {
+ guestAvatarSpacing: 4,
+ height: 24,
+ guestAvatar: {
+ cornerRadius: 8,
+ width: 14,
+ },
+ name: {
+ ...text(theme, "mono", "placeholder", { size: "sm" }),
+ margin: {
+ right: 6,
+ },
+ },
+ padding: {
+ left: 8,
+ },
+ };
+
+ const sharedProject = {
+ ...project,
+ background: backgroundColor(theme, 300),
+ cornerRadius: 6,
+ name: {
+ ...project.name,
+ ...text(theme, "mono", "secondary", { size: "sm" }),
+ },
+ };
+
+ return {
+ ...panel,
+ hostRowHeight: 28,
+ treeBranchColor: borderColor(theme, "muted"),
+ treeBranchWidth: 1,
+ hostAvatar: {
+ cornerRadius: 10,
+ width: 18,
+ },
+ hostUsername: {
+ ...text(theme, "mono", "primary", { size: "sm" }),
+ padding: {
+ left: 8,
+ },
+ },
+ project,
+ sharedProject,
+ hoveredSharedProject: {
+ ...sharedProject,
+ background: backgroundColor(theme, 300, "hovered"),
+ cornerRadius: 6,
+ },
+ unsharedProject: project,
+ hoveredUnsharedProject: {
+ ...project,
+ cornerRadius: 6,
+ },
+ }
+}
@@ -0,0 +1,146 @@
+import Theme from "../themes/theme";
+import {
+ backgroundColor,
+ border,
+ iconColor,
+ player,
+ text,
+ TextColor
+} from "./components";
+
+export default function editor(theme: Theme) {
+ const autocompleteItem = {
+ cornerRadius: 6,
+ padding: {
+ bottom: 2,
+ left: 6,
+ right: 6,
+ top: 2,
+ },
+ };
+
+ function diagnostic(theme: Theme, color: TextColor) {
+ return {
+ textScaleFactor: 0.857,
+ header: {
+ border: border(theme, "primary", {
+ top: true,
+ }),
+ },
+ message: {
+ text: text(theme, "sans", color, { size: "sm" }),
+ highlightText: text(theme, "sans", color, {
+ size: "sm",
+ weight: "bold",
+ }),
+ },
+ };
+ }
+
+ return {
+ // textColor: theme.syntax.primary.color,
+ textColor: theme.syntax.primary.color.value,
+ background: backgroundColor(theme, 500),
+ activeLineBackground: theme.editor.line.active.value,
+ codeActionsIndicator: iconColor(theme, "muted"),
+ diffBackgroundDeleted: backgroundColor(theme, "error"),
+ diffBackgroundInserted: backgroundColor(theme, "ok"),
+ documentHighlightReadBackground: theme.editor.highlight.occurrence.value,
+ documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value,
+ errorColor: theme.textColor.error.value,
+ gutterBackground: backgroundColor(theme, 500),
+ gutterPaddingFactor: 3.5,
+ highlightedLineBackground: theme.editor.line.highlighted.value,
+ lineNumber: theme.editor.gutter.primary.value,
+ lineNumberActive: theme.editor.gutter.active.value,
+ renameFade: 0.6,
+ unnecessaryCodeFade: 0.5,
+ selection: player(theme, 1).selection,
+ guestSelections: [
+ player(theme, 2).selection,
+ player(theme, 3).selection,
+ player(theme, 4).selection,
+ player(theme, 5).selection,
+ player(theme, 6).selection,
+ player(theme, 7).selection,
+ player(theme, 8).selection,
+ ],
+ autocomplete: {
+ background: backgroundColor(theme, 500),
+ cornerRadius: 8,
+ padding: 4,
+ border: border(theme, "secondary"),
+ item: autocompleteItem,
+ hoveredItem: {
+ ...autocompleteItem,
+ background: backgroundColor(theme, 500, "hovered"),
+ },
+ margin: {
+ left: -14,
+ },
+ matchHighlight: text(theme, "mono", "feature"),
+ selectedItem: {
+ ...autocompleteItem,
+ background: backgroundColor(theme, 500, "active"),
+ },
+ },
+ diagnosticHeader: {
+ background: backgroundColor(theme, 300),
+ iconWidthFactor: 1.5,
+ textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
+ border: border(theme, "secondary", {
+ bottom: true,
+ top: true,
+ }),
+ code: {
+ ...text(theme, "mono", "muted", { size: "sm" }),
+ margin: {
+ left: 10,
+ },
+ },
+ message: {
+ highlightText: text(theme, "sans", "primary", {
+ size: "sm",
+ weight: "bold",
+ }),
+ text: text(theme, "sans", "secondary", { size: "sm" }),
+ },
+ },
+ diagnosticPathHeader: {
+ background: theme.editor.line.active.value,
+ textScaleFactor: 0.857,
+ filename: text(theme, "mono", "primary", { size: "sm" }),
+ path: {
+ ...text(theme, "mono", "muted", { size: "sm" }),
+ margin: {
+ left: 12,
+ },
+ },
+ },
+ errorDiagnostic: diagnostic(theme, "error"),
+ warningDiagnostic: diagnostic(theme, "warning"),
+ informationDiagnostic: diagnostic(theme, "info"),
+ hintDiagnostic: diagnostic(theme, "info"),
+ invalidErrorDiagnostic: diagnostic(theme, "muted"),
+ invalidHintDiagnostic: diagnostic(theme, "muted"),
+ invalidInformationDiagnostic: diagnostic(theme, "muted"),
+ invalidWarningDiagnostic: diagnostic(theme, "muted"),
+ syntax: {
+ keyword: theme.syntax.keyword.color.value,
+ function: theme.syntax.function.color.value,
+ string: theme.syntax.string.color.value,
+ type: theme.syntax.type.color.value,
+ number: theme.syntax.number.color.value,
+ comment: theme.syntax.comment.color.value,
+ property: theme.syntax.property.color.value,
+ variant: theme.syntax.variant.color.value,
+ constant: theme.syntax.constant.color.value,
+ title: { color: theme.syntax.title.color.value, weight: "bold" },
+ emphasis: theme.textColor.feature.value,
+ "emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
+ link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
+ link_text: { color: theme.syntax.linkText.color.value, italic: true },
+ list_marker: theme.syntax.punctuation.color.value,
+ },
+ };
+}
@@ -0,0 +1,37 @@
+import Theme from "../themes/theme";
+import { Color } from "../utils/color";
+import { panel } from "./app";
+import { backgroundColor, iconColor, text, TextColor } from "./components";
+
+export default function projectPanel(theme: Theme) {
+ function entry(theme: Theme, textColor: TextColor, background?: Color) {
+ return {
+ height: 22,
+ background,
+ iconColor: iconColor(theme, "muted"),
+ iconSize: 8,
+ iconSpacing: 8,
+ text: text(theme, "mono", textColor, { size: "sm" }),
+ };
+ }
+
+ return {
+ ...panel,
+ entry: entry(theme, "secondary"),
+ hoveredEntry: entry(
+ theme,
+ "secondary",
+ backgroundColor(theme, 300, "hovered")
+ ),
+ selectedEntry: entry(theme, "primary"),
+ hoveredSelectedEntry: entry(
+ theme,
+ "primary",
+ backgroundColor(theme, 300, "hovered")
+ ),
+ padding: {
+ top: 6,
+ left: 12,
+ },
+ };
+}
@@ -0,0 +1,84 @@
+import Theme from "../themes/theme";
+import { backgroundColor, border, player, text } from "./components";
+
+export default function search(theme: Theme) {
+ const optionButton = {
+ ...text(theme, "mono", "secondary"),
+ background: backgroundColor(theme, "on500"),
+ cornerRadius: 4,
+ border: border(theme, "secondary"),
+ margin: {
+ left: 2,
+ right: 2,
+ },
+ padding: {
+ bottom: 3,
+ left: 8,
+ right: 8,
+ top: 3,
+ },
+ };
+
+ const editor = {
+ background: backgroundColor(theme, 500),
+ cornerRadius: 8,
+ minWidth: 200,
+ maxWidth: 500,
+ placeholderText: text(theme, "mono", "placeholder"),
+ selection: player(theme, 1).selection,
+ text: text(theme, "mono", "active"),
+ border: border(theme, "secondary"),
+ margin: {
+ right: 6,
+ },
+ padding: {
+ top: 3,
+ bottom: 3,
+ left: 12,
+ right: 8,
+ },
+ };
+
+ return {
+ matchBackground: theme.editor.highlight.match.value,
+ tabIconSpacing: 8,
+ tabIconWidth: 14,
+ activeHoveredOptionButton: {
+ ...optionButton,
+ ...text(theme, "mono", "active"),
+ background: backgroundColor(theme, "on500", "active"),
+ border: border(theme, "muted"),
+ },
+ activeOptionButton: {
+ ...optionButton,
+ ...text(theme, "mono", "active"),
+ background: backgroundColor(theme, "on500", "active"),
+ border: border(theme, "muted"),
+ },
+ editor,
+ hoveredOptionButton: {
+ ...optionButton,
+ ...text(theme, "mono", "active"),
+ border: border(theme, "muted"),
+ },
+ invalidEditor: {
+ ...editor,
+ border: border(theme, "error"),
+ },
+ matchIndex: {
+ ...text(theme, "mono", "muted"),
+ padding: 6,
+ },
+ optionButton,
+ optionButtonGroup: {
+ padding: {
+ left: 4,
+ right: 4,
+ },
+ },
+ resultsStatus: {
+ ...text(theme, "mono", "primary"),
+ size: 18,
+ },
+ };
+}
@@ -0,0 +1,59 @@
+import Theme from "../themes/theme";
+import { backgroundColor, border, player, shadow, text } from "./components";
+
+export default function selectorModal(theme: Theme): Object {
+ const item = {
+ padding: {
+ bottom: 4,
+ left: 12,
+ right: 12,
+ top: 4,
+ },
+ cornerRadius: 8,
+ text: text(theme, "sans", "secondary"),
+ highlightText: text(theme, "sans", "feature", { weight: "bold" }),
+ };
+
+ const activeItem = {
+ ...item,
+ background: backgroundColor(theme, 300, "active"),
+ text: text(theme, "sans", "primary"),
+ };
+
+ return {
+ background: backgroundColor(theme, 300),
+ cornerRadius: 8,
+ padding: 8,
+ item,
+ activeItem,
+ border: border(theme, "primary"),
+ empty: {
+ text: text(theme, "sans", "placeholder"),
+ padding: {
+ bottom: 4,
+ left: 12,
+ right: 12,
+ top: 8,
+ },
+ },
+ inputEditor: {
+ background: backgroundColor(theme, 500),
+ cornerRadius: 8,
+ placeholderText: text(theme, "sans", "placeholder"),
+ selection: player(theme, 1).selection,
+ text: text(theme, "mono", "primary"),
+ border: border(theme, "secondary"),
+ padding: {
+ bottom: 7,
+ left: 16,
+ right: 16,
+ top: 7,
+ },
+ },
+ margin: {
+ bottom: 52,
+ top: 52,
+ },
+ shadow: shadow(theme),
+ };
+}
@@ -0,0 +1,158 @@
+import Theme from "../themes/theme";
+import { backgroundColor, border, iconColor, text } from "./components";
+
+export default function workspace(theme: Theme) {
+ const signInPrompt = {
+ ...text(theme, "sans", "secondary", { size: "xs" }),
+ border: border(theme, "primary"),
+ cornerRadius: 6,
+ margin: {
+ top: 1,
+ right: 6,
+ },
+ padding: {
+ left: 6,
+ right: 6,
+ },
+ };
+
+ const tab = {
+ height: 32,
+ background: backgroundColor(theme, 300),
+ iconClose: iconColor(theme, "muted"),
+ iconCloseActive: iconColor(theme, "active"),
+ iconConflict: iconColor(theme, "warning"),
+ iconDirty: iconColor(theme, "info"),
+ iconWidth: 8,
+ spacing: 8,
+ text: text(theme, "sans", "secondary", { size: "sm" }),
+ border: border(theme, "primary", {
+ left: true,
+ bottom: true,
+ overlay: true,
+ }),
+ padding: {
+ left: 8,
+ right: 8,
+ },
+ };
+
+ const activeTab = {
+ ...tab,
+ background: backgroundColor(theme, 500),
+ text: text(theme, "sans", "active", { size: "sm" }),
+ border: {
+ ...tab.border,
+ bottom: false,
+ },
+ };
+
+ const sidebarItem = {
+ height: 32,
+ iconColor: iconColor(theme, "secondary"),
+ iconSize: 18,
+ };
+ const sidebar = {
+ width: 30,
+ background: backgroundColor(theme, 300),
+ border: border(theme, "primary", { right: true }),
+ item: sidebarItem,
+ activeItem: {
+ ...sidebarItem,
+ iconColor: iconColor(theme, "active"),
+ },
+ resizeHandle: {
+ background: border(theme, "primary").color,
+ padding: {
+ left: 1,
+ },
+ },
+ };
+
+ return {
+ background: backgroundColor(theme, 300),
+ leaderBorderOpacity: 0.7,
+ leaderBorderWidth: 2.0,
+ tab,
+ activeTab,
+ leftSidebar: {
+ ...sidebar,
+ border: border(theme, "primary", { right: true }),
+ },
+ rightSidebar: {
+ ...sidebar,
+ border: border(theme, "primary", { left: true }),
+ },
+ paneDivider: {
+ color: border(theme, "secondary").color,
+ width: 1,
+ },
+ status_bar: {
+ height: 24,
+ itemSpacing: 8,
+ padding: {
+ left: 6,
+ right: 6,
+ },
+ border: border(theme, "primary", { top: true, overlay: true }),
+ cursorPosition: text(theme, "sans", "muted"),
+ diagnosticMessage: text(theme, "sans", "muted"),
+ lspMessage: text(theme, "sans", "muted"),
+ autoUpdateProgressMessage: text(theme, "sans", "muted"),
+ autoUpdateDoneMessage: text(theme, "sans", "muted"),
+ },
+ titlebar: {
+ avatarWidth: 18,
+ height: 32,
+ background: backgroundColor(theme, 100),
+ shareIconColor: iconColor(theme, "secondary"),
+ shareIconActiveColor: iconColor(theme, "feature"),
+ title: text(theme, "sans", "primary"),
+ avatar: {
+ cornerRadius: 10,
+ border: {
+ color: "#00000088",
+ width: 1,
+ },
+ },
+ avatarRibbon: {
+ height: 3,
+ width: 12,
+ // TODO: The background for this ideally should be
+ // set with a token, not hardcoded in rust
+ },
+ border: border(theme, "primary", { bottom: true }),
+ signInPrompt,
+ hoveredSignInPrompt: {
+ ...signInPrompt,
+ ...text(theme, "sans", "active", { size: "xs" }),
+ },
+ offlineIcon: {
+ color: iconColor(theme, "secondary"),
+ width: 16,
+ padding: {
+ right: 4,
+ },
+ },
+ outdatedWarning: {
+ ...text(theme, "sans", "warning"),
+ size: 13,
+ },
+ },
+ toolbar: {
+ height: 34,
+ background: backgroundColor(theme, 500),
+ border: border(theme, "secondary", { bottom: true }),
+ itemSpacing: 8,
+ padding: { left: 16, right: 8, top: 4, bottom: 4 },
+ },
+ breadcrumbs: {
+ ...text(theme, "mono", "secondary"),
+ padding: { left: 6 },
+ },
+ disconnectedOverlay: {
+ ...text(theme, "sans", "active"),
+ background: "#000000aa",
+ },
+ };
+}
@@ -0,0 +1,241 @@
+import { colors, fontWeights, NumberToken } from "../tokens";
+import { withOpacity } from "../utils/color";
+import Theme, { buildPlayer, Syntax } from "./theme";
+
+const backgroundColor = {
+ 100: {
+ base: colors.neutral[750],
+ hovered: colors.neutral[725],
+ active: colors.neutral[800],
+ focused: colors.neutral[675],
+ },
+ 300: {
+ base: colors.neutral[800],
+ hovered: colors.neutral[775],
+ active: colors.neutral[750],
+ focused: colors.neutral[775],
+ },
+ 500: {
+ base: colors.neutral[900],
+ hovered: withOpacity(colors.neutral[0], 0.08),
+ active: withOpacity(colors.neutral[0], 0.12),
+ focused: colors.neutral[825],
+ },
+ on300: {
+ base: withOpacity(colors.neutral[850], 0.5),
+ hovered: colors.neutral[875],
+ active: colors.neutral[900],
+ focused: colors.neutral[875],
+ },
+ on500: {
+ base: colors.neutral[850],
+ hovered: colors.neutral[800],
+ active: colors.neutral[775],
+ focused: colors.neutral[800],
+ },
+ ok: {
+ base: colors.green[600],
+ hovered: colors.green[600],
+ active: colors.green[600],
+ focused: colors.green[600],
+ },
+ error: {
+ base: colors.red[400],
+ hovered: colors.red[400],
+ active: colors.red[400],
+ focused: colors.red[400],
+ },
+ warning: {
+ base: colors.amber[300],
+ hovered: colors.amber[300],
+ active: colors.amber[300],
+ focused: colors.amber[300],
+ },
+ info: {
+ base: colors.blue[500],
+ hovered: colors.blue[500],
+ active: colors.blue[500],
+ focused: colors.blue[500],
+ },
+};
+
+const borderColor = {
+ primary: colors.neutral[875],
+ secondary: colors.neutral[775],
+ muted: colors.neutral[675],
+ focused: colors.indigo[500],
+ active: colors.neutral[900],
+ ok: colors.green[500],
+ error: colors.red[500],
+ warning: colors.amber[500],
+ info: colors.blue[500],
+};
+
+const textColor = {
+ primary: colors.neutral[50],
+ secondary: colors.neutral[350],
+ muted: colors.neutral[450],
+ placeholder: colors.neutral[650],
+ active: colors.neutral[0],
+ //TODO: (design) define feature and it's correct value
+ feature: colors.blue[400],
+ ok: colors.green[600],
+ error: colors.red[400],
+ warning: colors.amber[300],
+ info: colors.blue[500],
+};
+
+const iconColor = {
+ primary: colors.neutral[200],
+ secondary: colors.neutral[350],
+ muted: colors.neutral[600],
+ placeholder: colors.neutral[700],
+ active: colors.neutral[0],
+ //TODO: (design) define feature and it's correct value
+ feature: colors.blue[500],
+ ok: colors.green[600],
+ error: colors.red[500],
+ warning: colors.amber[400],
+ info: colors.blue[600],
+};
+
+const player = {
+ 1: buildPlayer(colors.blue[500]),
+ 2: buildPlayer(colors.lime[500]),
+ 3: buildPlayer(colors.fuschia[500]),
+ 4: buildPlayer(colors.orange[500]),
+ 5: buildPlayer(colors.purple[500]),
+ 6: buildPlayer(colors.teal[400]),
+ 7: buildPlayer(colors.pink[400]),
+ 8: buildPlayer(colors.yellow[400]),
+};
+
+const editor = {
+ background: backgroundColor[500].base,
+ indent_guide: borderColor.muted,
+ indent_guide_active: borderColor.secondary,
+ line: {
+ active: withOpacity(colors.neutral[0], 0.07),
+ highlighted: withOpacity(colors.neutral[0], 0.12),
+ inserted: backgroundColor.ok.active,
+ deleted: backgroundColor.error.active,
+ modified: backgroundColor.info.active,
+ },
+ highlight: {
+ selection: player[1].selectionColor,
+ occurrence: withOpacity(colors.neutral[0], 0.12),
+ activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side
+ matchingBracket: backgroundColor[500].active,
+ match: withOpacity(colors.violet[700], 0.5),
+ activeMatch: withOpacity(colors.violet[600], 0.7),
+ related: backgroundColor[500].focused,
+ },
+ gutter: {
+ primary: textColor.placeholder,
+ active: textColor.active,
+ },
+};
+
+const syntax: Syntax = {
+ primary: {
+ color: colors.neutral[150],
+ weight: fontWeights.normal,
+ },
+ comment: {
+ color: colors.neutral[300],
+ weight: fontWeights.normal,
+ },
+ punctuation: {
+ color: colors.neutral[200],
+ weight: fontWeights.normal,
+ },
+ constant: {
+ color: colors.neutral[150],
+ weight: fontWeights.normal,
+ },
+ keyword: {
+ color: colors.blue[400],
+ weight: fontWeights.normal,
+ },
+ function: {
+ color: colors.yellow[200],
+ weight: fontWeights.normal,
+ },
+ type: {
+ color: colors.teal[300],
+ weight: fontWeights.normal,
+ },
+ variant: {
+ color: colors.sky[300],
+ weight: fontWeights.normal,
+ },
+ property: {
+ color: colors.blue[400],
+ weight: fontWeights.normal,
+ },
+ enum: {
+ color: colors.orange[500],
+ weight: fontWeights.normal,
+ },
+ operator: {
+ color: colors.orange[500],
+ weight: fontWeights.normal,
+ },
+ string: {
+ color: colors.orange[300],
+ weight: fontWeights.normal,
+ },
+ number: {
+ color: colors.lime[300],
+ weight: fontWeights.normal,
+ },
+ boolean: {
+ color: colors.lime[300],
+ weight: fontWeights.normal,
+ },
+ predictive: {
+ color: textColor.muted,
+ weight: fontWeights.normal,
+ },
+ title: {
+ color: colors.amber[500],
+ weight: fontWeights.bold,
+ },
+ emphasis: {
+ color: textColor.active,
+ weight: fontWeights.normal,
+ },
+ emphasisStrong: {
+ color: textColor.active,
+ weight: fontWeights.bold,
+ },
+ linkUrl: {
+ color: colors.lime[500],
+ weight: fontWeights.normal,
+ // TODO: add underline
+ },
+ linkText: {
+ color: colors.orange[500],
+ weight: fontWeights.normal,
+ // TODO: add italic
+ },
+};
+
+const shadowAlpha: NumberToken = {
+ value: 0.32,
+ type: "number",
+};
+
+const theme: Theme = {
+ name: "dark",
+ backgroundColor,
+ borderColor,
+ textColor,
+ iconColor,
+ editor,
+ syntax,
+ player,
+ shadowAlpha,
+};
+
+export default theme;
@@ -0,0 +1,239 @@
+import { colors, fontWeights, NumberToken } from "../tokens";
+import { withOpacity } from "../utils/color";
+import Theme, { buildPlayer, Syntax } from "./theme";
+
+const backgroundColor = {
+ 100: {
+ base: colors.neutral[75],
+ hovered: colors.neutral[100],
+ active: colors.neutral[150],
+ focused: colors.neutral[100],
+ },
+ 300: {
+ base: colors.neutral[25],
+ hovered: colors.neutral[75],
+ active: colors.neutral[100],
+ focused: colors.neutral[75],
+ },
+ 500: {
+ base: colors.neutral[0],
+ hovered: withOpacity(colors.neutral[900], 0.03),
+ active: withOpacity(colors.neutral[900], 0.06),
+ focused: colors.neutral[50],
+ },
+ on300: {
+ base: colors.neutral[50],
+ hovered: colors.neutral[100],
+ active: colors.neutral[150],
+ focused: colors.neutral[100],
+ },
+ on500: {
+ base: colors.neutral[50],
+ hovered: colors.neutral[25],
+ active: colors.neutral[0],
+ focused: colors.neutral[25],
+ },
+ ok: {
+ base: colors.green[100],
+ hovered: colors.green[100],
+ active: colors.green[100],
+ focused: colors.green[100],
+ },
+ error: {
+ base: colors.red[100],
+ hovered: colors.red[100],
+ active: colors.red[100],
+ focused: colors.red[100],
+ },
+ warning: {
+ base: colors.yellow[100],
+ hovered: colors.yellow[100],
+ active: colors.yellow[100],
+ focused: colors.yellow[100],
+ },
+ info: {
+ base: colors.blue[100],
+ hovered: colors.blue[100],
+ active: colors.blue[100],
+ focused: colors.blue[100],
+ },
+};
+
+const borderColor = {
+ primary: colors.neutral[150],
+ secondary: colors.neutral[150],
+ muted: colors.neutral[100],
+ focused: colors.indigo[500],
+ active: colors.neutral[250],
+ ok: colors.green[200],
+ error: colors.red[200],
+ warning: colors.yellow[200],
+ info: colors.blue[200],
+};
+
+const textColor = {
+ primary: colors.neutral[750],
+ secondary: colors.neutral[650],
+ muted: colors.neutral[550],
+ placeholder: colors.neutral[450],
+ active: colors.neutral[900],
+ feature: colors.indigo[500],
+ ok: colors.green[500],
+ error: colors.red[500],
+ warning: colors.yellow[500],
+ info: colors.blue[500],
+};
+
+const iconColor = {
+ primary: colors.neutral[700],
+ secondary: colors.neutral[500],
+ muted: colors.neutral[350],
+ placeholder: colors.neutral[300],
+ active: colors.neutral[900],
+ feature: colors.indigo[500],
+ ok: colors.green[600],
+ error: colors.red[600],
+ warning: colors.yellow[400],
+ info: colors.blue[600],
+};
+
+const player = {
+ 1: buildPlayer(colors.blue[500]),
+ 2: buildPlayer(colors.emerald[400]),
+ 3: buildPlayer(colors.fuschia[400]),
+ 4: buildPlayer(colors.orange[400]),
+ 5: buildPlayer(colors.purple[400]),
+ 6: buildPlayer(colors.teal[400]),
+ 7: buildPlayer(colors.pink[400]),
+ 8: buildPlayer(colors.yellow[400]),
+};
+
+const editor = {
+ background: backgroundColor[500].base,
+ indent_guide: borderColor.muted,
+ indent_guide_active: borderColor.secondary,
+ line: {
+ active: withOpacity(colors.neutral[900], 0.06),
+ highlighted: withOpacity(colors.neutral[900], 0.12),
+ inserted: backgroundColor.ok.active,
+ deleted: backgroundColor.error.active,
+ modified: backgroundColor.info.active,
+ },
+ highlight: {
+ selection: player[1].selectionColor,
+ occurrence: withOpacity(colors.neutral[900], 0.06),
+ activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side
+ matchingBracket: colors.neutral[0],
+ match: colors.yellow[100],
+ activeMatch: colors.yellow[200], // TODO: This is not hooked up to occurences on the rust side
+ related: colors.neutral[0],
+ },
+ gutter: {
+ primary: colors.neutral[300],
+ active: textColor.active,
+ },
+};
+
+const syntax: Syntax = {
+ primary: {
+ color: colors.neutral[800],
+ weight: fontWeights.normal,
+ },
+ comment: {
+ color: colors.neutral[500],
+ weight: fontWeights.normal,
+ },
+ punctuation: {
+ color: colors.neutral[600],
+ weight: fontWeights.normal,
+ },
+ constant: {
+ color: colors.neutral[800],
+ weight: fontWeights.normal,
+ },
+ keyword: {
+ color: colors.indigo[700],
+ weight: fontWeights.normal,
+ },
+ function: {
+ color: colors.orange[600],
+ weight: fontWeights.normal,
+ },
+ type: {
+ color: colors.yellow[600],
+ weight: fontWeights.normal,
+ },
+ variant: {
+ color: colors.rose[700],
+ weight: fontWeights.normal,
+ },
+ property: {
+ color: colors.emerald[700],
+ weight: fontWeights.normal,
+ },
+ enum: {
+ color: colors.red[500],
+ weight: fontWeights.normal,
+ },
+ operator: {
+ color: colors.red[500],
+ weight: fontWeights.normal,
+ },
+ string: {
+ color: colors.red[500],
+ weight: fontWeights.normal,
+ },
+ number: {
+ color: colors.indigo[500],
+ weight: fontWeights.normal,
+ },
+ boolean: {
+ color: colors.red[500],
+ weight: fontWeights.normal,
+ },
+ predictive: {
+ color: textColor.placeholder,
+ weight: fontWeights.normal,
+ },
+ title: {
+ color: colors.sky[500],
+ weight: fontWeights.bold,
+ },
+ emphasis: {
+ color: textColor.active,
+ weight: fontWeights.normal,
+ },
+ emphasisStrong: {
+ color: textColor.active,
+ weight: fontWeights.bold,
+ },
+ linkUrl: {
+ color: colors.lime[500],
+ weight: fontWeights.normal,
+ // TODO: add underline
+ },
+ linkText: {
+ color: colors.red[500],
+ weight: fontWeights.normal,
+ // TODO: add italic
+ },
+};
+
+const shadowAlpha: NumberToken = {
+ value: 0.12,
+ type: "number",
+};
+
+const theme: Theme = {
+ name: "light",
+ backgroundColor,
+ borderColor,
+ textColor,
+ iconColor,
+ editor,
+ syntax,
+ player,
+ shadowAlpha,
+};
+
+export default theme;
@@ -0,0 +1,147 @@
+import { ColorToken, FontWeightToken, NumberToken } from "../tokens";
+import { withOpacity } from "../utils/color";
+
+export interface SyntaxHighlightStyle {
+ color: ColorToken;
+ weight: FontWeightToken;
+}
+
+export interface Player {
+ baseColor: ColorToken;
+ cursorColor: ColorToken;
+ selectionColor: ColorToken;
+ borderColor: ColorToken;
+}
+export function buildPlayer(
+ color: ColorToken,
+ cursorOpacity?: number,
+ selectionOpacity?: number,
+ borderOpacity?: number
+) {
+ return {
+ baseColor: color,
+ cursorColor: withOpacity(color, cursorOpacity || 1.0),
+ selectionColor: withOpacity(color, selectionOpacity || 0.24),
+ borderColor: withOpacity(color, borderOpacity || 0.8),
+ }
+}
+
+export interface BackgroundColorSet {
+ base: ColorToken;
+ hovered: ColorToken;
+ active: ColorToken;
+ focused: ColorToken;
+}
+
+export interface Syntax {
+ primary: SyntaxHighlightStyle;
+ comment: SyntaxHighlightStyle;
+ punctuation: SyntaxHighlightStyle;
+ constant: SyntaxHighlightStyle;
+ keyword: SyntaxHighlightStyle;
+ function: SyntaxHighlightStyle;
+ type: SyntaxHighlightStyle;
+ variant: SyntaxHighlightStyle;
+ property: SyntaxHighlightStyle;
+ enum: SyntaxHighlightStyle;
+ operator: SyntaxHighlightStyle;
+ string: SyntaxHighlightStyle;
+ number: SyntaxHighlightStyle;
+ boolean: SyntaxHighlightStyle;
+ predictive: SyntaxHighlightStyle;
+ // TODO: Either move the following or rename
+ title: SyntaxHighlightStyle;
+ emphasis: SyntaxHighlightStyle;
+ emphasisStrong: SyntaxHighlightStyle;
+ linkUrl: SyntaxHighlightStyle;
+ linkText: SyntaxHighlightStyle;
+};
+
+export default interface Theme {
+ name: string;
+ backgroundColor: {
+ 100: BackgroundColorSet;
+ 300: BackgroundColorSet;
+ 500: BackgroundColorSet;
+ on300: BackgroundColorSet;
+ on500: BackgroundColorSet;
+ ok: BackgroundColorSet;
+ error: BackgroundColorSet;
+ warning: BackgroundColorSet;
+ info: BackgroundColorSet;
+ };
+ borderColor: {
+ primary: ColorToken;
+ secondary: ColorToken;
+ muted: ColorToken;
+ focused: ColorToken;
+ active: ColorToken;
+ ok: ColorToken;
+ error: ColorToken;
+ warning: ColorToken;
+ info: ColorToken;
+ };
+ textColor: {
+ primary: ColorToken;
+ secondary: ColorToken;
+ muted: ColorToken;
+ placeholder: ColorToken;
+ active: ColorToken;
+ feature: ColorToken;
+ ok: ColorToken;
+ error: ColorToken;
+ warning: ColorToken;
+ info: ColorToken;
+ };
+ iconColor: {
+ primary: ColorToken;
+ secondary: ColorToken;
+ muted: ColorToken;
+ placeholder: ColorToken;
+ active: ColorToken;
+ feature: ColorToken;
+ ok: ColorToken;
+ error: ColorToken;
+ warning: ColorToken;
+ info: ColorToken;
+ };
+ editor: {
+ background: ColorToken;
+ indent_guide: ColorToken;
+ indent_guide_active: ColorToken;
+ line: {
+ active: ColorToken;
+ highlighted: ColorToken;
+ inserted: ColorToken;
+ deleted: ColorToken;
+ modified: ColorToken;
+ };
+ highlight: {
+ selection: ColorToken;
+ occurrence: ColorToken;
+ activeOccurrence: ColorToken;
+ matchingBracket: ColorToken;
+ match: ColorToken;
+ activeMatch: ColorToken;
+ related: ColorToken;
+ };
+ gutter: {
+ primary: ColorToken;
+ active: ColorToken;
+ };
+ };
+
+ syntax: Syntax,
+
+ player: {
+ 1: Player;
+ 2: Player;
+ 3: Player;
+ 4: Player;
+ 5: Player;
+ 6: Player;
+ 7: Player;
+ 8: Player;
+ };
+ shadowAlpha: NumberToken;
+}
@@ -0,0 +1,102 @@
+import { colorRamp } from "./utils/color";
+
+interface Token<V, T> {
+ value: V,
+ type: T
+}
+
+export type FontFamily = string;
+export type FontFamilyToken = Token<FontFamily, "fontFamily">;
+function fontFamily(value: FontFamily): FontFamilyToken {
+ return {
+ value,
+ type: "fontFamily"
+ }
+}
+export const fontFamilies = {
+ sans: fontFamily("Zed Sans"),
+ mono: fontFamily("Zed Mono"),
+}
+
+export type FontSize = number;
+export type FontSizeToken = Token<FontSize, "fontSize">;
+function fontSize(value: FontSize) {
+ return {
+ value,
+ type: "fontSize"
+ };
+}
+export const fontSizes = {
+ "3xs": fontSize(8),
+ "2xs": fontSize(10),
+ xs: fontSize(12),
+ sm: fontSize(14),
+ md: fontSize(16),
+ lg: fontSize(18),
+ xl: fontSize(20),
+};
+
+export type FontWeight =
+ | "thin"
+ | "extra_light"
+ | "light"
+ | "normal"
+ | "medium"
+ | "semibold"
+ | "bold"
+ | "extra_bold"
+ | "black";
+export type FontWeightToken = Token<FontWeight, "fontWeight">;
+function fontWeight(value: FontWeight): FontWeightToken {
+ return {
+ value,
+ type: "fontWeight"
+ };
+}
+export const fontWeights = {
+ "thin": fontWeight("thin"),
+ "extra_light": fontWeight("extra_light"),
+ "light": fontWeight("light"),
+ "normal": fontWeight("normal"),
+ "medium": fontWeight("medium"),
+ "semibold": fontWeight("semibold"),
+ "bold": fontWeight("bold"),
+ "extra_bold": fontWeight("extra_bold"),
+ "black": fontWeight("black"),
+}
+
+export type Color = string;
+export interface ColorToken {
+ value: Color,
+ type: "color",
+ step?: number,
+}
+export const colors = {
+ neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
+ rose: colorRamp("#F43F5EFF"),
+ red: colorRamp("#EF4444FF"),
+ orange: colorRamp("#F97316FF"),
+ amber: colorRamp("#F59E0BFF"),
+ yellow: colorRamp("#EAB308FF"),
+ lime: colorRamp("#84CC16FF"),
+ green: colorRamp("#22C55EFF"),
+ emerald: colorRamp("#10B981FF"),
+ teal: colorRamp("#14B8A6FF"),
+ cyan: colorRamp("#06BBD4FF"),
+ sky: colorRamp("#0EA5E9FF"),
+ blue: colorRamp("#3B82F6FF"),
+ indigo: colorRamp("#6366F1FF"),
+ violet: colorRamp("#8B5CF6FF"),
+ purple: colorRamp("#A855F7FF"),
+ fuschia: colorRamp("#D946E4FF"),
+ pink: colorRamp("#EC4899FF"),
+}
+
+export type NumberToken = Token<number, "number">;
+
+export default {
+ fontFamilies,
+ fontSizes,
+ fontWeights,
+ colors,
+};
@@ -0,0 +1,52 @@
+import chroma, { Scale } from "chroma-js";
+import { ColorToken } from "../tokens";
+
+export type Color = string;
+export type ColorRampStep = { value: Color; type: "color"; step: number };
+export type ColorRamp = {
+ [index: number]: ColorRampStep;
+};
+
+export function colorRamp(
+ color: Color | [Color, Color],
+ options?: { steps?: number; increment?: number; }
+): ColorRamp {
+ let scale: Scale;
+ if (Array.isArray(color)) {
+ const [startColor, endColor] = color;
+ scale = chroma.scale([startColor, endColor]);
+ } else {
+ let hue = Math.round(chroma(color).hsl()[0]);
+ let startColor = chroma.hsl(hue, 0.88, 0.96);
+ let endColor = chroma.hsl(hue, 0.68, 0.12);
+ scale = chroma
+ .scale([startColor, color, endColor])
+ .domain([0, 0.5, 1])
+ .mode("hsl")
+ .gamma(1)
+ // .correctLightness(true)
+ .padding([0, 0]);
+ }
+
+ const ramp: ColorRamp = {};
+ const steps = options?.steps || 10;
+ const increment = options?.increment || 100;
+
+ scale.colors(steps, "hex").forEach((color, ix) => {
+ const step = ix * increment;
+ ramp[step] = {
+ value: color,
+ step,
+ type: "color",
+ };
+ });
+
+ return ramp;
+}
+
+export function withOpacity(color: ColorToken, opacity: number): ColorToken {
+ return {
+ ...color,
+ value: chroma(color.value).alpha(opacity).hex()
+ };
+}
@@ -0,0 +1,35 @@
+import { snakeCase } from "case-anything";
+
+// https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case
+
+// Typescript magic to convert any string from camelCase to snake_case at compile time
+type SnakeCase<S> =
+ S extends string ?
+ S extends `${infer T}${infer U}` ?
+ `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}` :
+ S :
+ S;
+
+type SnakeCased<Type> = {
+ [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
+}
+
+export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
+ const snakeObject: any = {};
+ for (const key in object) {
+ snakeObject[snakeCase(key)] = snakeCaseValue(object[key]);
+ }
+ return snakeObject;
+}
+
+function snakeCaseValue(value: any): any {
+ if (typeof value === "object") {
+ if (Array.isArray(value)) {
+ return value.map(snakeCaseValue);
+ } else {
+ return snakeCaseTree(value);
+ }
+ } else {
+ return value;
+ }
+}
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "es2015",
+ "module": "commonjs",
+ "esModuleInterop": true,
+ "noImplicitAny": true,
+ "removeComments": true,
+ "preserveConstEnums": true,
+ "sourceMap": true
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}