diff --git a/.gitignore b/.gitignore index e88434ce4e..a733bb3bb7 100644 --- a/.gitignore +++ b/.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 diff --git a/.zed.toml b/.zed.toml index 0cbe5c59a5..fae32125f4 100644 --- a/.zed.toml +++ b/.zed.toml @@ -1 +1 @@ -collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "Kethku"] +collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "gibusu", "Kethku"] diff --git a/Cargo.lock b/Cargo.lock index 3bd89c4d79..01dcaea9ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,7 +135,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -168,6 +168,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" +[[package]] +name = "assets" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "rust-embed", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -300,7 +309,7 @@ dependencies = [ "polling", "vec-arena", "waker-fn", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -355,7 +364,7 @@ dependencies = [ "futures-lite", "once_cell", "signal-hook", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -449,7 +458,7 @@ dependencies = [ "async-io", "async-lock", "async-process", - "crossbeam-utils", + "crossbeam-utils 0.8.2", "futures-channel", "futures-core", "futures-io", @@ -541,7 +550,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -555,6 +564,7 @@ dependencies = [ "log", "serde", "serde_json", + "settings", "smol", "surf", "tempdir", @@ -632,7 +642,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.33.3", - "env_logger", + "env_logger 0.8.3", "lazy_static", "lazycell", "log", @@ -747,6 +757,7 @@ dependencies = [ "language", "project", "search", + "settings", "theme", "workspace", ] @@ -899,6 +910,7 @@ dependencies = [ "editor", "gpui", "postage", + "settings", "theme", "time 0.3.7", "util", @@ -916,7 +928,7 @@ dependencies = [ "num-traits", "serde", "time 0.1.44", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -971,9 +983,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.0-beta.2" +version = "3.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c" dependencies = [ "atty", "bitflags", @@ -983,24 +995,36 @@ dependencies = [ "os_str_bytes", "strsim 0.10.0", "termcolor", - "textwrap 0.12.1", - "unicode-width", - "vec_map", + "textwrap 0.15.0", ] [[package]] name = "clap_derive" -version = "3.0.0-beta.2" +version = "3.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" dependencies = [ - "heck", + "heck 0.4.0", "proc-macro-error", "proc-macro2", "quote", "syn", ] +[[package]] +name = "cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 3.1.8", + "core-foundation", + "core-services", + "dirs 3.0.1", + "ipc-channel", + "plist", + "serde", +] + [[package]] name = "client" version = "0.1.0" @@ -1083,6 +1107,60 @@ dependencies = [ "objc", ] +[[package]] +name = "collab" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-io", + "async-sqlx-session", + "async-std", + "async-trait", + "async-tungstenite", + "base64 0.13.0", + "clap 3.1.8", + "client", + "collections", + "comrak", + "ctor", + "editor", + "either", + "env_logger 0.8.3", + "envy", + "futures", + "gpui", + "handlebars", + "http-auth-basic", + "json_env_logger", + "jwt-simple", + "language", + "lazy_static", + "lipsum", + "log", + "lsp", + "oauth2", + "oauth2-surf", + "parking_lot", + "project", + "rand 0.8.3", + "rpc", + "rust-embed", + "scrypt", + "serde", + "serde_json", + "settings", + "sha-1 0.9.6", + "sqlx 0.5.5", + "surf", + "theme", + "tide", + "tide-compress", + "time 0.2.27", + "toml", + "util", + "workspace", +] + [[package]] name = "collections" version = "0.1.0" @@ -1096,6 +1174,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "command_palette" +version = "0.1.0" +dependencies = [ + "ctor", + "editor", + "env_logger 0.8.3", + "fuzzy", + "gpui", + "picker", + "serde_json", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "comrak" version = "0.10.1" @@ -1149,6 +1244,7 @@ dependencies = [ "client", "gpui", "postage", + "settings", "theme", "workspace", ] @@ -1207,6 +1303,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core-services" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b344b958cae90858bf6086f49599ecc5ec8698eacad0ea155509ba11fab347" +dependencies = [ + "core-foundation", +] + [[package]] name = "core-text" version = "19.2.0" @@ -1261,6 +1366,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + [[package]] name = "crossbeam-channel" version = "0.5.0" @@ -1268,7 +1383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils", + "crossbeam-utils 0.8.2", ] [[package]] @@ -1279,7 +1394,7 @@ checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-utils 0.8.2", ] [[package]] @@ -1289,7 +1404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d60ab4a8dba064f2fbb5aa270c28da5cf4bbd0e72dae1140a6b0353a779dbe00" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils", + "crossbeam-utils 0.8.2", "lazy_static", "loom", "memoffset", @@ -1303,7 +1418,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils", + "crossbeam-utils 0.8.2", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] @@ -1407,7 +1533,7 @@ dependencies = [ "openssl-sys", "schannel", "socket2 0.4.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1423,7 +1549,7 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1498,6 +1624,7 @@ dependencies = [ "postage", "project", "serde_json", + "settings", "theme", "unindent", "util", @@ -1568,7 +1695,7 @@ checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1579,7 +1706,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1602,7 +1729,7 @@ checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" dependencies = [ "lazy_static", "libc", - "winapi", + "winapi 0.3.9", "wio", ] @@ -1648,10 +1775,11 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger", + "env_logger 0.8.3", "futures", "fuzzy", "gpui", + "indoc", "itertools", "language", "lazy_static", @@ -1664,6 +1792,7 @@ dependencies = [ "rand 0.8.3", "rpc", "serde", + "settings", "smallvec", "smol", "snippet", @@ -1714,6 +1843,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "log", +] + [[package]] name = "env_logger" version = "0.8.3" @@ -1736,6 +1874,15 @@ dependencies = [ "serde", ] +[[package]] +name = "erased-serde" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad132dd8d0d0b546348d7d86cb3191aad14b34e5f979781fc005c80d4ac67ffd" +dependencies = [ + "serde", +] + [[package]] name = "etagere" version = "0.2.4" @@ -1779,9 +1926,9 @@ checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" [[package]] name = "fastrand" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -1818,12 +1965,14 @@ version = "0.1.0" dependencies = [ "ctor", "editor", - "env_logger", + "env_logger 0.8.3", "fuzzy", "gpui", + "picker", "postage", "project", "serde_json", + "settings", "theme", "util", "workspace", @@ -1897,7 +2046,7 @@ dependencies = [ "pathfinder_simd", "servo-fontconfig", "walkdir", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1982,6 +2131,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + [[package]] name = "funty" version = "1.1.0" @@ -2113,7 +2278,7 @@ dependencies = [ "libc", "log", "rustc_version", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2224,6 +2389,7 @@ dependencies = [ "editor", "gpui", "postage", + "settings", "text", "workspace", ] @@ -2245,7 +2411,7 @@ dependencies = [ "core-text", "ctor", "dhat", - "env_logger", + "env_logger 0.8.3", "etagere", "font-kit", "foreign-types", @@ -2360,6 +2526,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.18" @@ -2441,7 +2613,7 @@ checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes 1.0.1", "fnv", - "itoa", + "itoa 0.4.7", ] [[package]] @@ -2519,7 +2691,7 @@ version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.2", "globset", "lazy_static", "log", @@ -2584,6 +2756,34 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipc-channel" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb1d9211085f0ea6f1379d944b93c4d07e8207aa3bcf49f37eda12b85081887" +dependencies = [ + "bincode", + "crossbeam-channel 0.4.4", + "fnv", + "lazy_static", + "libc", + "mio", + "rand 0.7.3", + "serde", + "tempfile", + "uuid", + "winapi 0.3.9", +] + [[package]] name = "isahc" version = "0.9.14" @@ -2591,7 +2791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a" dependencies = [ "bytes 0.5.6", - "crossbeam-utils", + "crossbeam-utils 0.8.2", "curl", "curl-sys", "flume", @@ -2622,6 +2822,12 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jobserver" version = "0.1.24" @@ -2662,6 +2868,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json_env_logger" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2ec540ea0448b187d3a8b4a9f13e75527d06ef76b3a2baa1cd982aecb62ce2" +dependencies = [ + "env_logger 0.7.1", + "kv-log-macro", + "log", + "serde_json", +] + [[package]] name = "jwt-simple" version = "0.10.1" @@ -2696,6 +2914,16 @@ dependencies = [ "sha2 0.9.5", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kurbo" version = "0.8.1" @@ -2725,7 +2953,7 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger", + "env_logger 0.8.3", "futures", "fuzzy", "gpui", @@ -2745,6 +2973,7 @@ dependencies = [ "text", "theme", "tree-sitter", + "tree-sitter-json", "tree-sitter-rust", "unindent", "util", @@ -2791,7 +3020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" dependencies = [ "cfg-if 1.0.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2822,6 +3051,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + [[package]] name = "lipsum" version = "0.8.0" @@ -2843,11 +3081,12 @@ dependencies = [ [[package]] name = "log" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if 1.0.0", + "serde", "value-bag", ] @@ -2880,7 +3119,7 @@ dependencies = [ "async-pipe", "collections", "ctor", - "env_logger", + "env_logger 0.8.3", "futures", "gpui", "log", @@ -2928,6 +3167,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md-5" version = "0.9.1" @@ -3021,6 +3266,37 @@ dependencies = [ "autocfg 1.0.1", ] +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + [[package]] name = "multimap" version = "0.8.3" @@ -3037,6 +3313,17 @@ dependencies = [ "socket2 0.3.19", ] +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "nom" version = "5.1.2" @@ -3258,9 +3545,12 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "2.4.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] [[package]] name = "outline" @@ -3271,7 +3561,9 @@ dependencies = [ "gpui", "language", "ordered-float", + "picker", "postage", + "settings", "smol", "text", "workspace", @@ -3316,7 +3608,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3443,6 +3735,21 @@ dependencies = [ "indexmap", ] +[[package]] +name = "picker" +version = "0.1.0" +dependencies = [ + "ctor", + "editor", + "env_logger 0.8.3", + "gpui", + "serde_json", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "pico-args" version = "0.4.0" @@ -3505,6 +3812,20 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "plist" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" +dependencies = [ + "base64 0.13.0", + "indexmap", + "line-wrap", + "serde", + "time 0.3.7", + "xml-rs", +] + [[package]] name = "png" version = "0.16.8" @@ -3527,7 +3848,7 @@ dependencies = [ "libc", "log", "wepoll-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3607,9 +3928,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] @@ -3641,6 +3962,7 @@ dependencies = [ "rpc", "serde", "serde_json", + "settings", "sha2 0.10.2", "similar", "smol", @@ -3660,6 +3982,7 @@ dependencies = [ "postage", "project", "serde_json", + "settings", "theme", "util", "workspace", @@ -3671,11 +3994,16 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", + "futures", "fuzzy", "gpui", + "language", + "lsp", "ordered-float", + "picker", "postage", "project", + "settings", "smol", "text", "util", @@ -3699,7 +4027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603" dependencies = [ "bytes 1.0.1", - "heck", + "heck 0.3.3", "itertools", "log", "multimap", @@ -3764,7 +4092,7 @@ dependencies = [ "libc", "rand_core 0.3.1", "rdrand", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3881,9 +4209,9 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" dependencies = [ - "crossbeam-channel", + "crossbeam-channel 0.5.0", "crossbeam-deque", - "crossbeam-utils", + "crossbeam-utils 0.8.2", "lazy_static", "num_cpus", ] @@ -3945,7 +4273,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3985,7 +4313,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4012,11 +4340,11 @@ dependencies = [ "async-tungstenite", "base64 0.13.0", "clock", + "collections", "futures", "gpui", "log", "parking_lot", - "postage", "prost", "prost-build", "rand 0.8.3", @@ -4162,6 +4490,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "salsa20" version = "0.8.0" @@ -4187,7 +4521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4274,7 +4608,9 @@ dependencies = [ "log", "postage", "project", + "serde", "serde_json", + "settings", "theme", "unindent", "util", @@ -4346,6 +4682,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_fmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2963a69a2b3918c1dc75a45a18bd3fcd1120e31d3f59deb1b2f9b5d5ffb8baa4" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.64" @@ -4353,7 +4698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "indexmap", - "itoa", + "itoa 0.4.7", "ryu", "serde", ] @@ -4397,7 +4742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.7", "ryu", "serde", ] @@ -4423,6 +4768,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "settings" +version = "0.1.0" +dependencies = [ + "anyhow", + "assets", + "collections", + "gpui", + "schemars", + "serde", + "serde_json", + "serde_path_to_error", + "theme", + "toml", + "util", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -4639,7 +5001,7 @@ checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if 1.0.0", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4649,7 +5011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4723,9 +5085,9 @@ dependencies = [ "bytes 0.5.6", "chrono", "crc", - "crossbeam-channel", + "crossbeam-channel 0.5.0", "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.8.2", "either", "futures-channel", "futures-core", @@ -4733,7 +5095,7 @@ dependencies = [ "hashlink 0.6.0", "hex", "hmac 0.10.1", - "itoa", + "itoa 0.4.7", "libc", "log", "md-5", @@ -4771,9 +5133,9 @@ dependencies = [ "byteorder", "bytes 1.0.1", "crc", - "crossbeam-channel", + "crossbeam-channel 0.5.0", "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.8.2", "dirs 3.0.1", "either", "futures-channel", @@ -4782,7 +5144,7 @@ dependencies = [ "hashlink 0.7.0", "hex", "hmac 0.10.1", - "itoa", + "itoa 0.4.7", "libc", "log", "md-5", @@ -4819,7 +5181,7 @@ dependencies = [ "dotenv", "either", "futures", - "heck", + "heck 0.3.3", "lazy_static", "proc-macro2", "quote", @@ -4840,7 +5202,7 @@ dependencies = [ "dotenv", "either", "futures", - "heck", + "heck 0.3.3", "once_cell", "proc-macro2", "quote", @@ -4975,7 +5337,7 @@ version = "0.1.0" dependencies = [ "arrayvec 0.7.1", "ctor", - "env_logger", + "env_logger 0.8.3", "log", "rand 0.8.3", ] @@ -5007,6 +5369,9 @@ name = "sval" version = "1.0.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" +dependencies = [ + "serde", +] [[package]] name = "svg_fmt" @@ -5036,9 +5401,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.67" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", @@ -5075,16 +5440,16 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", + "fastrand", "libc", - "rand 0.8.3", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5105,7 +5470,7 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger", + "env_logger 0.8.3", "gpui", "lazy_static", "log", @@ -5128,12 +5493,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.12.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" -dependencies = [ - "unicode-width", -] +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "theme" @@ -5158,7 +5520,9 @@ dependencies = [ "gpui", "log", "parking_lot", + "picker", "postage", + "settings", "smol", "theme", "workspace", @@ -5253,7 +5617,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5268,7 +5632,7 @@ dependencies = [ "stdweb", "time-macros", "version_check", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5277,6 +5641,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ + "itoa 1.0.1", "libc", "num_threads", ] @@ -5689,14 +6054,20 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", +] [[package]] name = "value-bag" -version = "1.0.0-alpha.7" +version = "1.0.0-alpha.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" dependencies = [ "ctor", + "erased-serde", + "serde", + "serde_fmt", "sval", "version_check", ] @@ -5729,6 +6100,7 @@ checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" name = "vim" version = "0.1.0" dependencies = [ + "assets", "collections", "editor", "gpui", @@ -5736,6 +6108,8 @@ dependencies = [ "language", "log", "project", + "serde", + "settings", "util", "workspace", ] @@ -5753,7 +6127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" dependencies = [ "same-file", - "winapi", + "winapi 0.3.9", "winapi-util", ] @@ -5910,6 +6284,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -5920,6 +6300,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -5932,7 +6318,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5947,7 +6333,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5965,14 +6351,24 @@ dependencies = [ "parking_lot", "postage", "project", - "schemars", "serde", "serde_json", + "settings", "smallvec", "theme", "util", ] +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "wyz" version = "0.2.0" @@ -5985,6 +6381,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "xmlparser" version = "0.13.3" @@ -5999,26 +6401,28 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "zed" -version = "0.24.0" +version = "0.29.0" dependencies = [ "anyhow", + "assets", "async-compression", "async-recursion", "async-trait", "auto_update", "breadcrumbs", "chat_panel", + "cli", "client", "clock", "collections", + "command_palette", "contacts_panel", - "crossbeam-channel", "ctor", "diagnostics", "dirs 3.0.1", "easy-parallel", "editor", - "env_logger", + "env_logger 0.8.3", "file_finder", "fsevent", "futures", @@ -6052,6 +6456,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", + "settings", "simplelog", "smallvec", "smol", @@ -6077,57 +6482,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "zed-server" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-io", - "async-sqlx-session", - "async-std", - "async-trait", - "async-tungstenite", - "base64 0.13.0", - "clap 3.0.0-beta.2", - "client", - "collections", - "comrak", - "ctor", - "editor", - "either", - "env_logger", - "envy", - "futures", - "gpui", - "handlebars", - "http-auth-basic", - "jwt-simple", - "language", - "lazy_static", - "lipsum", - "lsp", - "oauth2", - "oauth2-surf", - "parking_lot", - "postage", - "project", - "rand 0.8.3", - "rpc", - "rust-embed", - "scrypt", - "serde", - "serde_json", - "sha-1 0.9.6", - "sqlx 0.5.5", - "surf", - "tide", - "tide-compress", - "time 0.2.27", - "toml", - "util", - "workspace", -] - [[package]] name = "zeroize" version = "1.3.0" diff --git a/Dockerfile b/Dockerfile index af4365649f..544d9eae86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,20 +14,20 @@ RUN --mount=type=cache,target=./script/node_modules \ RUN --mount=type=cache,target=./script/node_modules \ script/build-css --release -# Compile server +# Compile collab server RUN --mount=type=cache,target=./script/node_modules \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=./target \ - cargo build --release --package zed-server --bin zed-server + cargo build --release --package collab --bin collab -# Copy server binary out of cached directory +# Copy collab server binary out of cached directory RUN --mount=type=cache,target=./target \ - cp /app/target/release/zed-server /app/zed-server + cp /app/target/release/collab /app/collab -# Copy server binary to the runtime image +# Copy collab server binary to the runtime image FROM debian:bullseye-slim as runtime RUN apt-get update; \ apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates WORKDIR app -COPY --from=builder /app/zed-server /app -ENTRYPOINT ["/app/zed-server"] +COPY --from=builder /app/collab /app +ENTRYPOINT ["/app/collab"] diff --git a/Dockerfile.migrator b/Dockerfile.migrator index 1b7a0adf3a..72e4e41d27 100644 --- a/Dockerfile.migrator +++ b/Dockerfile.migrator @@ -11,5 +11,5 @@ RUN apt-get update; \ apt-get install -y --no-install-recommends libssl1.1 WORKDIR app COPY --from=builder /app/bin/sqlx /app -COPY ./server/migrations /app/migrations +COPY ./collab/migrations /app/migrations ENTRYPOINT ["/app/sqlx", "migrate", "run"] diff --git a/Procfile b/Procfile index 08bff5acc2..a64b411ef3 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ web: cd ../zed.dev && PORT=3000 npx next dev -collab: cd crates/server && cargo run +collab: cd crates/collab && cargo run diff --git a/README.md b/README.md index c380115100..1bac43b694 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ script/sqlx migrate run script/seed-db ``` -Run `zed.dev` and the collaboration server. +Run the web frontend and the collaboration server. ``` brew install foreman diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf b/assets/fonts/zed-mono/zed-mono-extended.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf rename to assets/fonts/zed-mono/zed-mono-extended.ttf diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf b/assets/fonts/zed-mono/zed-mono-extendedbold.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf rename to assets/fonts/zed-mono/zed-mono-extendedbold.ttf diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf b/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf rename to assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf b/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf rename to assets/fonts/zed-mono/zed-mono-extendeditalic.ttf diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf b/assets/fonts/zed-sans/zed-sans-extended.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf rename to assets/fonts/zed-sans/zed-sans-extended.ttf diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf b/assets/fonts/zed-sans/zed-sans-extendedbold.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf rename to assets/fonts/zed-sans/zed-sans-extendedbold.ttf diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf b/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf rename to assets/fonts/zed-sans/zed-sans-extendedbolditalic.ttf diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf b/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf similarity index 100% rename from crates/zed/assets/fonts/zed-sans/zed-sans-extendeditalic.ttf rename to assets/fonts/zed-sans/zed-sans-extendeditalic.ttf diff --git a/crates/zed/assets/icons/broadcast-24.svg b/assets/icons/broadcast-24.svg similarity index 100% rename from crates/zed/assets/icons/broadcast-24.svg rename to assets/icons/broadcast-24.svg diff --git a/crates/zed/assets/icons/comment-16.svg b/assets/icons/comment-16.svg similarity index 100% rename from crates/zed/assets/icons/comment-16.svg rename to assets/icons/comment-16.svg diff --git a/crates/zed/assets/icons/diagnostic-error-10.svg b/assets/icons/diagnostic-error-10.svg similarity index 100% rename from crates/zed/assets/icons/diagnostic-error-10.svg rename to assets/icons/diagnostic-error-10.svg diff --git a/crates/zed/assets/icons/diagnostic-summary-error.svg b/assets/icons/diagnostic-summary-error.svg similarity index 100% rename from crates/zed/assets/icons/diagnostic-summary-error.svg rename to assets/icons/diagnostic-summary-error.svg diff --git a/crates/zed/assets/icons/diagnostic-summary-warning.svg b/assets/icons/diagnostic-summary-warning.svg similarity index 100% rename from crates/zed/assets/icons/diagnostic-summary-warning.svg rename to assets/icons/diagnostic-summary-warning.svg diff --git a/crates/zed/assets/icons/diagnostic-warning-10.svg b/assets/icons/diagnostic-warning-10.svg similarity index 100% rename from crates/zed/assets/icons/diagnostic-warning-10.svg rename to assets/icons/diagnostic-warning-10.svg diff --git a/crates/zed/assets/icons/disclosure-closed.svg b/assets/icons/disclosure-closed.svg similarity index 100% rename from crates/zed/assets/icons/disclosure-closed.svg rename to assets/icons/disclosure-closed.svg diff --git a/crates/zed/assets/icons/disclosure-open.svg b/assets/icons/disclosure-open.svg similarity index 100% rename from crates/zed/assets/icons/disclosure-open.svg rename to assets/icons/disclosure-open.svg diff --git a/crates/zed/assets/icons/file-16.svg b/assets/icons/file-16.svg similarity index 100% rename from crates/zed/assets/icons/file-16.svg rename to assets/icons/file-16.svg diff --git a/crates/zed/assets/icons/folder-tree-16.svg b/assets/icons/folder-tree-16.svg similarity index 100% rename from crates/zed/assets/icons/folder-tree-16.svg rename to assets/icons/folder-tree-16.svg diff --git a/crates/zed/assets/icons/magnifier.svg b/assets/icons/magnifier.svg similarity index 100% rename from crates/zed/assets/icons/magnifier.svg rename to assets/icons/magnifier.svg diff --git a/crates/zed/assets/icons/offline-14.svg b/assets/icons/offline-14.svg similarity index 100% rename from crates/zed/assets/icons/offline-14.svg rename to assets/icons/offline-14.svg diff --git a/crates/zed/assets/icons/signed-out-12.svg b/assets/icons/signed-out-12.svg similarity index 100% rename from crates/zed/assets/icons/signed-out-12.svg rename to assets/icons/signed-out-12.svg diff --git a/crates/zed/assets/icons/user-16.svg b/assets/icons/user-16.svg similarity index 100% rename from crates/zed/assets/icons/user-16.svg rename to assets/icons/user-16.svg diff --git a/crates/zed/assets/icons/x.svg b/assets/icons/x.svg similarity index 100% rename from crates/zed/assets/icons/x.svg rename to assets/icons/x.svg diff --git a/crates/zed/assets/icons/zap.svg b/assets/icons/zap.svg similarity index 100% rename from crates/zed/assets/icons/zap.svg rename to assets/icons/zap.svg diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json new file mode 100644 index 0000000000..506dd362d4 --- /dev/null +++ b/assets/keymaps/default.json @@ -0,0 +1,264 @@ +{ + "*": { + "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", + "cmd-s": "workspace::Save", + "cmd-alt-i": "zed::DebugElements", + "cmd-k cmd-left": "workspace::ActivatePreviousPane", + "cmd-k cmd-right": "workspace::ActivateNextPane", + "cmd-=": "zed::IncreaseBufferFontSize", + "cmd--": "zed::DecreaseBufferFontSize", + "cmd-,": "zed::OpenSettings" + }, + "menu": { + "up": "menu::SelectPrev", + "ctrl-p": "menu::SelectPrev", + "down": "menu::SelectNext", + "ctrl-n": "menu::SelectNext", + "cmd-up": "menu::SelectFirst", + "cmd-down": "menu::SelectLast", + "enter": "menu::Confirm", + "escape": "menu::Cancel" + }, + "Pane": { + "shift-cmd-{": "pane::ActivatePrevItem", + "shift-cmd-}": "pane::ActivateNextItem", + "cmd-w": "pane::CloseActiveItem", + "alt-cmd-w": "pane::CloseInactiveItems", + "ctrl--": "pane::GoBack", + "shift-ctrl-_": "pane::GoForward", + "cmd-k up": [ + "pane::Split", + "Up" + ], + "cmd-k down": [ + "pane::Split", + "Down" + ], + "cmd-k left": [ + "pane::Split", + "Left" + ], + "cmd-k right": [ + "pane::Split", + "Right" + ], + "cmd-shift-F": "project_search::ToggleFocus", + "cmd-f": "project_search::ToggleFocus", + "cmd-g": "search::SelectNextMatch", + "cmd-shift-G": "search::SelectPrevMatch" + }, + "Workspace": { + "cmd-shift-F": "project_search::Deploy", + "cmd-k cmd-t": "theme_selector::Toggle", + "cmd-k t": "theme_selector::Reload", + "cmd-t": "project_symbols::Toggle", + "cmd-p": "file_finder::Toggle", + "cmd-shift-P": "command_palette::Toggle", + "alt-shift-D": "diagnostics::Deploy", + "ctrl-alt-cmd-j": "journal::NewJournalEntry", + "cmd-1": [ + "workspace::ToggleSidebarItemFocus", + { + "side": "Left", + "item_index": 0 + } + ], + "cmd-shift-!": [ + "workspace::ToggleSidebarItem", + { + "side": "Left", + "item_index": 0 + } + ] + }, + "ProjectSearchBar": { + "enter": "project_search::Search", + "cmd-enter": "project_search::SearchInNew" + }, + "BufferSearchBar": { + "escape": "buffer_search::Dismiss", + "cmd-f": "buffer_search::FocusEditor", + "enter": "search::SelectNextMatch", + "shift-enter": "search::SelectPrevMatch" + }, + "Editor": { + "escape": "editor::Cancel", + "backspace": "editor::Backspace", + "ctrl-h": "editor::Backspace", + "delete": "editor::Delete", + "ctrl-d": "editor::Delete", + "tab": "editor::Tab", + "shift-tab": "editor::TabPrev", + "cmd-[": "editor::Outdent", + "cmd-]": "editor::Indent", + "ctrl-shift-K": "editor::DeleteLine", + "alt-backspace": "editor::DeleteToPreviousWordStart", + "alt-h": "editor::DeleteToPreviousWordStart", + "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", + "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", + "alt-delete": "editor::DeleteToNextWordEnd", + "alt-d": "editor::DeleteToNextWordEnd", + "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", + "ctrl-alt-d": "editor::DeleteToNextSubwordEnd", + "cmd-backspace": "editor::DeleteToBeginningOfLine", + "cmd-delete": "editor::DeleteToEndOfLine", + "ctrl-k": "editor::CutToEndOfLine", + "cmd-shift-D": "editor::DuplicateLine", + "ctrl-cmd-up": "editor::MoveLineUp", + "ctrl-cmd-down": "editor::MoveLineDown", + "cmd-x": "editor::Cut", + "cmd-c": "editor::Copy", + "cmd-v": "editor::Paste", + "cmd-z": "editor::Undo", + "cmd-shift-Z": "editor::Redo", + "up": "editor::MoveUp", + "down": "editor::MoveDown", + "left": "editor::MoveLeft", + "right": "editor::MoveRight", + "ctrl-p": "editor::MoveUp", + "ctrl-n": "editor::MoveDown", + "ctrl-b": "editor::MoveLeft", + "ctrl-f": "editor::MoveRight", + "alt-left": "editor::MoveToPreviousWordStart", + "alt-b": "editor::MoveToPreviousWordStart", + "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", + "ctrl-alt-b": "editor::MoveToPreviousSubwordStart", + "alt-right": "editor::MoveToNextWordEnd", + "alt-f": "editor::MoveToNextWordEnd", + "ctrl-alt-right": "editor::MoveToNextSubwordEnd", + "ctrl-alt-f": "editor::MoveToNextSubwordEnd", + "cmd-left": "editor::MoveToBeginningOfLine", + "ctrl-a": "editor::MoveToBeginningOfLine", + "cmd-right": "editor::MoveToEndOfLine", + "ctrl-e": "editor::MoveToEndOfLine", + "cmd-up": "editor::MoveToBeginning", + "cmd-down": "editor::MoveToEnd", + "shift-up": "editor::SelectUp", + "ctrl-shift-P": "editor::SelectUp", + "shift-down": "editor::SelectDown", + "ctrl-shift-N": "editor::SelectDown", + "shift-left": "editor::SelectLeft", + "ctrl-shift-B": "editor::SelectLeft", + "shift-right": "editor::SelectRight", + "ctrl-shift-F": "editor::SelectRight", + "alt-shift-left": "editor::SelectToPreviousWordStart", + "alt-shift-B": "editor::SelectToPreviousWordStart", + "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart", + "ctrl-alt-shift-B": "editor::SelectToPreviousSubwordStart", + "alt-shift-right": "editor::SelectToNextWordEnd", + "alt-shift-F": "editor::SelectToNextWordEnd", + "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd", + "cmd-shift-up": "editor::SelectToBeginning", + "cmd-shift-down": "editor::SelectToEnd", + "cmd-a": "editor::SelectAll", + "cmd-l": "editor::SelectLine", + "cmd-shift-L": "editor::SplitSelectionIntoLines", + "cmd-alt-up": "editor::AddSelectionAbove", + "cmd-ctrl-p": "editor::AddSelectionAbove", + "cmd-alt-down": "editor::AddSelectionBelow", + "cmd-ctrl-n": "editor::AddSelectionBelow", + "ctrl-alt-shift-F": "editor::SelectToNextSubwordEnd", + "cmd-shift-left": [ + "editor::SelectToBeginningOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-shift-A": [ + "editor::SelectToBeginningOfLine", + { + "stop_at_soft_wraps": true + } + ], + "cmd-shift-right": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "ctrl-shift-E": [ + "editor::SelectToEndOfLine", + { + "stop_at_soft_wraps": true + } + ], + "cmd-d": [ + "editor::SelectNext", + { + "replace_newest": false + } + ], + "cmd-k cmd-d": [ + "editor::SelectNext", + { + "replace_newest": true + } + ], + "cmd-/": "editor::ToggleComments", + "alt-up": "editor::SelectLargerSyntaxNode", + "ctrl-w": "editor::SelectLargerSyntaxNode", + "alt-down": "editor::SelectSmallerSyntaxNode", + "ctrl-shift-W": "editor::SelectSmallerSyntaxNode", + "cmd-u": "editor::UndoSelection", + "cmd-shift-U": "editor::RedoSelection", + "f8": "editor::GoToNextDiagnostic", + "shift-f8": "editor::GoToPrevDiagnostic", + "f2": "editor::Rename", + "f12": "editor::GoToDefinition", + "alt-shift-f12": "editor::FindAllReferences", + "ctrl-m": "editor::MoveToEnclosingBracket", + "pageup": "editor::PageUp", + "pagedown": "editor::PageDown", + "alt-cmd-[": "editor::Fold", + "alt-cmd-]": "editor::UnfoldLines", + "alt-cmd-f": "editor::FoldSelectedRanges", + "ctrl-space": "editor::ShowCompletions", + "cmd-.": "editor::ToggleCodeActions", + "alt-enter": "editor::OpenExcerpts", + "cmd-f10": "editor::RestartLanguageServer" + }, + "Editor && renaming": { + "enter": "editor::ConfirmRename" + }, + "Editor && showing_completions": { + "enter": "editor::ConfirmCompletion", + "tab": "editor::ConfirmCompletion" + }, + "Editor && showing_code_actions": { + "enter": "editor::ConfirmCodeAction" + }, + "Editor && mode == full": { + "enter": "editor::Newline", + "cmd-f": [ + "buffer_search::Deploy", + { + "focus": true + } + ], + "cmd-e": [ + "buffer_search::Deploy", + { + "focus": false + } + ], + "cmd-shift-O": "outline::Toggle", + "ctrl-g": "go_to_line::Toggle" + }, + "Editor && mode == auto_height": { + "alt-enter": [ + "editor::Input", + "\n" + ] + }, + "GoToLine": { + "escape": "go_to_line::Toggle", + "enter": "go_to_line::Confirm" + }, + "ChatPanel": { + "enter": "chat_panel::Send" + }, + "ProjectPanel": { + "left": "project_panel::CollapseSelectedEntry", + "right": "project_panel::ExpandSelectedEntry" + } +} \ No newline at end of file diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json new file mode 100644 index 0000000000..312416f42c --- /dev/null +++ b/assets/keymaps/vim.json @@ -0,0 +1,93 @@ +{ + "Editor && VimControl": { + "i": [ + "vim::SwitchMode", + "Insert" + ], + "g": [ + "vim::PushOperator", + { + "Namespace": "G" + } + ], + "h": "vim::Left", + "j": "vim::Down", + "k": "vim::Up", + "l": "vim::Right", + "0": "vim::StartOfLine", + "shift-$": "vim::EndOfLine", + "shift-G": "vim::EndOfDocument", + "w": "vim::NextWordStart", + "shift-W": [ + "vim::NextWordStart", + { + "ignorePunctuation": true + } + ], + "e": "vim::NextWordEnd", + "shift-E": [ + "vim::NextWordEnd", + { + "ignorePunctuation": true + } + ], + "b": "vim::PreviousWordStart", + "shift-B": [ + "vim::PreviousWordStart", + { + "ignorePunctuation": true + } + ], + "escape": [ + "vim::SwitchMode", + "Normal" + ] + }, + "Editor && vim_operator == g": { + "g": "vim::StartOfDocument" + }, + "Editor && vim_mode == insert": { + "escape": "vim::NormalBefore", + "ctrl-c": "vim::NormalBefore" + }, + "Editor && vim_mode == normal": { + "c": [ + "vim::PushOperator", + "Change" + ], + "d": [ + "vim::PushOperator", + "Delete" + ] + }, + "Editor && vim_operator == c": { + "w": [ + "vim::NextWordEnd", + { + "ignorePunctuation": false + } + ], + "shift-W": [ + "vim::NextWordEnd", + { + "ignorePunctuation": true + } + ] + }, + "Editor && vim_operator == d": { + "w": [ + "vim::NextWordStart", + { + "ignorePunctuation": false, + "stopAtNewline": true + } + ], + "shift-W": [ + "vim::NextWordStart", + { + "ignorePunctuation": true, + "stopAtNewline": true + } + ] + } +} \ No newline at end of file diff --git a/assets/themes/dark.json b/assets/themes/dark.json new file mode 100644 index 0000000000..5003d23d4d --- /dev/null +++ b/assets/themes/dark.json @@ -0,0 +1,1338 @@ +{ + "selector": { + "background": "#1c1c1c", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#4f8ff7", + "weight": "bold", + "size": 14 + } + }, + "active_item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#4f8ff7", + "weight": "bold", + "size": 14 + }, + "background": "#2b2b2b" + }, + "border": { + "color": "#070707", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#000000", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "margin": { + "bottom": 52, + "top": 52 + }, + "shadow": { + "blur": 16, + "color": "#00000052", + "offset": [ + 0, + 2 + ] + } + }, + "workspace": { + "background": "#1c1c1c", + "leader_border_opacity": 0.7, + "leader_border_width": 2, + "tab": { + "height": 32, + "background": "#1c1c1c", + "icon_close": "#555555", + "icon_close_active": "#ffffff", + "icon_conflict": "#f6a724", + "icon_dirty": "#135acd", + "icon_width": 8, + "spacing": 8, + "text": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "border": { + "color": "#070707", + "width": 1, + "left": true, + "bottom": true, + "overlay": true + }, + "padding": { + "left": 8, + "right": 8 + } + }, + "active_tab": { + "height": 32, + "background": "#000000", + "icon_close": "#555555", + "icon_close_active": "#ffffff", + "icon_conflict": "#f6a724", + "icon_dirty": "#135acd", + "icon_width": 8, + "spacing": 8, + "text": { + "family": "Zed Sans", + "color": "#ffffff", + "size": 14 + }, + "border": { + "color": "#070707", + "width": 1, + "left": true, + "bottom": false, + "overlay": true + }, + "padding": { + "left": 8, + "right": 8 + } + }, + "left_sidebar": { + "width": 30, + "background": "#1c1c1c", + "border": { + "color": "#070707", + "width": 1, + "right": true + }, + "item": { + "height": 32, + "icon_color": "#9c9c9c", + "icon_size": 18 + }, + "active_item": { + "height": 32, + "icon_color": "#ffffff", + "icon_size": 18 + }, + "resize_handle": { + "background": "#070707", + "padding": { + "left": 1 + } + } + }, + "right_sidebar": { + "width": 30, + "background": "#1c1c1c", + "border": { + "color": "#070707", + "width": 1, + "left": true + }, + "item": { + "height": 32, + "icon_color": "#9c9c9c", + "icon_size": 18 + }, + "active_item": { + "height": 32, + "icon_color": "#ffffff", + "icon_size": 18 + }, + "resize_handle": { + "background": "#070707", + "padding": { + "left": 1 + } + } + }, + "pane_divider": { + "color": "#232323", + "width": 1 + }, + "status_bar": { + "height": 24, + "item_spacing": 8, + "padding": { + "left": 6, + "right": 6 + }, + "border": { + "color": "#070707", + "width": 1, + "top": true, + "overlay": true + }, + "cursor_position": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "diagnostic_message": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "lsp_message": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "auto_update_progress_message": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "auto_update_done_message": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + } + }, + "titlebar": { + "avatar_width": 18, + "height": 32, + "background": "#2b2b2b", + "share_icon_color": "#9c9c9c", + "share_icon_active_color": "#2472f2", + "title": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + }, + "avatar": { + "corner_radius": 10, + "border": { + "color": "#00000088", + "width": 1 + } + }, + "avatar_ribbon": { + "height": 3, + "width": 12 + }, + "border": { + "color": "#070707", + "width": 1, + "bottom": true + }, + "sign_in_prompt": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 12, + "border": { + "color": "#070707", + "width": 1 + }, + "corner_radius": 6, + "margin": { + "top": 1, + "right": 6 + }, + "padding": { + "left": 6, + "right": 6 + } + }, + "hovered_sign_in_prompt": { + "family": "Zed Sans", + "color": "#ffffff", + "size": 12, + "border": { + "color": "#070707", + "width": 1 + }, + "corner_radius": 6, + "margin": { + "top": 1, + "right": 6 + }, + "padding": { + "left": 6, + "right": 6 + } + }, + "offline_icon": { + "color": "#9c9c9c", + "width": 16, + "padding": { + "right": 4 + } + }, + "outdated_warning": { + "family": "Zed Sans", + "color": "#f7bb57", + "size": 13 + } + }, + "toolbar": { + "height": 34, + "background": "#000000", + "border": { + "color": "#232323", + "width": 1, + "bottom": true + }, + "item_spacing": 8, + "padding": { + "left": 16, + "right": 8, + "top": 4, + "bottom": 4 + } + }, + "breadcrumbs": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14, + "padding": { + "left": 6 + } + }, + "disconnected_overlay": { + "family": "Zed Sans", + "color": "#ffffff", + "size": 14, + "background": "#000000aa" + } + }, + "editor": { + "text_color": "#d5d5d5", + "background": "#000000", + "active_line_background": "#ffffff12", + "code_actions_indicator": "#555555", + "diff_background_deleted": "#f15656", + "diff_background_inserted": "#1b9447", + "document_highlight_read_background": "#ffffff1f", + "document_highlight_write_background": "#ffffff29", + "error_color": "#f15656", + "gutter_background": "#000000", + "gutter_padding_factor": 3.5, + "highlighted_line_background": "#ffffff1f", + "line_number": "#474747", + "line_number_active": "#ffffff", + "rename_fade": 0.6, + "unnecessary_code_fade": 0.5, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "guest_selections": [ + { + "cursor": "#79ba16", + "selection": "#79ba163d" + }, + { + "cursor": "#d430e0", + "selection": "#d430e03d" + }, + { + "cursor": "#ee670a", + "selection": "#ee670a3d" + }, + { + "cursor": "#993bf3", + "selection": "#993bf33d" + }, + { + "cursor": "#16d6c1", + "selection": "#16d6c13d" + }, + { + "cursor": "#ef59a3", + "selection": "#ef59a33d" + }, + { + "cursor": "#f7bf17", + "selection": "#f7bf173d" + } + ], + "autocomplete": { + "background": "#000000", + "corner_radius": 8, + "padding": 4, + "border": { + "color": "#232323", + "width": 1 + }, + "item": { + "corner_radius": 6, + "padding": { + "bottom": 2, + "left": 6, + "right": 6, + "top": 2 + } + }, + "hovered_item": { + "corner_radius": 6, + "padding": { + "bottom": 2, + "left": 6, + "right": 6, + "top": 2 + }, + "background": "#ffffff14" + }, + "margin": { + "left": -14 + }, + "match_highlight": { + "family": "Zed Mono", + "color": "#4f8ff7", + "size": 14 + }, + "selected_item": { + "corner_radius": 6, + "padding": { + "bottom": 2, + "left": 6, + "right": 6, + "top": 2 + }, + "background": "#ffffff1f" + } + }, + "diagnostic_header": { + "background": "#1c1c1c", + "icon_width_factor": 1.5, + "text_scale_factor": 0.857, + "border": { + "color": "#232323", + "width": 1, + "bottom": true, + "top": true + }, + "code": { + "family": "Zed Mono", + "color": "#808080", + "size": 14, + "margin": { + "left": 10 + } + }, + "message": { + "highlight_text": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14, + "weight": "bold" + }, + "text": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + } + } + }, + "diagnostic_path_header": { + "background": "#ffffff12", + "text_scale_factor": 0.857, + "filename": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "path": { + "family": "Zed Mono", + "color": "#808080", + "size": 14, + "margin": { + "left": 12 + } + } + }, + "error_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#f15656", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#f15656", + "size": 14, + "weight": "bold" + } + } + }, + "warning_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#f7bb57", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#f7bb57", + "size": 14, + "weight": "bold" + } + } + }, + "information_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14, + "weight": "bold" + } + } + }, + "hint_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_error_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_hint_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_information_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_warning_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#070707", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "weight": "bold" + } + } + }, + "syntax": { + "keyword": "#4f8ff7", + "function": "#f9da82", + "string": "#f99d5f", + "type": "#3eeeda", + "number": "#aeef4b", + "comment": "#aaaaaa", + "property": "#4f8ff7", + "variant": "#53c1f5", + "constant": "#d5d5d5", + "title": { + "color": "#de900c", + "weight": "bold" + }, + "emphasis": "#4f8ff7", + "emphasis_strong": { + "color": "#4f8ff7", + "weight": "bold" + }, + "link_uri": { + "color": "#79ba16", + "underline": true + }, + "link_text": { + "color": "#ee670a", + "italic": true + }, + "list_marker": "#c6c6c6" + } + }, + "project_diagnostics": { + "tab_icon_spacing": 4, + "tab_icon_width": 13, + "tab_summary_spacing": 10, + "empty_message": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 18 + }, + "status_bar_item": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "margin": { + "right": 10 + } + } + }, + "command_palette": { + "keystroke_spacing": 8, + "key": { + "text": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 12 + }, + "corner_radius": 4, + "background": "#0e0e0e80", + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "top": 2, + "bottom": 2, + "left": 8, + "right": 8 + }, + "margin": { + "left": 2 + } + } + }, + "project_panel": { + "padding": { + "top": 6, + "left": 12 + }, + "entry": { + "height": 22, + "icon_color": "#555555", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14 + } + }, + "hovered_entry": { + "height": 22, + "background": "#232323", + "icon_color": "#555555", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14 + } + }, + "selected_entry": { + "height": 22, + "icon_color": "#555555", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + } + }, + "hovered_selected_entry": { + "height": 22, + "background": "#232323", + "icon_color": "#555555", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + } + } + }, + "chat_panel": { + "padding": { + "top": 12, + "left": 12, + "bottom": 12, + "right": 12 + }, + "channel_name": { + "family": "Zed Sans", + "color": "#f1f1f1", + "weight": "bold", + "size": 14 + }, + "channel_name_hash": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "padding": { + "right": 8 + } + }, + "channel_select": { + "header": { + "name": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 0 + }, + "hash": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "margin": { + "right": 8 + } + }, + "corner_radius": 0 + }, + "item": { + "name": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "margin": { + "right": 8 + } + }, + "corner_radius": 0 + }, + "hovered_item": { + "name": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "margin": { + "right": 8 + } + }, + "background": "#232323", + "corner_radius": 6 + }, + "active_item": { + "name": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "margin": { + "right": 8 + } + }, + "corner_radius": 0 + }, + "hovered_active_item": { + "name": { + "family": "Zed Sans", + "color": "#f1f1f1", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#808080", + "size": 14, + "margin": { + "right": 8 + } + }, + "background": "#232323", + "corner_radius": 6 + }, + "menu": { + "background": "#000000", + "corner_radius": 6, + "padding": 4, + "border": { + "color": "#070707", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#00000052", + "offset": [ + 0, + 2 + ] + } + } + }, + "sign_in_prompt": { + "family": "Zed Sans", + "color": "#9c9c9c", + "underline": true, + "size": 14 + }, + "hovered_sign_in_prompt": { + "family": "Zed Sans", + "color": "#f1f1f1", + "underline": true, + "size": 14 + }, + "message": { + "body": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14 + }, + "timestamp": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "padding": { + "bottom": 6 + }, + "sender": { + "family": "Zed Sans", + "color": "#f1f1f1", + "weight": "bold", + "size": 14, + "margin": { + "right": 8 + } + } + }, + "pending_message": { + "body": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "timestamp": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "padding": { + "bottom": 6 + }, + "sender": { + "family": "Zed Sans", + "color": "#808080", + "weight": "bold", + "size": 14, + "margin": { + "right": 8 + } + } + }, + "input_editor": { + "background": "#000000", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#232323", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + } + }, + "contacts_panel": { + "padding": { + "top": 12, + "left": 12, + "bottom": 12, + "right": 12 + }, + "host_row_height": 28, + "tree_branch_color": "#404040", + "tree_branch_width": 1, + "host_avatar": { + "corner_radius": 10, + "width": 18 + }, + "host_username": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14, + "padding": { + "left": 8 + } + }, + "project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + } + }, + "shared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + }, + "background": "#1c1c1c", + "corner_radius": 6 + }, + "hovered_shared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + }, + "background": "#232323", + "corner_radius": 6 + }, + "unshared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + } + }, + "hovered_unshared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + }, + "corner_radius": 6 + } + }, + "search": { + "match_background": "#3f15a380", + "tab_icon_spacing": 8, + "tab_icon_width": 14, + "active_hovered_option_button": { + "family": "Zed Mono", + "color": "#ffffff", + "size": 14, + "background": "#232323", + "corner_radius": 4, + "border": { + "color": "#404040", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "active_option_button": { + "family": "Zed Mono", + "color": "#ffffff", + "size": 14, + "background": "#232323", + "corner_radius": 4, + "border": { + "color": "#404040", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "editor": { + "background": "#000000", + "corner_radius": 8, + "min_width": 200, + "max_width": 500, + "placeholder_text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#ffffff", + "size": 14 + }, + "border": { + "color": "#232323", + "width": 1 + }, + "margin": { + "right": 6 + }, + "padding": { + "top": 3, + "bottom": 3, + "left": 12, + "right": 8 + } + }, + "hovered_option_button": { + "family": "Zed Mono", + "color": "#ffffff", + "size": 14, + "background": "#0e0e0e", + "corner_radius": 4, + "border": { + "color": "#404040", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "invalid_editor": { + "background": "#000000", + "corner_radius": 8, + "min_width": 200, + "max_width": 500, + "placeholder_text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#ffffff", + "size": 14 + }, + "border": { + "color": "#eb2d2d", + "width": 1 + }, + "margin": { + "right": 6 + }, + "padding": { + "top": 3, + "bottom": 3, + "left": 12, + "right": 8 + } + }, + "match_index": { + "family": "Zed Mono", + "color": "#808080", + "size": 14, + "padding": 6 + }, + "option_button": { + "family": "Zed Mono", + "color": "#9c9c9c", + "size": 14, + "background": "#0e0e0e", + "corner_radius": 4, + "border": { + "color": "#232323", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "option_button_group": { + "padding": { + "left": 4, + "right": 4 + } + }, + "results_status": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 18 + } + }, + "breadcrumbs": { + "family": "Zed Sans", + "color": "#9c9c9c", + "size": 14, + "padding": { + "left": 6 + } + } +} \ No newline at end of file diff --git a/assets/themes/light.json b/assets/themes/light.json new file mode 100644 index 0000000000..5a370f4aa2 --- /dev/null +++ b/assets/themes/light.json @@ -0,0 +1,1338 @@ +{ + "selector": { + "background": "#f8f8f8", + "corner_radius": 8, + "padding": 8, + "item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#484bed", + "weight": "bold", + "size": 14 + } + }, + "active_item": { + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 4 + }, + "corner_radius": 8, + "text": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#484bed", + "weight": "bold", + "size": 14 + }, + "background": "#e3e3e3" + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "empty": { + "text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 12, + "right": 12, + "top": 8 + } + }, + "input_editor": { + "background": "#ffffff", + "corner_radius": 8, + "placeholder_text": { + "family": "Zed Sans", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 16, + "right": 16, + "top": 7 + } + }, + "margin": { + "bottom": 52, + "top": 52 + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + } + }, + "workspace": { + "background": "#f8f8f8", + "leader_border_opacity": 0.7, + "leader_border_width": 2, + "tab": { + "height": 32, + "background": "#f8f8f8", + "icon_close": "#9c9c9c", + "icon_close_active": "#000000", + "icon_conflict": "#f7bf17", + "icon_dirty": "#135acd", + "icon_width": 8, + "spacing": 8, + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "border": { + "color": "#d5d5d5", + "width": 1, + "left": true, + "bottom": true, + "overlay": true + }, + "padding": { + "left": 8, + "right": 8 + } + }, + "active_tab": { + "height": 32, + "background": "#ffffff", + "icon_close": "#9c9c9c", + "icon_close_active": "#000000", + "icon_conflict": "#f7bf17", + "icon_dirty": "#135acd", + "icon_width": 8, + "spacing": 8, + "text": { + "family": "Zed Sans", + "color": "#000000", + "size": 14 + }, + "border": { + "color": "#d5d5d5", + "width": 1, + "left": true, + "bottom": false, + "overlay": true + }, + "padding": { + "left": 8, + "right": 8 + } + }, + "left_sidebar": { + "width": 30, + "background": "#f8f8f8", + "border": { + "color": "#d5d5d5", + "width": 1, + "right": true + }, + "item": { + "height": 32, + "icon_color": "#717171", + "icon_size": 18 + }, + "active_item": { + "height": 32, + "icon_color": "#000000", + "icon_size": 18 + }, + "resize_handle": { + "background": "#d5d5d5", + "padding": { + "left": 1 + } + } + }, + "right_sidebar": { + "width": 30, + "background": "#f8f8f8", + "border": { + "color": "#d5d5d5", + "width": 1, + "left": true + }, + "item": { + "height": 32, + "icon_color": "#717171", + "icon_size": 18 + }, + "active_item": { + "height": 32, + "icon_color": "#000000", + "icon_size": 18 + }, + "resize_handle": { + "background": "#d5d5d5", + "padding": { + "left": 1 + } + } + }, + "pane_divider": { + "color": "#d5d5d5", + "width": 1 + }, + "status_bar": { + "height": 24, + "item_spacing": 8, + "padding": { + "left": 6, + "right": 6 + }, + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true, + "overlay": true + }, + "cursor_position": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "diagnostic_message": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "lsp_message": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "auto_update_progress_message": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "auto_update_done_message": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + } + }, + "titlebar": { + "avatar_width": 18, + "height": 32, + "background": "#eaeaea", + "share_icon_color": "#717171", + "share_icon_active_color": "#484bed", + "title": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + }, + "avatar": { + "corner_radius": 10, + "border": { + "color": "#00000088", + "width": 1 + } + }, + "avatar_ribbon": { + "height": 3, + "width": 12 + }, + "border": { + "color": "#d5d5d5", + "width": 1, + "bottom": true + }, + "sign_in_prompt": { + "family": "Zed Sans", + "color": "#474747", + "size": 12, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "corner_radius": 6, + "margin": { + "top": 1, + "right": 6 + }, + "padding": { + "left": 6, + "right": 6 + } + }, + "hovered_sign_in_prompt": { + "family": "Zed Sans", + "color": "#000000", + "size": 12, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "corner_radius": 6, + "margin": { + "top": 1, + "right": 6 + }, + "padding": { + "left": 6, + "right": 6 + } + }, + "offline_icon": { + "color": "#717171", + "width": 16, + "padding": { + "right": 4 + } + }, + "outdated_warning": { + "family": "Zed Sans", + "color": "#d3a20b", + "size": 13 + } + }, + "toolbar": { + "height": 34, + "background": "#ffffff", + "border": { + "color": "#d5d5d5", + "width": 1, + "bottom": true + }, + "item_spacing": 8, + "padding": { + "left": 16, + "right": 8, + "top": 4, + "bottom": 4 + } + }, + "breadcrumbs": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "padding": { + "left": 6 + } + }, + "disconnected_overlay": { + "family": "Zed Sans", + "color": "#000000", + "size": 14, + "background": "#000000aa" + } + }, + "editor": { + "text_color": "#1c1c1c", + "background": "#ffffff", + "active_line_background": "#0000000f", + "code_actions_indicator": "#9c9c9c", + "diff_background_deleted": "#fcc6c6", + "diff_background_inserted": "#b7f9ce", + "document_highlight_read_background": "#0000000f", + "document_highlight_write_background": "#00000029", + "error_color": "#eb2d2d", + "gutter_background": "#ffffff", + "gutter_padding_factor": 3.5, + "highlighted_line_background": "#0000001f", + "line_number": "#aaaaaa", + "line_number_active": "#000000", + "rename_fade": 0.6, + "unnecessary_code_fade": 0.5, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "guest_selections": [ + { + "cursor": "#12d796", + "selection": "#12d7963d" + }, + { + "cursor": "#de57e8", + "selection": "#de57e83d" + }, + { + "cursor": "#f9812e", + "selection": "#f9812e3d" + }, + { + "cursor": "#b066f8", + "selection": "#b066f83d" + }, + { + "cursor": "#16d6c1", + "selection": "#16d6c13d" + }, + { + "cursor": "#ef59a3", + "selection": "#ef59a33d" + }, + { + "cursor": "#f7bf17", + "selection": "#f7bf173d" + } + ], + "autocomplete": { + "background": "#ffffff", + "corner_radius": 8, + "padding": 4, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "item": { + "corner_radius": 6, + "padding": { + "bottom": 2, + "left": 6, + "right": 6, + "top": 2 + } + }, + "hovered_item": { + "corner_radius": 6, + "padding": { + "bottom": 2, + "left": 6, + "right": 6, + "top": 2 + }, + "background": "#00000008" + }, + "margin": { + "left": -14 + }, + "match_highlight": { + "family": "Zed Mono", + "color": "#484bed", + "size": 14 + }, + "selected_item": { + "corner_radius": 6, + "padding": { + "bottom": 2, + "left": 6, + "right": 6, + "top": 2 + }, + "background": "#0000000f" + } + }, + "diagnostic_header": { + "background": "#f8f8f8", + "icon_width_factor": 1.5, + "text_scale_factor": 0.857, + "border": { + "color": "#d5d5d5", + "width": 1, + "bottom": true, + "top": true + }, + "code": { + "family": "Zed Mono", + "color": "#636363", + "size": 14, + "margin": { + "left": 10 + } + }, + "message": { + "highlight_text": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14, + "weight": "bold" + }, + "text": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + } + } + }, + "diagnostic_path_header": { + "background": "#0000000f", + "text_scale_factor": 0.857, + "filename": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "path": { + "family": "Zed Mono", + "color": "#636363", + "size": 14, + "margin": { + "left": 12 + } + } + }, + "error_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#eb2d2d", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#eb2d2d", + "size": 14, + "weight": "bold" + } + } + }, + "warning_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#d3a20b", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#d3a20b", + "size": 14, + "weight": "bold" + } + } + }, + "information_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14, + "weight": "bold" + } + } + }, + "hint_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#2472f2", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_error_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_hint_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_information_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "weight": "bold" + } + } + }, + "invalid_warning_diagnostic": { + "text_scale_factor": 0.857, + "header": { + "border": { + "color": "#d5d5d5", + "width": 1, + "top": true + } + }, + "message": { + "text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "highlight_text": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "weight": "bold" + } + } + }, + "syntax": { + "keyword": "#1819a1", + "function": "#bb550e", + "string": "#eb2d2d", + "type": "#a8820e", + "number": "#484bed", + "comment": "#717171", + "property": "#106c4e", + "variant": "#97142a", + "constant": "#1c1c1c", + "title": { + "color": "#1096d3", + "weight": "bold" + }, + "emphasis": "#484bed", + "emphasis_strong": { + "color": "#484bed", + "weight": "bold" + }, + "link_uri": { + "color": "#79ba16", + "underline": true + }, + "link_text": { + "color": "#eb2d2d", + "italic": true + }, + "list_marker": "#555555" + } + }, + "project_diagnostics": { + "tab_icon_spacing": 4, + "tab_icon_width": 13, + "tab_summary_spacing": 10, + "empty_message": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 18 + }, + "status_bar_item": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "margin": { + "right": 10 + } + } + }, + "command_palette": { + "keystroke_spacing": 8, + "key": { + "text": { + "family": "Zed Mono", + "color": "#474747", + "size": 12 + }, + "corner_radius": 4, + "background": "#f1f1f1", + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "top": 2, + "bottom": 2, + "left": 8, + "right": 8 + }, + "margin": { + "left": 2 + } + } + }, + "project_panel": { + "padding": { + "top": 6, + "left": 12 + }, + "entry": { + "height": 22, + "icon_color": "#9c9c9c", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + } + }, + "hovered_entry": { + "height": 22, + "background": "#eaeaea", + "icon_color": "#9c9c9c", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#474747", + "size": 14 + } + }, + "selected_entry": { + "height": 22, + "icon_color": "#9c9c9c", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + } + }, + "hovered_selected_entry": { + "height": 22, + "background": "#eaeaea", + "icon_color": "#9c9c9c", + "icon_size": 8, + "icon_spacing": 8, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + } + } + }, + "chat_panel": { + "padding": { + "top": 12, + "left": 12, + "bottom": 12, + "right": 12 + }, + "channel_name": { + "family": "Zed Sans", + "color": "#2b2b2b", + "weight": "bold", + "size": 14 + }, + "channel_name_hash": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "padding": { + "right": 8 + } + }, + "channel_select": { + "header": { + "name": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + }, + "padding": { + "bottom": 4, + "left": 0 + }, + "hash": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "margin": { + "right": 8 + } + }, + "corner_radius": 0 + }, + "item": { + "name": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "margin": { + "right": 8 + } + }, + "corner_radius": 0 + }, + "hovered_item": { + "name": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "margin": { + "right": 8 + } + }, + "background": "#eaeaea", + "corner_radius": 6 + }, + "active_item": { + "name": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "margin": { + "right": 8 + } + }, + "corner_radius": 0 + }, + "hovered_active_item": { + "name": { + "family": "Zed Sans", + "color": "#2b2b2b", + "size": 14 + }, + "padding": 4, + "hash": { + "family": "Zed Sans", + "color": "#636363", + "size": 14, + "margin": { + "right": 8 + } + }, + "background": "#eaeaea", + "corner_radius": 6 + }, + "menu": { + "background": "#ffffff", + "corner_radius": 6, + "padding": 4, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "shadow": { + "blur": 16, + "color": "#0000001f", + "offset": [ + 0, + 2 + ] + } + } + }, + "sign_in_prompt": { + "family": "Zed Sans", + "color": "#474747", + "underline": true, + "size": 14 + }, + "hovered_sign_in_prompt": { + "family": "Zed Sans", + "color": "#2b2b2b", + "underline": true, + "size": 14 + }, + "message": { + "body": { + "family": "Zed Sans", + "color": "#474747", + "size": 14 + }, + "timestamp": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "padding": { + "bottom": 6 + }, + "sender": { + "family": "Zed Sans", + "color": "#2b2b2b", + "weight": "bold", + "size": 14, + "margin": { + "right": 8 + } + } + }, + "pending_message": { + "body": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "timestamp": { + "family": "Zed Sans", + "color": "#636363", + "size": 14 + }, + "padding": { + "bottom": 6 + }, + "sender": { + "family": "Zed Sans", + "color": "#636363", + "weight": "bold", + "size": 14, + "margin": { + "right": 8 + } + } + }, + "input_editor": { + "background": "#ffffff", + "corner_radius": 6, + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "placeholder_text": { + "family": "Zed Mono", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "padding": { + "bottom": 7, + "left": 8, + "right": 8, + "top": 7 + } + } + }, + "contacts_panel": { + "padding": { + "top": 12, + "left": 12, + "bottom": 12, + "right": 12 + }, + "host_row_height": 28, + "tree_branch_color": "#e3e3e3", + "tree_branch_width": 1, + "host_avatar": { + "corner_radius": 10, + "width": 18 + }, + "host_username": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14, + "padding": { + "left": 8 + } + }, + "project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#808080", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + } + }, + "shared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + }, + "background": "#f8f8f8", + "corner_radius": 6 + }, + "hovered_shared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + }, + "background": "#eaeaea", + "corner_radius": 6 + }, + "unshared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#808080", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + } + }, + "hovered_unshared_project": { + "guest_avatar_spacing": 4, + "height": 24, + "guest_avatar": { + "corner_radius": 8, + "width": 14 + }, + "name": { + "family": "Zed Mono", + "color": "#808080", + "size": 14, + "margin": { + "right": 6 + } + }, + "padding": { + "left": 8 + }, + "corner_radius": 6 + } + }, + "search": { + "match_background": "#fce9b7", + "tab_icon_spacing": 8, + "tab_icon_width": 14, + "active_hovered_option_button": { + "family": "Zed Mono", + "color": "#000000", + "size": 14, + "background": "#ffffff", + "corner_radius": 4, + "border": { + "color": "#e3e3e3", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "active_option_button": { + "family": "Zed Mono", + "color": "#000000", + "size": 14, + "background": "#ffffff", + "corner_radius": 4, + "border": { + "color": "#e3e3e3", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "editor": { + "background": "#ffffff", + "corner_radius": 8, + "min_width": 200, + "max_width": 500, + "placeholder_text": { + "family": "Zed Mono", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#000000", + "size": 14 + }, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "margin": { + "right": 6 + }, + "padding": { + "top": 3, + "bottom": 3, + "left": 12, + "right": 8 + } + }, + "hovered_option_button": { + "family": "Zed Mono", + "color": "#000000", + "size": 14, + "background": "#f1f1f1", + "corner_radius": 4, + "border": { + "color": "#e3e3e3", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "invalid_editor": { + "background": "#ffffff", + "corner_radius": 8, + "min_width": 200, + "max_width": 500, + "placeholder_text": { + "family": "Zed Mono", + "color": "#808080", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + }, + "text": { + "family": "Zed Mono", + "color": "#000000", + "size": 14 + }, + "border": { + "color": "#f9a0a0", + "width": 1 + }, + "margin": { + "right": 6 + }, + "padding": { + "top": 3, + "bottom": 3, + "left": 12, + "right": 8 + } + }, + "match_index": { + "family": "Zed Mono", + "color": "#636363", + "size": 14, + "padding": 6 + }, + "option_button": { + "family": "Zed Mono", + "color": "#474747", + "size": 14, + "background": "#f1f1f1", + "corner_radius": 4, + "border": { + "color": "#d5d5d5", + "width": 1 + }, + "margin": { + "left": 2, + "right": 2 + }, + "padding": { + "bottom": 3, + "left": 8, + "right": 8, + "top": 3 + } + }, + "option_button_group": { + "padding": { + "left": 4, + "right": 4 + } + }, + "results_status": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 18 + } + }, + "breadcrumbs": { + "family": "Zed Sans", + "color": "#474747", + "size": 14, + "padding": { + "left": 6 + } + } +} \ No newline at end of file diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 0000000000..71db57f320 --- /dev/null +++ b/crates/assets/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "assets" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/assets.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +anyhow = "1.0.38" +rust-embed = { version = "6.3", features = ["include-exclude"] } + diff --git a/crates/zed/src/assets.rs b/crates/assets/src/assets.rs similarity index 95% rename from crates/zed/src/assets.rs rename to crates/assets/src/assets.rs index c0f3a1fbfc..7d5748e43b 100644 --- a/crates/zed/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -3,7 +3,7 @@ use gpui::AssetSource; use rust_embed::RustEmbed; #[derive(RustEmbed)] -#[folder = "assets"] +#[folder = "../../assets"] #[exclude = "*.DS_Store"] pub struct Assets; diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 39f422ea6f..dcb08186b8 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -8,9 +8,10 @@ path = "src/auto_update.rs" doctest = false [dependencies] -gpui = { path = "../gpui" } -theme = { path = "../theme" } client = { path = "../client" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +theme = { path = "../theme" } workspace = { path = "../workspace" } anyhow = "1.0.38" lazy_static = "1.4" diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 4e70333cad..a40788f4c4 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use client::http::{self, HttpClient}; use gpui::{ - action, + actions, elements::{Empty, MouseEventHandler, Text}, platform::AppVersion, AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, @@ -9,10 +9,11 @@ use gpui::{ }; use lazy_static::lazy_static; use serde::Deserialize; +use settings::Settings; use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; use surf::Request; -use workspace::{ItemHandle, Settings, StatusItemView}; +use workspace::{ItemHandle, StatusItemView}; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); const ACCESS_TOKEN: &'static str = "618033988749894"; @@ -24,6 +25,8 @@ lazy_static! { pub static ref ZED_APP_PATH: Option = 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>, } -action!(DismissErrorMessage); - #[derive(Deserialize)] struct JsonRelease { version: String, @@ -66,16 +67,15 @@ pub fn init(http_client: Arc, 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> { cx.default_global::>>().clone() diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 7dbafdb3be..88fd614a89 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -14,6 +14,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } search = { path = "../search" } +settings = { path = "../settings" } theme = { path = "../theme" } workspace = { path = "../workspace" } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 59c8b08b68..b2bba37e38 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -6,8 +6,9 @@ use gpui::{ use language::{Buffer, OutlineItem}; use project::Project; use search::ProjectSearchView; +use settings::Settings; use theme::SyntaxTheme; -use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView}; +use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView}; pub enum Event { UpdateLocation, diff --git a/crates/chat_panel/Cargo.toml b/crates/chat_panel/Cargo.toml index a64ecc8b7b..95426517d7 100644 --- a/crates/chat_panel/Cargo.toml +++ b/crates/chat_panel/Cargo.toml @@ -11,6 +11,7 @@ doctest = false client = { path = "../client" } editor = { path = "../editor" } gpui = { path = "../gpui" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index a7c9123894..187c0139db 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -4,19 +4,18 @@ use client::{ }; use editor::Editor; use gpui::{ - action, + actions, elements::*, - keymap::Binding, platform::CursorStyle, views::{ItemType, Select, SelectStyle}, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; use postage::prelude::Stream; +use settings::{Settings, SoftWrap}; use std::sync::Arc; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; -use workspace::{settings::SoftWrap, Settings}; const MESSAGE_LOADING_THRESHOLD: usize = 50; @@ -33,14 +32,11 @@ pub struct ChatPanel { pub enum Event {} -action!(Send); -action!(LoadMoreMessages); +actions!(chat_panel, [Send, LoadMoreMessages]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ChatPanel::send); cx.add_action(ChatPanel::load_more_messages); - - cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]); } impl ChatPanel { diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 0000000000..be7cc24b3e --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/cli.rs" +doctest = false + +[[bin]] +name = "cli" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +clap = { version = "3.1", features = ["derive"] } +dirs = "3.0" +ipc-channel = "0.16" +serde = { version = "1.0", features = ["derive"] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.9" +core-services = "0.2" +plist = "1.3" diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs new file mode 100644 index 0000000000..7cad42b534 --- /dev/null +++ b/crates/cli/src/cli.rs @@ -0,0 +1,22 @@ +pub use ipc_channel::ipc; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize)] +pub struct IpcHandshake { + pub requests: ipc::IpcSender, + pub responses: ipc::IpcReceiver, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum CliRequest { + Open { paths: Vec, wait: bool }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum CliResponse { + Ping, + Stdout { message: String }, + Stderr { message: String }, + Exit { status: i32 }, +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 0000000000..4bc2d6e73d --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,124 @@ +use anyhow::{anyhow, Result}; +use clap::Parser; +use cli::{CliRequest, CliResponse, IpcHandshake}; +use core_foundation::{ + array::{CFArray, CFIndex}, + string::kCFStringEncodingUTF8, + url::{CFURLCreateWithBytes, CFURL}, +}; +use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType}; +use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender}; +use serde::Deserialize; +use std::{ffi::OsStr, fs, path::PathBuf, ptr}; + +#[derive(Parser)] +#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] +struct Args { + /// Wait for all of the given paths to be closed before exiting. + #[clap(short, long)] + wait: bool, + /// A sequence of space-separated paths that you want to open. + #[clap()] + paths: Vec, + /// Print Zed's version and the app path. + #[clap(short, long)] + version: bool, + /// Custom Zed.app path + #[clap(short, long)] + bundle_path: Option, +} + +#[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::>>()?, + 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 { + 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, IpcReceiver)> { + let (server, server_name) = IpcOneShotServer::::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)) + } +} diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index c60b82f1f8..211dc7a04b 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -21,7 +21,7 @@ async-tungstenite = { version = "0.16", features = ["async-tls"] } futures = "0.3" image = "0.23" lazy_static = "1.4.0" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f0bc41008c..93cbba48a1 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -13,7 +13,7 @@ use async_tungstenite::tungstenite::{ }; use futures::{future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{ - action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext, + actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; use http::HttpClient; @@ -50,7 +50,7 @@ lazy_static! { .and_then(|s| if s.is_empty() { None } else { Some(s) }); } -action!(Authenticate); +actions!(client, [Authenticate]); pub fn init(rpc: Arc, cx: &mut MutableAppContext) { cx.add_global_action(move |_: &Authenticate, cx| { diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 35a8e85922..5417f2b51d 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -6,7 +6,6 @@ use anyhow::{anyhow, Result}; use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt}; use gpui::{executor, ModelHandle, TestAppContext}; use parking_lot::Mutex; -use postage::barrier; use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope}; use std::{fmt, rc::Rc, sync::Arc}; @@ -23,7 +22,6 @@ struct FakeServerState { connection_id: Option, forbid_connections: bool, auth_count: usize, - connection_killer: Option, 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) }) } diff --git a/crates/server/.env.template.toml b/crates/collab/.env.template.toml similarity index 100% rename from crates/server/.env.template.toml rename to crates/collab/.env.template.toml diff --git a/crates/server/Cargo.toml b/crates/collab/Cargo.toml similarity index 87% rename from crates/server/Cargo.toml rename to crates/collab/Cargo.toml index c39fb2f10b..34047406a7 100644 --- a/crates/server/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -1,12 +1,12 @@ [package] authors = ["Nathan Sobo "] -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" diff --git a/crates/server/Procfile b/crates/collab/Procfile similarity index 56% rename from crates/server/Procfile rename to crates/collab/Procfile index 74cb9a094b..ef8914fcc0 100644 --- a/crates/server/Procfile +++ b/crates/collab/Procfile @@ -1,2 +1,2 @@ -web: ./target/release/zed-server +collab: ./target/release/collab release: ./target/release/sqlx migrate run diff --git a/crates/server/README.md b/crates/collab/README.md similarity index 100% rename from crates/server/README.md rename to crates/collab/README.md diff --git a/crates/server/basic.conf b/crates/collab/basic.conf similarity index 100% rename from crates/server/basic.conf rename to crates/collab/basic.conf diff --git a/crates/server/favicon.ico b/crates/collab/favicon.ico similarity index 100% rename from crates/server/favicon.ico rename to crates/collab/favicon.ico diff --git a/crates/server/k8s/environments/production.sh b/crates/collab/k8s/environments/production.sh similarity index 100% rename from crates/server/k8s/environments/production.sh rename to crates/collab/k8s/environments/production.sh diff --git a/crates/server/k8s/environments/staging.sh b/crates/collab/k8s/environments/staging.sh similarity index 100% rename from crates/server/k8s/environments/staging.sh rename to crates/collab/k8s/environments/staging.sh diff --git a/crates/server/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml similarity index 89% rename from crates/server/k8s/manifest.template.yml rename to crates/collab/k8s/manifest.template.yml index e1c33d4e49..f243a0adac 100644 --- a/crates/server/k8s/manifest.template.yml +++ b/crates/collab/k8s/manifest.template.yml @@ -8,14 +8,14 @@ kind: Service apiVersion: v1 metadata: namespace: ${ZED_KUBE_NAMESPACE} - name: zed + name: collab annotations: service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" - service.beta.kubernetes.io/do-loadbalancer-certificate-id: "2634d353-1ab4-437f-add2-4ffd8f315233" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: "40879815-9a6b-4bbb-8207-8f2c7c0218f9" spec: type: LoadBalancer selector: - app: zed + app: collab ports: - name: web protocol: TCP @@ -26,19 +26,19 @@ apiVersion: apps/v1 kind: Deployment metadata: namespace: ${ZED_KUBE_NAMESPACE} - name: zed + name: collab spec: replicas: 1 selector: matchLabels: - app: zed + app: collab template: metadata: labels: - app: zed + app: collab spec: containers: - - name: zed + - name: collab image: "${ZED_IMAGE_ID}" ports: - containerPort: 8080 @@ -81,6 +81,10 @@ spec: secretKeyRef: name: api key: token + - name: LOG_JSON + value: "1" + - name: RUST_LOG + value: "trace" securityContext: capabilities: # FIXME - Switch to the more restrictive `PERFMON` capability. diff --git a/crates/server/k8s/migrate.template.yml b/crates/collab/k8s/migrate.template.yml similarity index 100% rename from crates/server/k8s/migrate.template.yml rename to crates/collab/k8s/migrate.template.yml diff --git a/crates/server/migrations/20210527024318_initial_schema.sql b/crates/collab/migrations/20210527024318_initial_schema.sql similarity index 100% rename from crates/server/migrations/20210527024318_initial_schema.sql rename to crates/collab/migrations/20210527024318_initial_schema.sql diff --git a/crates/server/migrations/20210607190313_create_access_tokens.sql b/crates/collab/migrations/20210607190313_create_access_tokens.sql similarity index 100% rename from crates/server/migrations/20210607190313_create_access_tokens.sql rename to crates/collab/migrations/20210607190313_create_access_tokens.sql diff --git a/crates/server/migrations/20210805175147_create_chat_tables.sql b/crates/collab/migrations/20210805175147_create_chat_tables.sql similarity index 100% rename from crates/server/migrations/20210805175147_create_chat_tables.sql rename to crates/collab/migrations/20210805175147_create_chat_tables.sql diff --git a/crates/server/migrations/20210916123647_add_nonce_to_channel_messages.sql b/crates/collab/migrations/20210916123647_add_nonce_to_channel_messages.sql similarity index 100% rename from crates/server/migrations/20210916123647_add_nonce_to_channel_messages.sql rename to crates/collab/migrations/20210916123647_add_nonce_to_channel_messages.sql diff --git a/crates/server/migrations/20210920192001_add_interests_to_signups.sql b/crates/collab/migrations/20210920192001_add_interests_to_signups.sql similarity index 100% rename from crates/server/migrations/20210920192001_add_interests_to_signups.sql rename to crates/collab/migrations/20210920192001_add_interests_to_signups.sql diff --git a/crates/server/src/admin.rs b/crates/collab/src/admin.rs similarity index 100% rename from crates/server/src/admin.rs rename to crates/collab/src/admin.rs diff --git a/crates/server/src/api.rs b/crates/collab/src/api.rs similarity index 99% rename from crates/server/src/api.rs rename to crates/collab/src/api.rs index 69b60fe9ec..c909650f26 100644 --- a/crates/server/src/api.rs +++ b/crates/collab/src/api.rs @@ -111,7 +111,6 @@ async fn create_access_token(request: Request) -> tide::Result { .get_user_by_github_login(request.param("github_login")?) .await? .ok_or_else(|| surf::Error::from_str(StatusCode::NotFound, "user not found"))?; - let access_token = auth::create_access_token(request.db().as_ref(), user.id).await?; #[derive(Deserialize)] struct QueryParams { @@ -123,9 +122,6 @@ async fn create_access_token(request: Request) -> tide::Result { surf::Error::from_str(StatusCode::UnprocessableEntity, "invalid query params") })?; - let encrypted_access_token = - auth::encrypt_access_token(&access_token, query_params.public_key.clone())?; - let mut user_id = user.id; if let Some(impersonate) = query_params.impersonate { if user.admin { @@ -151,6 +147,10 @@ async fn create_access_token(request: Request) -> tide::Result { } } + let access_token = auth::create_access_token(request.db().as_ref(), user_id).await?; + let encrypted_access_token = + auth::encrypt_access_token(&access_token, query_params.public_key.clone())?; + Ok(tide::Response::builder(StatusCode::Ok) .body(json!({"user_id": user_id, "encrypted_access_token": encrypted_access_token})) .build()) diff --git a/crates/server/src/assets.rs b/crates/collab/src/assets.rs similarity index 100% rename from crates/server/src/assets.rs rename to crates/collab/src/assets.rs diff --git a/crates/server/src/auth.rs b/crates/collab/src/auth.rs similarity index 100% rename from crates/server/src/auth.rs rename to crates/collab/src/auth.rs diff --git a/crates/server/src/bin/dotenv.rs b/crates/collab/src/bin/dotenv.rs similarity index 100% rename from crates/server/src/bin/dotenv.rs rename to crates/collab/src/bin/dotenv.rs diff --git a/crates/server/src/bin/seed.rs b/crates/collab/src/bin/seed.rs similarity index 100% rename from crates/server/src/bin/seed.rs rename to crates/collab/src/bin/seed.rs diff --git a/crates/server/src/careers.rs b/crates/collab/src/careers.rs similarity index 100% rename from crates/server/src/careers.rs rename to crates/collab/src/careers.rs diff --git a/crates/server/src/community.rs b/crates/collab/src/community.rs similarity index 100% rename from crates/server/src/community.rs rename to crates/collab/src/community.rs diff --git a/crates/server/src/db.rs b/crates/collab/src/db.rs similarity index 100% rename from crates/server/src/db.rs rename to crates/collab/src/db.rs diff --git a/crates/server/src/env.rs b/crates/collab/src/env.rs similarity index 100% rename from crates/server/src/env.rs rename to crates/collab/src/env.rs diff --git a/crates/server/src/errors.rs b/crates/collab/src/errors.rs similarity index 100% rename from crates/server/src/errors.rs rename to crates/collab/src/errors.rs diff --git a/crates/server/src/expiring.rs b/crates/collab/src/expiring.rs similarity index 100% rename from crates/server/src/expiring.rs rename to crates/collab/src/expiring.rs diff --git a/crates/server/src/github.rs b/crates/collab/src/github.rs similarity index 100% rename from crates/server/src/github.rs rename to crates/collab/src/github.rs diff --git a/crates/server/src/home.rs b/crates/collab/src/home.rs similarity index 91% rename from crates/server/src/home.rs rename to crates/collab/src/home.rs index 69bee449a8..bddeadc352 100644 --- a/crates/server/src/home.rs +++ b/crates/collab/src/home.rs @@ -1,7 +1,8 @@ use crate::{AppState, Request, RequestExt as _}; -use serde::Deserialize; +use log::as_serde; +use serde::{Deserialize, Serialize}; use std::sync::Arc; -use tide::{http::mime, log, Server}; +use tide::{http::mime, Server}; pub fn add_routes(app: &mut Server>) { 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 diff --git a/crates/server/src/main.rs b/crates/collab/src/main.rs similarity index 97% rename from crates/server/src/main.rs rename to crates/collab/src/main.rs index 47c8c82190..97de59f36b 100644 --- a/crates/server/src/main.rs +++ b/crates/collab/src/main.rs @@ -27,7 +27,7 @@ use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; use std::sync::Arc; use surf::http::cookies::SameSite; -use tide::{log, sessions::SessionMiddleware}; +use tide::sessions::SessionMiddleware; use tide_compress::CompressMiddleware; type Request = tide::Request>; @@ -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!( diff --git a/crates/server/src/releases.rs b/crates/collab/src/releases.rs similarity index 100% rename from crates/server/src/releases.rs rename to crates/collab/src/releases.rs diff --git a/crates/server/src/rpc.rs b/crates/collab/src/rpc.rs similarity index 83% rename from crates/server/src/rpc.rs rename to crates/collab/src/rpc.rs index 768432ef75..ba93be570c 100644 --- a/crates/server/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -7,11 +7,14 @@ use super::{ }; use anyhow::anyhow; use async_io::Timer; -use async_std::task; +use async_std::{ + sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, + task, +}; use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream}; use collections::{HashMap, HashSet}; use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt}; -use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use log::{as_debug, as_display}; use rpc::{ proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, Connection, ConnectionId, Peer, TypedEnvelope, @@ -20,17 +23,20 @@ use sha1::{Digest as _, Sha1}; use std::{ any::TypeId, future::Future, + marker::PhantomData, + ops::{Deref, DerefMut}, + rc::Rc, sync::Arc, time::{Duration, Instant}, }; use store::{Store, Worktree}; use surf::StatusCode; -use tide::log; use tide::{ http::headers::{HeaderName, CONNECTION, UPGRADE}, Request, Response, }; use time::OffsetDateTime; +use util::ResultExt; type MessageHandler = Box< dyn Send @@ -58,6 +64,16 @@ pub struct RealExecutor; const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; +struct StoreReadGuard<'a> { + guard: RwLockReadGuard<'a, Store>, + _not_send: PhantomData>, +} + +struct StoreWriteGuard<'a> { + guard: RwLockWriteGuard<'a, Store>, + _not_send: PhantomData>, +} + impl Server { pub fn new( app_state: Arc, @@ -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(&mut self, handler: F) -> &mut Self + where + F: 'static + + Send + + Sync + + Fn(Arc, &mut Store, TypedEnvelope) -> tide::Result, + 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( self: &Arc, 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, 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, + self: Arc, request: TypedEnvelope, ) -> tide::Result { 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, + self: Arc, request: TypedEnvelope, ) -> 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, + self: Arc, request: TypedEnvelope, ) -> tide::Result { - 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, + self: Arc, request: TypedEnvelope, ) -> 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, + fn join_project( + self: Arc, + state: &mut Store, request: TypedEnvelope, ) -> tide::Result { 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, + self: Arc, request: TypedEnvelope, ) -> 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, + self: Arc, request: TypedEnvelope, ) -> tide::Result { - 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::>(); - 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, + self: Arc, request: TypedEnvelope, ) -> 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, + self: Arc, request: TypedEnvelope, ) -> tide::Result { - 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, + self: Arc, request: TypedEnvelope, ) -> 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, + self: Arc, request: TypedEnvelope, ) -> 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, request: TypedEnvelope, ) -> tide::Result { - 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 { 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, request: TypedEnvelope, ) -> 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 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, + self: &Arc, + state: &Store, user_ids: impl IntoIterator, - ) -> 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: Arc, request: TypedEnvelope, ) -> 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 @@ -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: Arc, request: TypedEnvelope, ) -> 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, request: TypedEnvelope, ) -> 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 @@ -956,12 +1028,57 @@ impl Server { }) } - fn state<'a>(self: &'a Arc) -> RwLockReadGuard<'a, Store> { - self.store.read() + async fn state<'a>(self: &'a Arc) -> 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, + } } - fn state_mut<'a>(self: &'a mut Arc) -> RwLockWriteGuard<'a, Store> { - self.store.write() + async fn state_mut<'a>(self: &'a Arc) -> 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 + } +} + +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( - sender_id: ConnectionId, - receiver_ids: Vec, - mut f: F, -) -> anyhow::Result<()> +fn broadcast(sender_id: ConnectionId, receiver_ids: Vec, 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>, rpc: &Arc) { @@ -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::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::>() == &[] + }) + .await; + project_b + .condition(cx_b, |project, cx| { + project.diagnostic_summaries(cx).collect::>() == &[] + }) + .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::( @@ -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(), ) }) @@ -4526,7 +4680,9 @@ mod tests { editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx)); workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(&client_a_id.into(), cx).unwrap() + workspace + .toggle_follow(&ToggleFollow(client_a_id), cx) + .unwrap() }) .await .unwrap(); @@ -4561,6 +4717,29 @@ mod tests { }) .await; + // When client A navigates back and forth, client B does so as well. + workspace_a + .update(cx_a, |workspace, cx| { + workspace::Pane::go_back(workspace, None, cx) + }) + .await; + workspace_b + .condition(cx_b, |workspace, cx| { + workspace.active_item(cx).unwrap().id() == editor_b2.id() + }) + .await; + + workspace_a + .update(cx_a, |workspace, cx| { + workspace::Pane::go_forward(workspace, None, cx) + }) + .await; + workspace_b + .condition(cx_b, |workspace, cx| { + workspace.active_item(cx).unwrap().id() == editor_b1.id() + }) + .await; + // Changes to client A's editor are reflected on client B. editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([1..1, 2..2], None, cx); @@ -4603,7 +4782,9 @@ mod tests { // Client A starts following client B. workspace_a .update(cx_a, |workspace, cx| { - workspace.toggle_follow(&client_b_id.into(), cx).unwrap() + workspace + .toggle_follow(&ToggleFollow(client_b_id), cx) + .unwrap() }) .await .unwrap(); @@ -4832,7 +5013,9 @@ mod tests { }); workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(&leader_id.into(), cx).unwrap() + workspace + .toggle_follow(&ToggleFollow(leader_id), cx) + .unwrap() }) .await .unwrap(); @@ -4857,7 +5040,9 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(&leader_id.into(), cx).unwrap() + workspace + .toggle_follow(&ToggleFollow(leader_id), cx) + .unwrap() }) .await .unwrap(); @@ -4875,7 +5060,9 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(&leader_id.into(), cx).unwrap() + workspace + .toggle_follow(&ToggleFollow(leader_id), cx) + .unwrap() }) .await .unwrap(); @@ -4895,7 +5082,9 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(&leader_id.into(), cx).unwrap() + workspace + .toggle_follow(&ToggleFollow(leader_id), cx) + .unwrap() }) .await .unwrap(); @@ -4933,11 +5122,17 @@ mod tests { } #[gpui::test(iterations = 100)] - async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { + async fn test_random_collaboration( + cx: &mut TestAppContext, + deterministic: Arc, + rng: StdRng, + ) { cx.foreground().forbid_parking(); let max_peers = env::var("MAX_PEERS") .map(|i| i.parse().expect("invalid `MAX_PEERS` variable")) .unwrap_or(5); + assert!(max_peers <= 5); + let max_operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); @@ -4951,22 +5146,23 @@ mod tests { fs.insert_tree( "/_collab", json!({ - ".zed.toml": r#"collaborators = ["guest-1", "guest-2", "guest-3", "guest-4", "guest-5"]"# + ".zed.toml": r#"collaborators = ["guest-1", "guest-2", "guest-3", "guest-4"]"# }), ) .await; - let operations = Rc::new(Cell::new(0)); let mut server = TestServer::start(cx.foreground(), cx.background()).await; let mut clients = Vec::new(); + let mut user_ids = Vec::new(); + let mut op_start_signals = Vec::new(); let files = Arc::new(Mutex::new(Vec::new())); let mut next_entity_id = 100000; let mut host_cx = TestAppContext::new( cx.foreground_platform(), cx.platform(), - cx.foreground(), - cx.background(), + deterministic.build_foreground(next_entity_id), + deterministic.build_background(), cx.font_cache(), cx.leak_detector(), next_entity_id, @@ -5126,64 +5322,176 @@ mod tests { }); host_language_registry.add(Arc::new(language)); - clients.push(cx.foreground().spawn(host.simulate_host( + let op_start_signal = futures::channel::mpsc::unbounded(); + user_ids.push(host.current_user_id(&host_cx)); + op_start_signals.push(op_start_signal.0); + clients.push(host_cx.foreground().spawn(host.simulate_host( host_project, files, - operations.clone(), - max_operations, + op_start_signal.1, rng.clone(), host_cx, ))); - while operations.get() < max_operations { - cx.background().simulate_random_delay().await; - if clients.len() >= max_peers { - break; - } else if rng.lock().gen_bool(0.05) { - operations.set(operations.get() + 1); + let disconnect_host_at = if rng.lock().gen_bool(0.2) { + rng.lock().gen_range(0..max_operations) + } else { + max_operations + }; + let mut available_guests = vec![ + "guest-1".to_string(), + "guest-2".to_string(), + "guest-3".to_string(), + "guest-4".to_string(), + ]; + let mut operations = 0; + while operations < max_operations { + if operations == disconnect_host_at { + server.disconnect_client(user_ids[0]); + cx.foreground().advance_clock(RECEIVE_TIMEOUT); + drop(op_start_signals); + let mut clients = futures::future::join_all(clients).await; + cx.foreground().run_until_parked(); - let guest_id = clients.len(); - log::info!("Adding guest {}", guest_id); - next_entity_id += 100000; - let mut guest_cx = TestAppContext::new( - cx.foreground_platform(), - cx.platform(), - cx.foreground(), - cx.background(), - cx.font_cache(), - cx.leak_detector(), - next_entity_id, - ); - let guest = server - .create_client(&mut guest_cx, &format!("guest-{}", guest_id)) - .await; - let guest_project = Project::remote( - host_project_id, - guest.client.clone(), - guest.user_store.clone(), - guest_lang_registry.clone(), - FakeFs::new(cx.background()), - &mut guest_cx.to_async(), - ) - .await - .unwrap(); - clients.push(cx.foreground().spawn(guest.simulate_guest( - guest_id, - guest_project, - operations.clone(), - max_operations, - rng.clone(), - guest_cx, - ))); + let (host, mut host_cx, host_err) = clients.remove(0); + if let Some(host_err) = host_err { + log::error!("host error - {}", host_err); + } + host.project + .as_ref() + .unwrap() + .read_with(&host_cx, |project, _| assert!(!project.is_shared())); + for (guest, mut guest_cx, guest_err) in clients { + if let Some(guest_err) = guest_err { + log::error!("{} error - {}", guest.username, guest_err); + } + let contacts = server + .store + .read() + .await + .contacts_for_user(guest.current_user_id(&guest_cx)); + assert!(!contacts + .iter() + .flat_map(|contact| &contact.projects) + .any(|project| project.id == host_project_id)); + guest + .project + .as_ref() + .unwrap() + .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); + guest_cx.update(|_| drop(guest)); + } + host_cx.update(|_| drop(host)); - log::info!("Guest {} added", guest_id); + return; + } + + let distribution = rng.lock().gen_range(0..100); + match distribution { + 0..=19 if !available_guests.is_empty() => { + let guest_ix = rng.lock().gen_range(0..available_guests.len()); + let guest_username = available_guests.remove(guest_ix); + log::info!("Adding new connection for {}", guest_username); + next_entity_id += 100000; + let mut guest_cx = TestAppContext::new( + cx.foreground_platform(), + cx.platform(), + deterministic.build_foreground(next_entity_id), + deterministic.build_background(), + cx.font_cache(), + cx.leak_detector(), + next_entity_id, + ); + let guest = server.create_client(&mut guest_cx, &guest_username).await; + let guest_project = Project::remote( + host_project_id, + guest.client.clone(), + guest.user_store.clone(), + guest_lang_registry.clone(), + FakeFs::new(cx.background()), + &mut guest_cx.to_async(), + ) + .await + .unwrap(); + let op_start_signal = futures::channel::mpsc::unbounded(); + user_ids.push(guest.current_user_id(&guest_cx)); + op_start_signals.push(op_start_signal.0); + clients.push(guest_cx.foreground().spawn(guest.simulate_guest( + guest_username.clone(), + guest_project, + op_start_signal.1, + rng.clone(), + guest_cx, + ))); + + log::info!("Added connection for {}", guest_username); + operations += 1; + } + 20..=29 if clients.len() > 1 => { + log::info!("Removing guest"); + let guest_ix = rng.lock().gen_range(1..clients.len()); + let removed_guest_id = user_ids.remove(guest_ix); + let guest = clients.remove(guest_ix); + op_start_signals.remove(guest_ix); + server.disconnect_client(removed_guest_id); + cx.foreground().advance_clock(RECEIVE_TIMEOUT); + let (guest, mut guest_cx, guest_err) = guest.await; + if let Some(guest_err) = guest_err { + log::error!("{} error - {}", guest.username, guest_err); + } + guest + .project + .as_ref() + .unwrap() + .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); + for user_id in &user_ids { + for contact in server.store.read().await.contacts_for_user(*user_id) { + assert_ne!( + contact.user_id, removed_guest_id.0 as u64, + "removed guest is still a contact of another peer" + ); + for project in contact.projects { + for project_guest_id in project.guests { + assert_ne!( + project_guest_id, removed_guest_id.0 as u64, + "removed guest appears as still participating on a project" + ); + } + } + } + } + + log::info!("{} removed", guest.username); + available_guests.push(guest.username.clone()); + guest_cx.update(|_| drop(guest)); + + operations += 1; + } + _ => { + while operations < max_operations && rng.lock().gen_bool(0.7) { + op_start_signals + .choose(&mut *rng.lock()) + .unwrap() + .unbounded_send(()) + .unwrap(); + operations += 1; + } + + if rng.lock().gen_bool(0.8) { + cx.foreground().run_until_parked(); + } + } } } + drop(op_start_signals); let mut clients = futures::future::join_all(clients).await; cx.foreground().run_until_parked(); - let (host_client, mut host_cx) = clients.remove(0); + let (host_client, mut host_cx, host_err) = clients.remove(0); + if let Some(host_err) = host_err { + panic!("host error - {}", host_err); + } let host_project = host_client.project.as_ref().unwrap(); let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| { project @@ -5201,8 +5509,10 @@ mod tests { .unwrap() .read_with(&host_cx, |project, cx| project.check_invariants(cx)); - for (guest_client, mut guest_cx) in clients.into_iter() { - let guest_id = guest_client.client.id(); + for (guest_client, mut guest_cx, guest_err) in clients.into_iter() { + if let Some(guest_err) = guest_err { + panic!("{} error - {}", guest_client.username, guest_err); + } let worktree_snapshots = guest_client .project @@ -5221,23 +5531,23 @@ mod tests { assert_eq!( worktree_snapshots.keys().collect::>(), host_worktree_snapshots.keys().collect::>(), - "guest {} has different worktrees than the host", - guest_id + "{} has different worktrees than the host", + guest_client.username ); for (id, host_snapshot) in &host_worktree_snapshots { let guest_snapshot = &worktree_snapshots[id]; assert_eq!( guest_snapshot.root_name(), host_snapshot.root_name(), - "guest {} has different root name than the host for worktree {}", - guest_id, + "{} has different root name than the host for worktree {}", + guest_client.username, id ); assert_eq!( guest_snapshot.entries(false).collect::>(), host_snapshot.entries(false).collect::>(), - "guest {} has different snapshot than the host for worktree {}", - guest_id, + "{} has different snapshot than the host for worktree {}", + guest_client.username, id ); } @@ -5253,7 +5563,7 @@ mod tests { let host_buffer = host_project.read_with(&host_cx, |project, cx| { project.buffer_for_id(buffer_id, cx).expect(&format!( "host does not have buffer for guest:{}, peer:{}, id:{}", - guest_id, guest_client.peer_id, buffer_id + guest_client.username, guest_client.peer_id, buffer_id )) }); let path = host_buffer @@ -5262,16 +5572,16 @@ mod tests { assert_eq!( guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()), 0, - "guest {}, buffer {}, path {:?} has deferred operations", - guest_id, + "{}, buffer {}, path {:?} has deferred operations", + guest_client.username, buffer_id, path, ); assert_eq!( guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()), host_buffer.read_with(&host_cx, |buffer, _| buffer.text()), - "guest {}, buffer {}, path {:?}, differs from the host's buffer", - guest_id, + "{}, buffer {}, path {:?}, differs from the host's buffer", + guest_client.username, buffer_id, path ); @@ -5289,7 +5599,7 @@ mod tests { server: Arc, foreground: Rc, notifications: mpsc::UnboundedReceiver<()>, - connection_killers: Arc>>, + connection_killers: Arc>>>, forbid_connections: Arc, _test_db: TestDb, } @@ -5357,9 +5667,9 @@ mod tests { "server is forbidding connections" ))) } else { - let (client_conn, server_conn, kill_conn) = + let (client_conn, server_conn, killed) = Connection::in_memory(cx.background()); - connection_killers.lock().insert(user_id, kill_conn); + connection_killers.lock().insert(user_id, killed); cx.background() .spawn(server.handle_connection( server_conn, @@ -5391,6 +5701,7 @@ mod tests { let client = TestClient { client, peer_id, + username: name.to_string(), user_store, language_registry: Arc::new(LanguageRegistry::test()), project: Default::default(), @@ -5401,7 +5712,11 @@ mod tests { } fn disconnect_client(&self, user_id: UserId) { - self.connection_killers.lock().remove(&user_id); + self.connection_killers + .lock() + .remove(&user_id) + .unwrap() + .store(true, SeqCst); } fn forbid_connections(&self) { @@ -5428,7 +5743,7 @@ mod tests { } async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> { - self.server.store.read() + self.server.store.read().await } async fn condition(&mut self, mut predicate: F) @@ -5436,7 +5751,7 @@ mod tests { F: FnMut(&Store) -> bool, { async_std::future::timeout(Duration::from_millis(500), async { - while !(predicate)(&*self.server.store.read()) { + while !(predicate)(&*self.server.store.read().await) { self.foreground.start_waiting(); self.notifications.next().await; self.foreground.finish_waiting(); @@ -5447,6 +5762,14 @@ mod tests { } } + impl Deref for TestServer { + type Target = Server; + + fn deref(&self) -> &Self::Target { + &self.server + } + } + impl Drop for TestServer { fn drop(&mut self) { self.peer.reset(); @@ -5455,6 +5778,7 @@ mod tests { struct TestClient { client: Arc, + username: String, pub peer_id: PeerId, pub user_store: ModelHandle, language_registry: Arc, @@ -5549,6 +5873,7 @@ mod tests { project: project.clone(), user_store: self.user_store.clone(), languages: self.language_registry.clone(), + themes: ThemeRegistry::new((), cx.font_cache().clone()), channel_list: cx.add_model(|cx| { ChannelList::new(self.user_store.clone(), self.client.clone(), cx) }), @@ -5563,374 +5888,431 @@ mod tests { mut self, project: ModelHandle, files: Arc>>, - operations: Rc>, - max_operations: usize, + op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, - ) -> (Self, TestAppContext) { - let fs = project.read_with(&cx, |project, _| project.fs().clone()); - while operations.get() < max_operations { - operations.set(operations.get() + 1); + ) -> (Self, TestAppContext, Option) { + async fn simulate_host_internal( + client: &mut TestClient, + project: ModelHandle, + files: Arc>>, + mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, + rng: Arc>, + cx: &mut TestAppContext, + ) -> anyhow::Result<()> { + let fs = project.read_with(cx, |project, _| project.fs().clone()); - let distribution = rng.lock().gen_range::(0..100); - match distribution { - 0..=20 if !files.lock().is_empty() => { - let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let mut path = path.as_path(); - while let Some(parent_path) = path.parent() { - path = parent_path; + while op_start_signal.next().await.is_some() { + let distribution = rng.lock().gen_range::(0..100); + match distribution { + 0..=20 if !files.lock().is_empty() => { + let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let mut path = path.as_path(); + while let Some(parent_path) = path.parent() { + path = parent_path; + if rng.lock().gen() { + break; + } + } + + log::info!("Host: find/create local worktree {:?}", path); + let find_or_create_worktree = project.update(cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }); if rng.lock().gen() { - break; + cx.background().spawn(find_or_create_worktree).detach(); + } else { + find_or_create_worktree.await?; } } - - log::info!("Host: find/create local worktree {:?}", path); - let find_or_create_worktree = project.update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) - }); - let find_or_create_worktree = async move { - find_or_create_worktree.await.unwrap(); - }; - if rng.lock().gen() { - cx.background().spawn(find_or_create_worktree).detach(); - } else { - find_or_create_worktree.await; - } - } - 10..=80 if !files.lock().is_empty() => { - let buffer = if self.buffers.is_empty() || rng.lock().gen() { - let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let (worktree, path) = project - .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(file.clone(), true, cx) - }) - .await - .unwrap(); - let project_path = - worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); - log::info!( - "Host: opening path {:?}, worktree {}, relative_path {:?}", - file, - project_path.0, - project_path.1 - ); - let buffer = project - .update(&mut cx, |project, cx| { - project.open_buffer(project_path, cx) - }) - .await - .unwrap(); - self.buffers.insert(buffer.clone()); - buffer - } else { - self.buffers - .iter() - .choose(&mut *rng.lock()) - .unwrap() - .clone() - }; - - if rng.lock().gen_bool(0.1) { - cx.update(|cx| { + 10..=80 if !files.lock().is_empty() => { + let buffer = if client.buffers.is_empty() || rng.lock().gen() { + let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let (worktree, path) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree( + file.clone(), + true, + cx, + ) + }) + .await?; + let project_path = + worktree.read_with(cx, |worktree, _| (worktree.id(), path)); log::info!( - "Host: dropping buffer {:?}", - buffer.read(cx).file().unwrap().full_path(cx) + "Host: opening path {:?}, worktree {}, relative_path {:?}", + file, + project_path.0, + project_path.1 ); - self.buffers.remove(&buffer); - drop(buffer); - }); - } else { - buffer.update(&mut cx, |buffer, cx| { - log::info!( - "Host: updating buffer {:?} ({})", - buffer.file().unwrap().full_path(cx), - buffer.remote_id() - ); - buffer.randomly_edit(&mut *rng.lock(), 5, cx) - }); + let buffer = project + .update(cx, |project, cx| project.open_buffer(project_path, cx)) + .await + .unwrap(); + client.buffers.insert(buffer.clone()); + buffer + } else { + client + .buffers + .iter() + .choose(&mut *rng.lock()) + .unwrap() + .clone() + }; + + if rng.lock().gen_bool(0.1) { + cx.update(|cx| { + log::info!( + "Host: dropping buffer {:?}", + buffer.read(cx).file().unwrap().full_path(cx) + ); + client.buffers.remove(&buffer); + drop(buffer); + }); + } else { + buffer.update(cx, |buffer, cx| { + log::info!( + "Host: updating buffer {:?} ({})", + buffer.file().unwrap().full_path(cx), + buffer.remote_id() + ); + + if rng.lock().gen_bool(0.7) { + buffer.randomly_edit(&mut *rng.lock(), 5, cx); + } else { + buffer.randomly_undo_redo(&mut *rng.lock(), cx); + } + }); + } } + _ => loop { + let path_component_count = rng.lock().gen_range::(1..=5); + let mut path = PathBuf::new(); + path.push("/"); + for _ in 0..path_component_count { + let letter = rng.lock().gen_range(b'a'..=b'z'); + path.push(std::str::from_utf8(&[letter]).unwrap()); + } + path.set_extension("rs"); + let parent_path = path.parent().unwrap(); + + log::info!("Host: creating file {:?}", path,); + + if fs.create_dir(&parent_path).await.is_ok() + && fs.create_file(&path, Default::default()).await.is_ok() + { + files.lock().push(path); + break; + } else { + log::info!("Host: cannot create file"); + } + }, } - _ => loop { - let path_component_count = rng.lock().gen_range::(1..=5); - let mut path = PathBuf::new(); - path.push("/"); - for _ in 0..path_component_count { - let letter = rng.lock().gen_range(b'a'..=b'z'); - path.push(std::str::from_utf8(&[letter]).unwrap()); - } - path.set_extension("rs"); - let parent_path = path.parent().unwrap(); - log::info!("Host: creating file {:?}", path,); - - if fs.create_dir(&parent_path).await.is_ok() - && fs.create_file(&path, Default::default()).await.is_ok() - { - files.lock().push(path); - break; - } else { - log::info!("Host: cannot create file"); - } - }, + cx.background().simulate_random_delay().await; } - cx.background().simulate_random_delay().await; + Ok(()) } + let result = simulate_host_internal( + &mut self, + project.clone(), + files, + op_start_signal, + rng, + &mut cx, + ) + .await; log::info!("Host done"); - self.project = Some(project); - (self, cx) + (self, cx, result.err()) } pub async fn simulate_guest( mut self, - guest_id: usize, + guest_username: String, project: ModelHandle, - operations: Rc>, - max_operations: usize, + op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, rng: Arc>, mut cx: TestAppContext, - ) -> (Self, TestAppContext) { - while operations.get() < max_operations { - let buffer = if self.buffers.is_empty() || rng.lock().gen() { - let worktree = if let Some(worktree) = project.read_with(&cx, |project, cx| { - project - .worktrees(&cx) - .filter(|worktree| { - let worktree = worktree.read(cx); - worktree.is_visible() - && worktree.entries(false).any(|e| e.is_file()) + ) -> (Self, TestAppContext, Option) { + async fn simulate_guest_internal( + client: &mut TestClient, + guest_username: &str, + project: ModelHandle, + mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>, + rng: Arc>, + cx: &mut TestAppContext, + ) -> anyhow::Result<()> { + while op_start_signal.next().await.is_some() { + let buffer = if client.buffers.is_empty() || rng.lock().gen() { + let worktree = if let Some(worktree) = + project.read_with(cx, |project, cx| { + project + .worktrees(&cx) + .filter(|worktree| { + let worktree = worktree.read(cx); + worktree.is_visible() + && worktree.entries(false).any(|e| e.is_file()) + }) + .choose(&mut *rng.lock()) + }) { + worktree + } else { + cx.background().simulate_random_delay().await; + continue; + }; + + let (worktree_root_name, project_path) = + worktree.read_with(cx, |worktree, _| { + let entry = worktree + .entries(false) + .filter(|e| e.is_file()) + .choose(&mut *rng.lock()) + .unwrap(); + ( + worktree.root_name().to_string(), + (worktree.id(), entry.path.clone()), + ) + }); + log::info!( + "{}: opening path {:?} in worktree {} ({})", + guest_username, + project_path.1, + project_path.0, + worktree_root_name, + ); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) }) - .choose(&mut *rng.lock()) - }) { - worktree + .await?; + log::info!( + "{}: opened path {:?} in worktree {} ({}) with buffer id {}", + guest_username, + project_path.1, + project_path.0, + worktree_root_name, + buffer.read_with(cx, |buffer, _| buffer.remote_id()) + ); + client.buffers.insert(buffer.clone()); + buffer } else { - cx.background().simulate_random_delay().await; - continue; + client + .buffers + .iter() + .choose(&mut *rng.lock()) + .unwrap() + .clone() }; - operations.set(operations.get() + 1); - let (worktree_root_name, project_path) = - worktree.read_with(&cx, |worktree, _| { - let entry = worktree - .entries(false) - .filter(|e| e.is_file()) - .choose(&mut *rng.lock()) - .unwrap(); - ( - worktree.root_name().to_string(), - (worktree.id(), entry.path.clone()), - ) - }); - log::info!( - "Guest {}: opening path {:?} in worktree {} ({})", - guest_id, - project_path.1, - project_path.0, - worktree_root_name, - ); - let buffer = project - .update(&mut cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - }) - .await - .unwrap(); - log::info!( - "Guest {}: opened path {:?} in worktree {} ({}) with buffer id {}", - guest_id, - project_path.1, - project_path.0, - worktree_root_name, - buffer.read_with(&cx, |buffer, _| buffer.remote_id()) - ); - self.buffers.insert(buffer.clone()); - buffer - } else { - operations.set(operations.get() + 1); - - self.buffers - .iter() - .choose(&mut *rng.lock()) - .unwrap() - .clone() - }; - - let choice = rng.lock().gen_range(0..100); - match choice { - 0..=9 => { - cx.update(|cx| { - log::info!( - "Guest {}: dropping buffer {:?}", - guest_id, - buffer.read(cx).file().unwrap().full_path(cx) - ); - self.buffers.remove(&buffer); - drop(buffer); - }); - } - 10..=19 => { - let completions = project.update(&mut cx, |project, cx| { - log::info!( - "Guest {}: requesting completions for buffer {} ({:?})", - guest_id, - buffer.read(cx).remote_id(), - buffer.read(cx).file().unwrap().full_path(cx) - ); - let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); - project.completions(&buffer, offset, cx) - }); - let completions = cx.background().spawn(async move { - completions.await.expect("completions request failed"); - }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching completions request", guest_id); - completions.detach(); - } else { - completions.await; + let choice = rng.lock().gen_range(0..100); + match choice { + 0..=9 => { + cx.update(|cx| { + log::info!( + "{}: dropping buffer {:?}", + guest_username, + buffer.read(cx).file().unwrap().full_path(cx) + ); + client.buffers.remove(&buffer); + drop(buffer); + }); + } + 10..=19 => { + let completions = project.update(cx, |project, cx| { + log::info!( + "{}: requesting completions for buffer {} ({:?})", + guest_username, + buffer.read(cx).remote_id(), + buffer.read(cx).file().unwrap().full_path(cx) + ); + let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); + project.completions(&buffer, offset, cx) + }); + let completions = cx.background().spawn(async move { + completions + .await + .map_err(|err| anyhow!("completions request failed: {:?}", err)) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching completions request", guest_username); + cx.update(|cx| completions.detach_and_log_err(cx)); + } else { + completions.await?; + } + } + 20..=29 => { + let code_actions = project.update(cx, |project, cx| { + log::info!( + "{}: requesting code actions for buffer {} ({:?})", + guest_username, + buffer.read(cx).remote_id(), + buffer.read(cx).file().unwrap().full_path(cx) + ); + let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock()); + project.code_actions(&buffer, range, cx) + }); + let code_actions = cx.background().spawn(async move { + code_actions.await.map_err(|err| { + anyhow!("code actions request failed: {:?}", err) + }) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching code actions request", guest_username); + cx.update(|cx| code_actions.detach_and_log_err(cx)); + } else { + code_actions.await?; + } + } + 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => { + let (requested_version, save) = buffer.update(cx, |buffer, cx| { + log::info!( + "{}: saving buffer {} ({:?})", + guest_username, + buffer.remote_id(), + buffer.file().unwrap().full_path(cx) + ); + (buffer.version(), buffer.save(cx)) + }); + let save = cx.background().spawn(async move { + let (saved_version, _) = save + .await + .map_err(|err| anyhow!("save request failed: {:?}", err))?; + assert!(saved_version.observed_all(&requested_version)); + Ok::<_, anyhow::Error>(()) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching save request", guest_username); + cx.update(|cx| save.detach_and_log_err(cx)); + } else { + save.await?; + } + } + 40..=44 => { + let prepare_rename = project.update(cx, |project, cx| { + log::info!( + "{}: preparing rename for buffer {} ({:?})", + guest_username, + buffer.read(cx).remote_id(), + buffer.read(cx).file().unwrap().full_path(cx) + ); + let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); + project.prepare_rename(buffer, offset, cx) + }); + let prepare_rename = cx.background().spawn(async move { + prepare_rename.await.map_err(|err| { + anyhow!("prepare rename request failed: {:?}", err) + }) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching prepare rename request", guest_username); + cx.update(|cx| prepare_rename.detach_and_log_err(cx)); + } else { + prepare_rename.await?; + } + } + 45..=49 => { + let definitions = project.update(cx, |project, cx| { + log::info!( + "{}: requesting definitions for buffer {} ({:?})", + guest_username, + buffer.read(cx).remote_id(), + buffer.read(cx).file().unwrap().full_path(cx) + ); + let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); + project.definition(&buffer, offset, cx) + }); + let definitions = cx.background().spawn(async move { + definitions + .await + .map_err(|err| anyhow!("definitions request failed: {:?}", err)) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching definitions request", guest_username); + cx.update(|cx| definitions.detach_and_log_err(cx)); + } else { + client + .buffers + .extend(definitions.await?.into_iter().map(|loc| loc.buffer)); + } + } + 50..=54 => { + let highlights = project.update(cx, |project, cx| { + log::info!( + "{}: requesting highlights for buffer {} ({:?})", + guest_username, + buffer.read(cx).remote_id(), + buffer.read(cx).file().unwrap().full_path(cx) + ); + let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); + project.document_highlights(&buffer, offset, cx) + }); + let highlights = cx.background().spawn(async move { + highlights + .await + .map_err(|err| anyhow!("highlights request failed: {:?}", err)) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching highlights request", guest_username); + cx.update(|cx| highlights.detach_and_log_err(cx)); + } else { + highlights.await?; + } + } + 55..=59 => { + let search = project.update(cx, |project, cx| { + let query = rng.lock().gen_range('a'..='z'); + log::info!("{}: project-wide search {:?}", guest_username, query); + project.search(SearchQuery::text(query, false, false), cx) + }); + let search = cx.background().spawn(async move { + search + .await + .map_err(|err| anyhow!("search request failed: {:?}", err)) + }); + if rng.lock().gen_bool(0.3) { + log::info!("{}: detaching search request", guest_username); + cx.update(|cx| search.detach_and_log_err(cx)); + } else { + client.buffers.extend(search.await?.into_keys()); + } + } + _ => { + buffer.update(cx, |buffer, cx| { + log::info!( + "{}: updating buffer {} ({:?})", + guest_username, + buffer.remote_id(), + buffer.file().unwrap().full_path(cx) + ); + if rng.lock().gen_bool(0.7) { + buffer.randomly_edit(&mut *rng.lock(), 5, cx); + } else { + buffer.randomly_undo_redo(&mut *rng.lock(), cx); + } + }); } } - 20..=29 => { - let code_actions = project.update(&mut cx, |project, cx| { - log::info!( - "Guest {}: requesting code actions for buffer {} ({:?})", - guest_id, - buffer.read(cx).remote_id(), - buffer.read(cx).file().unwrap().full_path(cx) - ); - let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock()); - project.code_actions(&buffer, range, cx) - }); - let code_actions = cx.background().spawn(async move { - code_actions.await.expect("code actions request failed"); - }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching code actions request", guest_id); - code_actions.detach(); - } else { - code_actions.await; - } - } - 30..=39 if buffer.read_with(&cx, |buffer, _| buffer.is_dirty()) => { - let (requested_version, save) = buffer.update(&mut cx, |buffer, cx| { - log::info!( - "Guest {}: saving buffer {} ({:?})", - guest_id, - buffer.remote_id(), - buffer.file().unwrap().full_path(cx) - ); - (buffer.version(), buffer.save(cx)) - }); - let save = cx.background().spawn(async move { - let (saved_version, _) = save.await.expect("save request failed"); - assert!(saved_version.observed_all(&requested_version)); - }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching save request", guest_id); - save.detach(); - } else { - save.await; - } - } - 40..=44 => { - let prepare_rename = project.update(&mut cx, |project, cx| { - log::info!( - "Guest {}: preparing rename for buffer {} ({:?})", - guest_id, - buffer.read(cx).remote_id(), - buffer.read(cx).file().unwrap().full_path(cx) - ); - let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); - project.prepare_rename(buffer, offset, cx) - }); - let prepare_rename = cx.background().spawn(async move { - prepare_rename.await.expect("prepare rename request failed"); - }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching prepare rename request", guest_id); - prepare_rename.detach(); - } else { - prepare_rename.await; - } - } - 45..=49 => { - let definitions = project.update(&mut cx, |project, cx| { - log::info!( - "Guest {}: requesting definitions for buffer {} ({:?})", - guest_id, - buffer.read(cx).remote_id(), - buffer.read(cx).file().unwrap().full_path(cx) - ); - let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); - project.definition(&buffer, offset, cx) - }); - let definitions = cx.background().spawn(async move { - definitions.await.expect("definitions request failed") - }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching definitions request", guest_id); - definitions.detach(); - } else { - self.buffers - .extend(definitions.await.into_iter().map(|loc| loc.buffer)); - } - } - 50..=54 => { - let highlights = project.update(&mut cx, |project, cx| { - log::info!( - "Guest {}: requesting highlights for buffer {} ({:?})", - guest_id, - buffer.read(cx).remote_id(), - buffer.read(cx).file().unwrap().full_path(cx) - ); - let offset = rng.lock().gen_range(0..=buffer.read(cx).len()); - project.document_highlights(&buffer, offset, cx) - }); - let highlights = cx.background().spawn(async move { - highlights.await.expect("highlights request failed"); - }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching highlights request", guest_id); - highlights.detach(); - } else { - highlights.await; - } - } - 55..=59 => { - let search = project.update(&mut cx, |project, cx| { - let query = rng.lock().gen_range('a'..='z'); - log::info!("Guest {}: project-wide search {:?}", guest_id, query); - project.search(SearchQuery::text(query, false, false), cx) - }); - let search = cx - .background() - .spawn(async move { search.await.expect("search request failed") }); - if rng.lock().gen_bool(0.3) { - log::info!("Guest {}: detaching search request", guest_id); - search.detach(); - } else { - self.buffers.extend(search.await.into_keys()); - } - } - _ => { - buffer.update(&mut cx, |buffer, cx| { - log::info!( - "Guest {}: updating buffer {} ({:?})", - guest_id, - buffer.remote_id(), - buffer.file().unwrap().full_path(cx) - ); - buffer.randomly_edit(&mut *rng.lock(), 5, cx) - }); - } + cx.background().simulate_random_delay().await; } - cx.background().simulate_random_delay().await; + Ok(()) } - log::info!("Guest {} done", guest_id); + let result = simulate_guest_internal( + &mut self, + &guest_username, + project.clone(), + op_start_signal, + rng, + &mut cx, + ) + .await; + log::info!("{}: done", guest_username); self.project = Some(project); - (self, cx) + (self, cx, result.err()) } } diff --git a/crates/server/src/rpc/store.rs b/crates/collab/src/rpc/store.rs similarity index 96% rename from crates/server/src/rpc/store.rs rename to crates/collab/src/rpc/store.rs index 6f5252fecf..946e9f8420 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -66,6 +66,10 @@ pub struct JoinedProject<'a> { pub project: &'a Project, } +pub struct SharedProject { + pub authorized_user_ids: Vec, +} + pub struct UnsharedProject { pub connection_ids: Vec, pub authorized_user_ids: Vec, @@ -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 { 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(); diff --git a/crates/server/src/team.rs b/crates/collab/src/team.rs similarity index 100% rename from crates/server/src/team.rs rename to crates/collab/src/team.rs diff --git a/crates/server/static/browserconfig.xml b/crates/collab/static/browserconfig.xml similarity index 100% rename from crates/server/static/browserconfig.xml rename to crates/collab/static/browserconfig.xml diff --git a/crates/server/static/fonts/VisbyCF-Bold.eot b/crates/collab/static/fonts/VisbyCF-Bold.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-Bold.eot rename to crates/collab/static/fonts/VisbyCF-Bold.eot diff --git a/crates/server/static/fonts/VisbyCF-Bold.woff b/crates/collab/static/fonts/VisbyCF-Bold.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-Bold.woff rename to crates/collab/static/fonts/VisbyCF-Bold.woff diff --git a/crates/server/static/fonts/VisbyCF-Bold.woff2 b/crates/collab/static/fonts/VisbyCF-Bold.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-Bold.woff2 rename to crates/collab/static/fonts/VisbyCF-Bold.woff2 diff --git a/crates/server/static/fonts/VisbyCF-BoldOblique.eot b/crates/collab/static/fonts/VisbyCF-BoldOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-BoldOblique.eot rename to crates/collab/static/fonts/VisbyCF-BoldOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-BoldOblique.woff b/crates/collab/static/fonts/VisbyCF-BoldOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-BoldOblique.woff rename to crates/collab/static/fonts/VisbyCF-BoldOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-BoldOblique.woff2 b/crates/collab/static/fonts/VisbyCF-BoldOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-BoldOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-BoldOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-DemiBold.eot b/crates/collab/static/fonts/VisbyCF-DemiBold.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-DemiBold.eot rename to crates/collab/static/fonts/VisbyCF-DemiBold.eot diff --git a/crates/server/static/fonts/VisbyCF-DemiBold.woff b/crates/collab/static/fonts/VisbyCF-DemiBold.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-DemiBold.woff rename to crates/collab/static/fonts/VisbyCF-DemiBold.woff diff --git a/crates/server/static/fonts/VisbyCF-DemiBold.woff2 b/crates/collab/static/fonts/VisbyCF-DemiBold.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-DemiBold.woff2 rename to crates/collab/static/fonts/VisbyCF-DemiBold.woff2 diff --git a/crates/server/static/fonts/VisbyCF-DemiBoldOblique.eot b/crates/collab/static/fonts/VisbyCF-DemiBoldOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-DemiBoldOblique.eot rename to crates/collab/static/fonts/VisbyCF-DemiBoldOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-DemiBoldOblique.woff b/crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-DemiBoldOblique.woff rename to crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-DemiBoldOblique.woff2 b/crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-DemiBoldOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-DemiBoldOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-ExtraBold.eot b/crates/collab/static/fonts/VisbyCF-ExtraBold.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-ExtraBold.eot rename to crates/collab/static/fonts/VisbyCF-ExtraBold.eot diff --git a/crates/server/static/fonts/VisbyCF-ExtraBold.woff b/crates/collab/static/fonts/VisbyCF-ExtraBold.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-ExtraBold.woff rename to crates/collab/static/fonts/VisbyCF-ExtraBold.woff diff --git a/crates/server/static/fonts/VisbyCF-ExtraBold.woff2 b/crates/collab/static/fonts/VisbyCF-ExtraBold.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-ExtraBold.woff2 rename to crates/collab/static/fonts/VisbyCF-ExtraBold.woff2 diff --git a/crates/server/static/fonts/VisbyCF-ExtraBoldOblique.eot b/crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-ExtraBoldOblique.eot rename to crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-ExtraBoldOblique.woff b/crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-ExtraBoldOblique.woff rename to crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-ExtraBoldOblique.woff2 b/crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-ExtraBoldOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-ExtraBoldOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-Heavy.eot b/crates/collab/static/fonts/VisbyCF-Heavy.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-Heavy.eot rename to crates/collab/static/fonts/VisbyCF-Heavy.eot diff --git a/crates/server/static/fonts/VisbyCF-Heavy.woff b/crates/collab/static/fonts/VisbyCF-Heavy.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-Heavy.woff rename to crates/collab/static/fonts/VisbyCF-Heavy.woff diff --git a/crates/server/static/fonts/VisbyCF-Heavy.woff2 b/crates/collab/static/fonts/VisbyCF-Heavy.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-Heavy.woff2 rename to crates/collab/static/fonts/VisbyCF-Heavy.woff2 diff --git a/crates/server/static/fonts/VisbyCF-HeavyOblique.eot b/crates/collab/static/fonts/VisbyCF-HeavyOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-HeavyOblique.eot rename to crates/collab/static/fonts/VisbyCF-HeavyOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-HeavyOblique.woff b/crates/collab/static/fonts/VisbyCF-HeavyOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-HeavyOblique.woff rename to crates/collab/static/fonts/VisbyCF-HeavyOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-HeavyOblique.woff2 b/crates/collab/static/fonts/VisbyCF-HeavyOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-HeavyOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-HeavyOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-Light.eot b/crates/collab/static/fonts/VisbyCF-Light.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-Light.eot rename to crates/collab/static/fonts/VisbyCF-Light.eot diff --git a/crates/server/static/fonts/VisbyCF-Light.woff b/crates/collab/static/fonts/VisbyCF-Light.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-Light.woff rename to crates/collab/static/fonts/VisbyCF-Light.woff diff --git a/crates/server/static/fonts/VisbyCF-Light.woff2 b/crates/collab/static/fonts/VisbyCF-Light.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-Light.woff2 rename to crates/collab/static/fonts/VisbyCF-Light.woff2 diff --git a/crates/server/static/fonts/VisbyCF-LightOblique.eot b/crates/collab/static/fonts/VisbyCF-LightOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-LightOblique.eot rename to crates/collab/static/fonts/VisbyCF-LightOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-LightOblique.woff b/crates/collab/static/fonts/VisbyCF-LightOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-LightOblique.woff rename to crates/collab/static/fonts/VisbyCF-LightOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-LightOblique.woff2 b/crates/collab/static/fonts/VisbyCF-LightOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-LightOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-LightOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-Medium.eot b/crates/collab/static/fonts/VisbyCF-Medium.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-Medium.eot rename to crates/collab/static/fonts/VisbyCF-Medium.eot diff --git a/crates/server/static/fonts/VisbyCF-Medium.woff b/crates/collab/static/fonts/VisbyCF-Medium.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-Medium.woff rename to crates/collab/static/fonts/VisbyCF-Medium.woff diff --git a/crates/server/static/fonts/VisbyCF-Medium.woff2 b/crates/collab/static/fonts/VisbyCF-Medium.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-Medium.woff2 rename to crates/collab/static/fonts/VisbyCF-Medium.woff2 diff --git a/crates/server/static/fonts/VisbyCF-MediumOblique.eot b/crates/collab/static/fonts/VisbyCF-MediumOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-MediumOblique.eot rename to crates/collab/static/fonts/VisbyCF-MediumOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-MediumOblique.woff b/crates/collab/static/fonts/VisbyCF-MediumOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-MediumOblique.woff rename to crates/collab/static/fonts/VisbyCF-MediumOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-MediumOblique.woff2 b/crates/collab/static/fonts/VisbyCF-MediumOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-MediumOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-MediumOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-Regular.eot b/crates/collab/static/fonts/VisbyCF-Regular.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-Regular.eot rename to crates/collab/static/fonts/VisbyCF-Regular.eot diff --git a/crates/server/static/fonts/VisbyCF-Regular.woff b/crates/collab/static/fonts/VisbyCF-Regular.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-Regular.woff rename to crates/collab/static/fonts/VisbyCF-Regular.woff diff --git a/crates/server/static/fonts/VisbyCF-Regular.woff2 b/crates/collab/static/fonts/VisbyCF-Regular.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-Regular.woff2 rename to crates/collab/static/fonts/VisbyCF-Regular.woff2 diff --git a/crates/server/static/fonts/VisbyCF-RegularOblique.eot b/crates/collab/static/fonts/VisbyCF-RegularOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-RegularOblique.eot rename to crates/collab/static/fonts/VisbyCF-RegularOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-RegularOblique.woff b/crates/collab/static/fonts/VisbyCF-RegularOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-RegularOblique.woff rename to crates/collab/static/fonts/VisbyCF-RegularOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-RegularOblique.woff2 b/crates/collab/static/fonts/VisbyCF-RegularOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-RegularOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-RegularOblique.woff2 diff --git a/crates/server/static/fonts/VisbyCF-Thin.eot b/crates/collab/static/fonts/VisbyCF-Thin.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-Thin.eot rename to crates/collab/static/fonts/VisbyCF-Thin.eot diff --git a/crates/server/static/fonts/VisbyCF-Thin.woff b/crates/collab/static/fonts/VisbyCF-Thin.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-Thin.woff rename to crates/collab/static/fonts/VisbyCF-Thin.woff diff --git a/crates/server/static/fonts/VisbyCF-Thin.woff2 b/crates/collab/static/fonts/VisbyCF-Thin.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-Thin.woff2 rename to crates/collab/static/fonts/VisbyCF-Thin.woff2 diff --git a/crates/server/static/fonts/VisbyCF-ThinOblique.eot b/crates/collab/static/fonts/VisbyCF-ThinOblique.eot similarity index 100% rename from crates/server/static/fonts/VisbyCF-ThinOblique.eot rename to crates/collab/static/fonts/VisbyCF-ThinOblique.eot diff --git a/crates/server/static/fonts/VisbyCF-ThinOblique.woff b/crates/collab/static/fonts/VisbyCF-ThinOblique.woff similarity index 100% rename from crates/server/static/fonts/VisbyCF-ThinOblique.woff rename to crates/collab/static/fonts/VisbyCF-ThinOblique.woff diff --git a/crates/server/static/fonts/VisbyCF-ThinOblique.woff2 b/crates/collab/static/fonts/VisbyCF-ThinOblique.woff2 similarity index 100% rename from crates/server/static/fonts/VisbyCF-ThinOblique.woff2 rename to crates/collab/static/fonts/VisbyCF-ThinOblique.woff2 diff --git a/crates/server/static/images/android-chrome-192x192.png b/crates/collab/static/images/android-chrome-192x192.png similarity index 100% rename from crates/server/static/images/android-chrome-192x192.png rename to crates/collab/static/images/android-chrome-192x192.png diff --git a/crates/server/static/images/android-chrome-512x512.png b/crates/collab/static/images/android-chrome-512x512.png similarity index 100% rename from crates/server/static/images/android-chrome-512x512.png rename to crates/collab/static/images/android-chrome-512x512.png diff --git a/crates/server/static/images/apple-touch-icon.png b/crates/collab/static/images/apple-touch-icon.png similarity index 100% rename from crates/server/static/images/apple-touch-icon.png rename to crates/collab/static/images/apple-touch-icon.png diff --git a/crates/server/static/images/favicon-16x16.png b/crates/collab/static/images/favicon-16x16.png similarity index 100% rename from crates/server/static/images/favicon-16x16.png rename to crates/collab/static/images/favicon-16x16.png diff --git a/crates/server/static/images/favicon-32x32.png b/crates/collab/static/images/favicon-32x32.png similarity index 100% rename from crates/server/static/images/favicon-32x32.png rename to crates/collab/static/images/favicon-32x32.png diff --git a/crates/server/static/images/favicon.png b/crates/collab/static/images/favicon.png similarity index 100% rename from crates/server/static/images/favicon.png rename to crates/collab/static/images/favicon.png diff --git a/crates/server/static/images/favicon.svg b/crates/collab/static/images/favicon.svg similarity index 100% rename from crates/server/static/images/favicon.svg rename to crates/collab/static/images/favicon.svg diff --git a/crates/server/static/images/mstile-144x144.png b/crates/collab/static/images/mstile-144x144.png similarity index 100% rename from crates/server/static/images/mstile-144x144.png rename to crates/collab/static/images/mstile-144x144.png diff --git a/crates/server/static/images/mstile-150x150.png b/crates/collab/static/images/mstile-150x150.png similarity index 100% rename from crates/server/static/images/mstile-150x150.png rename to crates/collab/static/images/mstile-150x150.png diff --git a/crates/server/static/images/mstile-310x150.png b/crates/collab/static/images/mstile-310x150.png similarity index 100% rename from crates/server/static/images/mstile-310x150.png rename to crates/collab/static/images/mstile-310x150.png diff --git a/crates/server/static/images/mstile-310x310.png b/crates/collab/static/images/mstile-310x310.png similarity index 100% rename from crates/server/static/images/mstile-310x310.png rename to crates/collab/static/images/mstile-310x310.png diff --git a/crates/server/static/images/mstile-70x70.png b/crates/collab/static/images/mstile-70x70.png similarity index 100% rename from crates/server/static/images/mstile-70x70.png rename to crates/collab/static/images/mstile-70x70.png diff --git a/crates/server/static/images/safari-pinned-tab.svg b/crates/collab/static/images/safari-pinned-tab.svg similarity index 100% rename from crates/server/static/images/safari-pinned-tab.svg rename to crates/collab/static/images/safari-pinned-tab.svg diff --git a/crates/server/static/images/zed-og-image.png b/crates/collab/static/images/zed-og-image.png similarity index 100% rename from crates/server/static/images/zed-og-image.png rename to crates/collab/static/images/zed-og-image.png diff --git a/crates/server/static/images/zed-twitter-image.png b/crates/collab/static/images/zed-twitter-image.png similarity index 100% rename from crates/server/static/images/zed-twitter-image.png rename to crates/collab/static/images/zed-twitter-image.png diff --git a/crates/server/static/prism.js b/crates/collab/static/prism.js similarity index 100% rename from crates/server/static/prism.js rename to crates/collab/static/prism.js diff --git a/crates/server/static/prose.css b/crates/collab/static/prose.css similarity index 100% rename from crates/server/static/prose.css rename to crates/collab/static/prose.css diff --git a/crates/server/static/prose.css.map b/crates/collab/static/prose.css.map similarity index 100% rename from crates/server/static/prose.css.map rename to crates/collab/static/prose.css.map diff --git a/crates/server/static/prose.scss b/crates/collab/static/prose.scss similarity index 100% rename from crates/server/static/prose.scss rename to crates/collab/static/prose.scss diff --git a/crates/server/static/site.webmanifest b/crates/collab/static/site.webmanifest similarity index 100% rename from crates/server/static/site.webmanifest rename to crates/collab/static/site.webmanifest diff --git a/crates/server/static/svg/hero.svg b/crates/collab/static/svg/hero.svg similarity index 100% rename from crates/server/static/svg/hero.svg rename to crates/collab/static/svg/hero.svg diff --git a/crates/server/styles.css b/crates/collab/styles.css similarity index 100% rename from crates/server/styles.css rename to crates/collab/styles.css diff --git a/crates/server/templates/admin.hbs b/crates/collab/templates/admin.hbs similarity index 100% rename from crates/server/templates/admin.hbs rename to crates/collab/templates/admin.hbs diff --git a/crates/server/templates/careers.hbs b/crates/collab/templates/careers.hbs similarity index 100% rename from crates/server/templates/careers.hbs rename to crates/collab/templates/careers.hbs diff --git a/crates/server/templates/community.hbs b/crates/collab/templates/community.hbs similarity index 100% rename from crates/server/templates/community.hbs rename to crates/collab/templates/community.hbs diff --git a/crates/server/templates/docs.hbs b/crates/collab/templates/docs.hbs similarity index 100% rename from crates/server/templates/docs.hbs rename to crates/collab/templates/docs.hbs diff --git a/crates/server/templates/error.hbs b/crates/collab/templates/error.hbs similarity index 100% rename from crates/server/templates/error.hbs rename to crates/collab/templates/error.hbs diff --git a/crates/server/templates/home.hbs b/crates/collab/templates/home.hbs similarity index 100% rename from crates/server/templates/home.hbs rename to crates/collab/templates/home.hbs diff --git a/crates/server/templates/partials/layout.hbs b/crates/collab/templates/partials/layout.hbs similarity index 100% rename from crates/server/templates/partials/layout.hbs rename to crates/collab/templates/partials/layout.hbs diff --git a/crates/server/templates/releases.hbs b/crates/collab/templates/releases.hbs similarity index 100% rename from crates/server/templates/releases.hbs rename to crates/collab/templates/releases.hbs diff --git a/crates/server/templates/signup.hbs b/crates/collab/templates/signup.hbs similarity index 100% rename from crates/server/templates/signup.hbs rename to crates/collab/templates/signup.hbs diff --git a/crates/server/templates/team.hbs b/crates/collab/templates/team.hbs similarity index 100% rename from crates/server/templates/team.hbs rename to crates/collab/templates/team.hbs diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml new file mode 100644 index 0000000000..aeaffa3e6f --- /dev/null +++ b/crates/command_palette/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "command_palette" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/command_palette.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } +gpui = { path = "../gpui" } +picker = { path = "../picker" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +editor = { path = "../editor", features = ["test-support"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +workspace = { path = "../workspace", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs new file mode 100644 index 0000000000..b535450d12 --- /dev/null +++ b/crates/command_palette/src/command_palette.rs @@ -0,0 +1,362 @@ +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, + elements::{ChildView, Flex, Label, ParentElement}, + keymap::Keystroke, + Action, Element, Entity, MutableAppContext, View, ViewContext, ViewHandle, +}; +use picker::{Picker, PickerDelegate}; +use settings::Settings; +use std::cmp; +use workspace::Workspace; + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CommandPalette::toggle); + Picker::::init(cx); +} + +actions!(command_palette, [Toggle]); + +pub struct CommandPalette { + picker: ViewHandle>, + actions: Vec, + matches: Vec, + selected_ix: usize, + focused_view_id: usize, +} + +pub enum Event { + Dismissed, + Confirmed { + window_id: usize, + focused_view_id: usize, + action: Box, + }, +} + +struct Command { + name: String, + action: Box, + keystrokes: Vec, +} + +impl CommandPalette { + pub fn new(focused_view_id: usize, cx: &mut ViewContext) -> 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) { + 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, + event: &Event, + cx: &mut ViewContext, + ) { + 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) { + 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.selected_ix = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut gpui::ViewContext, + ) -> 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::>(); + 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) { + cx.emit(Event::Dismissed); + } + + fn confirm(&mut self, cx: &mut ViewContext) { + 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::(); + 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::() + .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"); + }); + } +} diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index 43bd2548a8..6a4dbf653d 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] client = { path = "../client" } gpui = { path = "../gpui" } +settings = { path = "../settings" } theme = { path = "../theme" } workspace = { path = "../workspace" } postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 06c6b8f1bb..45b5f69b5e 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use client::{Contact, UserStore}; use gpui::{ elements::*, @@ -8,7 +6,9 @@ use gpui::{ Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, ViewContext, }; -use workspace::{AppState, JoinProject, JoinProjectParams, Settings}; +use settings::Settings; +use std::sync::Arc; +use workspace::{AppState, JoinProject}; pub struct ContactsPanel { contacts: ListState, @@ -206,10 +206,10 @@ impl ContactsPanel { }) .on_click(move |cx| { if !is_host && !is_guest { - cx.dispatch_global_action(JoinProject(JoinProjectParams { + cx.dispatch_global_action(JoinProject { project_id, app_state: app_state.clone(), - })); + }); } }) .flex(1., true) diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 4cf45041e9..4f59ffc68c 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -14,6 +14,7 @@ editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } project = { path = "../project" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index da50e99f1e..c15c20c775 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -8,7 +8,7 @@ use editor::{ highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset, }; use gpui::{ - action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity, + actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -16,6 +16,8 @@ use language::{ Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, }; use project::{DiagnosticSummary, Project, ProjectPath}; +use serde_json::json; +use settings::Settings; use std::{ any::{Any, TypeId}, cmp::Ordering, @@ -25,14 +27,13 @@ use std::{ sync::Arc, }; use util::TryFutureExt; -use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace}; +use workspace::{ItemHandle as _, ItemNavHistory, Workspace}; -action!(Deploy); +actions!(diagnostics, [Deploy]); const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]); cx.add_action(ProjectDiagnosticsEditor::deploy); } @@ -91,6 +92,31 @@ impl View for ProjectDiagnosticsEditor { cx.focus(&self.editor); } } + + fn debug_json(&self, cx: &AppContext) -> serde_json::Value { + let project = self.project.read(cx); + json!({ + "project": json!({ + "language_servers": project.language_server_statuses().collect::>(), + "summary": project.diagnostic_summary(cx), + }), + "summary": self.summary, + "paths_to_update": self.paths_to_update.iter().map(|path| + path.path.to_string_lossy() + ).collect::>(), + "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::>(), + }) + ).collect::>(), + }) + } } impl ProjectDiagnosticsEditor { diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 690c5100ec..2f9a228256 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,9 +1,11 @@ use crate::render_summary; use gpui::{ - elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext, + elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, RenderContext, View, + ViewContext, }; use project::Project; -use workspace::{Settings, StatusItemView}; +use settings::Settings; +use workspace::StatusItemView; pub struct DiagnosticSummary { summary: project::DiagnosticSummary, @@ -66,6 +68,10 @@ impl View for DiagnosticSummary { .on_click(|cx| cx.dispatch_action(crate::Deploy)) .boxed() } + + fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value { + serde_json::json!({ "summary": self.summary }) + } } impl StatusItemView for DiagnosticSummary { diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 77e169b91b..a1c1409d06 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -28,6 +28,7 @@ language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } rpc = { path = "../rpc" } +settings = { path = "../settings" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } @@ -36,9 +37,10 @@ workspace = { path = "../workspace" } aho-corasick = "0.7" anyhow = "1.0" futures = "0.3" +indoc = "1.0.4" itertools = "0.10" lazy_static = "1.4" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = "2.1.1" parking_lot = "0.11" postage = { version = "0.4", features = ["futures-traits"] } @@ -54,6 +56,7 @@ lsp = { path = "../lsp", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.8" diff --git a/crates/editor/src/context_menu.rs b/crates/editor/src/context_menu.rs new file mode 100644 index 0000000000..b1c4a1b51c --- /dev/null +++ b/crates/editor/src/context_menu.rs @@ -0,0 +1,272 @@ +pub enum ContextMenu { + Completions(CompletionsMenu), + CodeActions(CodeActionsMenu), +} + +impl ContextMenu { + pub fn select_prev(&mut self, cx: &mut ViewContext) -> 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) -> 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, + completions: Arc<[Completion]>, + match_candidates: Vec, + matches: Arc<[StringMatch]>, + selected_item: usize, + list: UniformListState, +} + +impl CompletionsMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + 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) { + 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::( + 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) { + 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, + selected_item: usize, + list: UniformListState, + deployed_from_indicator: bool, +} + +impl CodeActionsMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + if self.selected_item > 0 { + self.selected_item -= 1; + cx.notify() + } + } + + fn select_next(&mut self, cx: &mut ViewContext) { + 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::(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) + } +} diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 2bea851ec2..baf97a9b28 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -12,6 +12,7 @@ use gpui::{ Entity, ModelContext, ModelHandle, }; use language::{Point, Subscription as BufferSubscription}; +use settings::Settings; use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; @@ -46,7 +47,6 @@ impl Entity for DisplayMap { impl DisplayMap { pub fn new( buffer: ModelHandle, - tab_size: usize, font_id: FontId, font_size: f32, wrap_width: Option, @@ -55,6 +55,8 @@ impl DisplayMap { cx: &mut ModelContext, ) -> 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 { 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, cx: &mut ModelContext) { 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, cx: &mut ModelContext) -> u32 { + let language_name = buffer + .read(cx) + .as_singleton() + .and_then(|buffer| buffer.read(cx).language()) + .map(|language| language.name()); + + cx.global::().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) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 9d1b7a7588..b7f1836cdc 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -969,6 +969,7 @@ mod tests { use crate::multi_buffer::MultiBuffer; use gpui::{elements::Empty, Element}; use rand::prelude::*; + use settings::Settings; use std::env; use text::RandomCharIter; @@ -988,6 +989,8 @@ mod tests { #[gpui::test] fn test_basic_blocks(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let font_id = cx .font_cache() @@ -1157,7 +1160,7 @@ mod tests { let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot, subscription.consume().into_inner()); - let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, 4); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tabs_snapshot, tab_edits, cx) }); @@ -1167,6 +1170,8 @@ mod tests { #[gpui::test] fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let font_id = cx .font_cache() @@ -1209,6 +1214,8 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + cx.set_global(Settings::test(cx)); + let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); @@ -1296,7 +1303,8 @@ mod tests { let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), vec![]); - let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(folds_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tabs_snapshot, tab_edits, cx) }); @@ -1318,7 +1326,8 @@ mod tests { let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), vec![]); - let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(folds_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tabs_snapshot, tab_edits, cx) }); @@ -1338,7 +1347,7 @@ mod tests { } let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits); - let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tabs_snapshot, tab_edits, cx) }); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 2c09244a7d..3c020dceb7 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1210,6 +1210,7 @@ mod tests { use super::*; use crate::{MultiBuffer, ToPoint}; use rand::prelude::*; + use settings::Settings; use std::{cmp::Reverse, env, mem, sync::Arc}; use sum_tree::TreeMap; use text::RandomCharIter; @@ -1218,6 +1219,7 @@ mod tests { #[gpui::test] fn test_basic_folds(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1291,6 +1293,7 @@ mod tests { #[gpui::test] fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1354,6 +1357,7 @@ mod tests { #[gpui::test] fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1404,6 +1408,7 @@ mod tests { #[gpui::test(iterations = 100)] fn test_random_folds(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + cx.set_global(Settings::test(cx)); let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index de76a6f261..1491c790f6 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -12,7 +12,7 @@ use text::Point; pub struct TabMap(Mutex); 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, + tab_size: u32, ) -> (TabSnapshot, Vec) { 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::() + - 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, column: usize, tab_size: usize) -> usize { + fn expand_tabs(chars: impl Iterator, 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, 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)); } } } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 2fab37fb30..0e88d87bd7 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -559,11 +559,6 @@ impl WrapSnapshot { Patch::new(wrap_edits) } - pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, false, None) - .map(|h| h.text) - } - pub fn chunks<'a>( &'a self, rows: Range, @@ -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 { @@ -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::>(); 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 { + 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()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 486840c3d2..97be28ebd4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16,13 +16,13 @@ use display_map::*; pub use element::*; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - action, + actions, color::Color, elements::*, executor, fonts::{self, HighlightStyle, TextStyle}, geometry::vector::{vec2f, Vector2F}, - keymap::Binding, + impl_actions, impl_internal_actions, platform::CursorStyle, text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, @@ -41,6 +41,7 @@ pub use multi_buffer::{ use ordered_float::OrderedFloat; use project::{Project, ProjectTransaction}; use serde::{Deserialize, Serialize}; +use settings::Settings; use smallvec::SmallVec; use smol::Timer; use snippet::Snippet; @@ -55,93 +56,154 @@ use std::{ }; pub use sum_tree::Bias; use text::rope::TextDimension; -use theme::DiagnosticStyle; +use theme::{DiagnosticStyle, Theme}; use util::{post_inc, ResultExt, TryFutureExt}; -use workspace::{settings, ItemNavHistory, Settings, Workspace}; +use workspace::{ItemNavHistory, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; -action!(Cancel); -action!(Backspace); -action!(Delete); -action!(Input, String); -action!(Newline); -action!(Tab, Direction); -action!(Indent); -action!(Outdent); -action!(DeleteLine); -action!(DeleteToPreviousWordStart); -action!(DeleteToPreviousSubwordStart); -action!(DeleteToNextWordEnd); -action!(DeleteToNextSubwordEnd); -action!(DeleteToBeginningOfLine); -action!(DeleteToEndOfLine); -action!(CutToEndOfLine); -action!(DuplicateLine); -action!(MoveLineUp); -action!(MoveLineDown); -action!(Cut); -action!(Copy); -action!(Paste); -action!(Undo); -action!(Redo); -action!(MoveUp); -action!(MoveDown); -action!(MoveLeft); -action!(MoveRight); -action!(MoveToPreviousWordStart); -action!(MoveToPreviousSubwordStart); -action!(MoveToNextWordEnd); -action!(MoveToNextSubwordEnd); -action!(MoveToBeginningOfLine); -action!(MoveToEndOfLine); -action!(MoveToBeginning); -action!(MoveToEnd); -action!(SelectUp); -action!(SelectDown); -action!(SelectLeft); -action!(SelectRight); -action!(SelectToPreviousWordStart); -action!(SelectToPreviousSubwordStart); -action!(SelectToNextWordEnd); -action!(SelectToNextSubwordEnd); -action!(SelectToBeginningOfLine, bool); -action!(SelectToEndOfLine, bool); -action!(SelectToBeginning); -action!(SelectToEnd); -action!(SelectAll); -action!(SelectLine); -action!(SplitSelectionIntoLines); -action!(AddSelectionAbove); -action!(AddSelectionBelow); -action!(SelectNext, bool); -action!(ToggleComments); -action!(SelectLargerSyntaxNode); -action!(SelectSmallerSyntaxNode); -action!(MoveToEnclosingBracket); -action!(UndoSelection); -action!(RedoSelection); -action!(GoToDiagnostic, Direction); -action!(GoToDefinition); -action!(FindAllReferences); -action!(Rename); -action!(ConfirmRename); -action!(PageUp); -action!(PageDown); -action!(Fold); -action!(UnfoldLines); -action!(FoldSelectedRanges); -action!(Scroll, Vector2F); -action!(Select, SelectPhase); -action!(ShowCompletions); -action!(ToggleCodeActions, bool); -action!(ConfirmCompletion, Option); -action!(ConfirmCodeAction, Option); -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, +} + +#[derive(Clone, Default, Deserialize)] +pub struct ConfirmCodeAction { + #[serde(default)] + pub item_ix: Option, +} + +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>, highlighted_rows: Option>, - background_highlights: BTreeMap>)>, + background_highlights: BTreeMap Color, Vec>)>, nav_history: Option, context_menu: Option, completion_tasks: Vec<(CompletionId, Task>)>, @@ -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); @@ -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> { - self.buffer.read(cx).language(cx) + pub fn language_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> Option<&'a Arc> { + 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.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 self.mode == EditorMode::Full { + if self.active_diagnostics.is_some() { + self.dismiss_diagnostics(cx); + return; } - self.set_selections(selections, None, true, cx); - self.request_autoscroll(Autoscroll::Fit, cx); - } else { - let mut oldest_selection = self.oldest_selection::(&cx); - if self.selection_count() == 1 { - if oldest_selection.is_empty() { - cx.propagate_action(); - return; - } + 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; + } + + let mut oldest_selection = self.oldest_selection::(&cx); + 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, ) -> Option>> { 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, - ) { + pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { 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, ) -> Option>> { let editor = workspace.active_item(cx)?.act_as::(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::(ranges_to_highlight, color, cx); + editor.highlight_background::( + 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::( read_ranges, - read_background, + |theme| theme.editor.document_highlight_read_background, cx, ); this.highlight_background::( 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.move_to_snippet_tabstop(Bias::Left, cx); + pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Left, cx) } pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> 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::().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) { - 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) { + 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::().tab_size; - let mut selections = self.local_selections::(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) { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + + let mut selections = self.local_selections::(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::().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) { - let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(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::().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) { - let tab_size = cx.global::().tab_size; let selections = self.local_selections::(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::().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.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.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.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.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.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.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::(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) { - // 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::(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); @@ -4452,11 +4399,15 @@ impl Editor { self.selection_history.mode = SelectionHistoryMode::Normal; } - pub fn go_to_diagnostic( - &mut self, - &GoToDiagnostic(direction): &GoToDiagnostic, - cx: &mut ViewContext, - ) { + fn go_to_next_diagnostic(&mut self, _: &GoToNextDiagnostic, cx: &mut ViewContext) { + self.go_to_diagnostic(Direction::Next, cx) + } + + fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext) { + self.go_to_diagnostic(Direction::Prev, cx) + } + + pub fn go_to_diagnostic(&mut self, direction: Direction, cx: &mut ViewContext) { let buffer = self.buffer.read(cx).snapshot(cx); let selection = self.newest_selection_with_snapshot::(&buffer); let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { @@ -4640,8 +4591,11 @@ impl Editor { let editor = cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_background::(ranges_to_highlight, color, cx); + editor.highlight_background::( + ranges_to_highlight, + |theme| theme.editor.highlighted_line_background, + cx, + ); }); workspace.add_item(Box::new(editor), cx); }); @@ -4854,25 +4808,6 @@ impl Editor { Some(rename) } - fn invalidate_rename_range( - &mut self, - buffer: &MultiBufferSnapshot, - cx: &mut ViewContext, - ) { - if let Some(rename) = self.pending_rename.as_ref() { - if self.selections.len() == 1 { - let head = self.selections[0].head().to_offset(buffer); - let range = rename.range.to_offset(buffer).to_inclusive(); - if range.contains(&head) { - return; - } - } - let rename = self.pending_rename.take().unwrap(); - self.remove_blocks([rename.block_id].into_iter().collect(), cx); - self.clear_background_highlights::(cx); - } - } - #[cfg(any(test, feature = "test-support"))] pub fn pending_rename(&self) -> Option<&RenameState> { self.pending_rename.as_ref() @@ -5378,7 +5313,7 @@ impl Editor { self.select_larger_syntax_node_stack.clear(); self.autoclose_stack.invalidate(&self.selections, &buffer); self.snippet_stack.invalidate(&self.selections, &buffer); - self.invalidate_rename_range(&buffer, cx); + self.take_rename(false, cx); let new_cursor_position = self.newest_anchor_selection().head(); @@ -5668,16 +5603,22 @@ impl Editor { } pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - let language = self.language(cx); + let language_name = self + .buffer + .read(cx) + .as_singleton() + .and_then(|singleton_buffer| singleton_buffer.read(cx).language()) + .map(|l| l.name()); + let settings = cx.global::(); let mode = self .soft_wrap_mode_override - .unwrap_or_else(|| settings.soft_wrap(language)); + .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref())); match mode { settings::SoftWrap::None => SoftWrap::None, settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length(language)) + SoftWrap::Column(settings.preferred_line_length(language_name.as_deref())) } } } @@ -5703,18 +5644,18 @@ impl Editor { pub fn highlight_background( &mut self, ranges: Vec>, - color: Color, + color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { self.background_highlights - .insert(TypeId::of::(), (color, ranges)); + .insert(TypeId::of::(), (color_fetcher, ranges)); cx.notify(); } pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Option<(Color, Vec>)> { + ) -> Option<(fn(&Theme) -> Color, Vec>)> { cx.notify(); self.background_highlights.remove(&TypeId::of::()) } @@ -5728,23 +5669,20 @@ impl Editor { let buffer = &snapshot.buffer_snapshot; let start = buffer.anchor_before(0); let end = buffer.anchor_after(buffer.len()); - self.background_highlights_in_range(start..end, &snapshot) - } - - pub fn background_highlights_for_type(&self) -> Option<(Color, &[Range])> { - self.background_highlights - .get(&TypeId::of::()) - .map(|(color, ranges)| (*color, ranges.as_slice())) + let theme = cx.global::().theme.as_ref(); + self.background_highlights_in_range(start..end, &snapshot, theme) } pub fn background_highlights_in_range( &self, search_range: Range, display_snapshot: &DisplaySnapshot, + theme: &Theme, ) -> Vec<(Range, Color)> { let mut results = Vec::new(); let buffer = &display_snapshot.buffer_snapshot; - for (color, ranges) in self.background_highlights.values() { + for (color_fetcher, ranges) in self.background_highlights.values() { + let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { let cmp = probe.end.cmp(&search_range.start, &buffer); if cmp.is_gt() { @@ -5767,7 +5705,7 @@ impl Editor { .end .to_point(buffer) .to_display_point(display_snapshot); - results.push((start..end, *color)) + results.push((start..end, color)) } } results @@ -5935,8 +5873,6 @@ impl Editor { // and activating a new item causes the pane to call a method on us reentrantly, // which panics if we're on the stack. cx.defer(move |workspace, cx| { - workspace.activate_next_pane(cx); - for (buffer, ranges) in new_selections_by_buffer.into_iter() { let editor = workspace.open_project_item::(buffer, cx); editor.update(cx, |editor, cx| { @@ -6461,14 +6397,18 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { + use crate::test::{assert_text_with_selections, select_ranges}; + use super::*; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; + use indoc::indoc; use language::{FakeLspAdapter, LanguageConfig}; use lsp::FakeLanguageServer; use project::FakeFs; + use settings::LanguageOverride; use smol::stream::StreamExt; use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; @@ -6478,7 +6418,7 @@ mod tests { #[gpui::test] fn test_edit_events(cx: &mut MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let events = Rc::new(RefCell::new(Vec::new())); @@ -6586,7 +6526,7 @@ mod tests { #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let mut now = Instant::now(); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let group_interval = buffer.read(cx).transaction_group_interval(); @@ -6655,7 +6595,7 @@ mod tests { #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -6720,7 +6660,7 @@ mod tests { #[gpui::test] fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -6752,10 +6692,10 @@ mod tests { #[gpui::test] fn test_navigation_history(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); use workspace::Item; let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); - let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); + let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); cx.add_window(Default::default(), |cx| { let mut editor = build_editor(buffer.clone(), cx); @@ -6806,13 +6746,29 @@ mod tests { ); assert!(nav_history.borrow_mut().pop_backward().is_none()); + // Set scroll position to check later + editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx); + let original_scroll_position = editor.scroll_position; + let original_scroll_top_anchor = editor.scroll_top_anchor.clone(); + + // Jump to the end of the document and adjust scroll + editor.move_to_end(&MoveToEnd, cx); + editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx); + assert_ne!(editor.scroll_position, original_scroll_position); + assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor); + + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(editor.scroll_position, original_scroll_position); + assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor); + editor }); } #[gpui::test] fn test_cancel(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -6852,7 +6808,7 @@ mod tests { #[gpui::test] fn test_fold(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple( &" impl Foo { @@ -6937,7 +6893,7 @@ mod tests { #[gpui::test] fn test_move_cursor(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7011,7 +6967,7 @@ mod tests { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7112,7 +7068,7 @@ mod tests { #[gpui::test] fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); view.update(cx, |view, cx| { @@ -7157,7 +7113,7 @@ mod tests { #[gpui::test] fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\n def", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7228,7 +7184,12 @@ mod tests { view.update(cx, |view, cx| { view.move_left(&MoveLeft, cx); - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); assert_eq!( view.selected_display_ranges(cx), &[ @@ -7239,7 +7200,12 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); assert_eq!( view.selected_display_ranges(cx), &[ @@ -7250,7 +7216,12 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + view.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); assert_eq!( view.selected_display_ranges(cx), &[ @@ -7261,7 +7232,12 @@ mod tests { }); view.update(cx, |view, cx| { - view.select_to_end_of_line(&SelectToEndOfLine(true), cx); + view.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: true, + }, + cx, + ); assert_eq!( view.selected_display_ranges(cx), &[ @@ -7298,7 +7274,7 @@ mod tests { #[gpui::test] fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7403,7 +7379,7 @@ mod tests { #[gpui::test] fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -7456,7 +7432,7 @@ mod tests { #[gpui::test] fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one two three four", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7493,7 +7469,7 @@ mod tests { #[gpui::test] fn test_newline(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7514,7 +7490,7 @@ mod tests { #[gpui::test] fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple( " a @@ -7599,7 +7575,7 @@ mod tests { #[gpui::test] fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let (_, editor) = cx.add_window(Default::default(), |cx| { let mut editor = build_editor(buffer.clone(), cx); @@ -7626,81 +7602,226 @@ mod tests { #[gpui::test] fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); - let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); + cx.set_global(Settings::test(cx)); + let buffer = MultiBuffer::build_simple( + indoc! {" + one two + three + four"}, + cx, + ); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); view.update(cx, |view, cx| { // two selections on the same line - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), - ], + select_ranges( + view, + indoc! {" + [one] [two] + three + four"}, cx, ); // indent from mid-tabstop to full tabstop - view.tab(&Tab(Direction::Next), cx); - assert_eq!(view.text(cx), " one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), - ] + view.tab(&Tab, cx); + assert_text_with_selections( + view, + indoc! {" + [one] [two] + three + four"}, + cx, ); // outdent from 1 tabstop to 0 tabstops - view.tab(&Tab(Direction::Prev), cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - ] + view.tab_prev(&TabPrev, cx); + assert_text_with_selections( + view, + indoc! {" + [one] [two] + three + four"}, + cx, ); // select across line ending - view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); + select_ranges( + view, + indoc! {" + one two + t[hree + ] four"}, + cx, + ); // indent and outdent affect only the preceding line - view.tab(&Tab(Direction::Next), cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] + view.tab(&Tab, cx); + assert_text_with_selections( + view, + indoc! {" + one two + t[hree + ] four"}, + cx, ); - view.tab(&Tab(Direction::Prev), cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] + view.tab_prev(&TabPrev, cx); + assert_text_with_selections( + view, + indoc! {" + one two + t[hree + ] four"}, + cx, ); // Ensure that indenting/outdenting works when the cursor is at column 0. - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(&Tab(Direction::Next), cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + select_ranges( + view, + indoc! {" + one two + []three + four"}, + cx, + ); + view.tab(&Tab, cx); + assert_text_with_selections( + view, + indoc! {" + one two + []three + four"}, + cx, ); - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(&Tab(Direction::Prev), cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + select_ranges( + view, + indoc! {" + one two + [] three + four"}, + cx, + ); + view.tab_prev(&TabPrev, cx); + assert_text_with_selections( + view, + indoc! {" + one two + []three + four"}, + cx, ); }); } + #[gpui::test] + fn test_indent_outdent_with_excerpts(cx: &mut gpui::MutableAppContext) { + cx.set_global( + Settings::test(cx) + .with_overrides( + "TOML", + LanguageOverride { + tab_size: Some(2), + ..Default::default() + }, + ) + .with_overrides( + "Rust", + LanguageOverride { + tab_size: Some(4), + ..Default::default() + }, + ), + ); + let toml_language = Arc::new(Language::new( + LanguageConfig { + name: "TOML".into(), + ..Default::default() + }, + None, + )); + let rust_language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + None, + )); + + let toml_buffer = cx + .add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let rust_buffer = cx.add_model(|cx| { + Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx) + }); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + toml_buffer.clone(), + [Point::new(0, 0)..Point::new(2, 0)], + cx, + ); + multibuffer.push_excerpts( + rust_buffer.clone(), + [Point::new(0, 0)..Point::new(1, 0)], + cx, + ); + multibuffer + }); + + cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(multibuffer, cx); + + assert_eq!( + editor.text(cx), + indoc! {" + a = 1 + b = 2 + + const c: usize = 3; + "} + ); + + select_ranges( + &mut editor, + indoc! {" + [a] = 1 + b = 2 + + [const c:] usize = 3; + "}, + cx, + ); + + editor.tab(&Tab, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + [a] = 1 + b = 2 + + [const c:] usize = 3; + "}, + cx, + ); + editor.tab_prev(&TabPrev, cx); + assert_text_with_selections( + &mut editor, + indoc! {" + [a] = 1 + b = 2 + + [const c:] usize = 3; + "}, + cx, + ); + + editor + }); + } + #[gpui::test] fn test_backspace(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let (_, view) = cx.add_window(Default::default(), |cx| { build_editor(MultiBuffer::build_simple("", cx), cx) }); @@ -7745,7 +7866,7 @@ mod tests { #[gpui::test] fn test_delete(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); @@ -7773,7 +7894,7 @@ mod tests { #[gpui::test] fn test_delete_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7796,7 +7917,7 @@ mod tests { ); }); - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7812,7 +7933,7 @@ mod tests { #[gpui::test] fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7862,7 +7983,7 @@ mod tests { #[gpui::test] fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -7958,7 +8079,7 @@ mod tests { #[gpui::test] fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let snapshot = buffer.read(cx).snapshot(cx); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -7979,7 +8100,7 @@ mod tests { #[gpui::test] fn test_clipboard(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); let view = cx .add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)) @@ -8108,7 +8229,7 @@ mod tests { #[gpui::test] fn test_select_all(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -8122,7 +8243,7 @@ mod tests { #[gpui::test] fn test_select_line(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -8167,7 +8288,7 @@ mod tests { #[gpui::test] fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); view.update(cx, |view, cx| { @@ -8233,7 +8354,7 @@ mod tests { #[gpui::test] fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); @@ -8417,7 +8538,7 @@ mod tests { #[gpui::test] fn test_select_next(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]"); let buffer = MultiBuffer::build_simple(&text, cx); @@ -8425,10 +8546,20 @@ mod tests { view.update(cx, |view, cx| { view.select_ranges([ranges[1].start + 1..ranges[1].start + 1], None, cx); - view.select_next(&SelectNext(false), cx); + view.select_next( + &SelectNext { + replace_newest: false, + }, + cx, + ); assert_eq!(view.selected_ranges(cx), &ranges[1..2]); - view.select_next(&SelectNext(false), cx); + view.select_next( + &SelectNext { + replace_newest: false, + }, + cx, + ); assert_eq!(view.selected_ranges(cx), &ranges[1..3]); view.undo_selection(&UndoSelection, cx); @@ -8437,17 +8568,27 @@ mod tests { view.redo_selection(&RedoSelection, cx); assert_eq!(view.selected_ranges(cx), &ranges[1..3]); - view.select_next(&SelectNext(false), cx); + view.select_next( + &SelectNext { + replace_newest: false, + }, + cx, + ); assert_eq!(view.selected_ranges(cx), &ranges[1..4]); - view.select_next(&SelectNext(false), cx); + view.select_next( + &SelectNext { + replace_newest: false, + }, + cx, + ); assert_eq!(view.selected_ranges(cx), &ranges[0..4]); }); } #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig::default(), Some(tree_sitter_rust::language()), @@ -8588,7 +8729,7 @@ mod tests { #[gpui::test] async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new( Language::new( LanguageConfig { @@ -8645,7 +8786,7 @@ mod tests { #[gpui::test] async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ @@ -8792,7 +8933,7 @@ mod tests { #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let text = " a. b @@ -8900,7 +9041,7 @@ mod tests { #[gpui::test] async fn test_format_during_save(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let mut language = Language::new( LanguageConfig { @@ -8952,6 +9093,7 @@ mod tests { params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); + assert_eq!(params.options.tab_size, 4); Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), @@ -8988,11 +9130,39 @@ mod tests { "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); + + // Set rust language override and assert overriden tabsize is sent to language server + cx.update(|cx| { + cx.update_global::(|settings, _| { + settings.language_overrides.insert( + "Rust".into(), + LanguageOverride { + tab_size: Some(8), + ..Default::default() + }, + ); + }) + }); + + let save = cx.update(|cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + assert_eq!(params.options.tab_size, 8); + Ok(Some(vec![])) + }) + .next() + .await; + cx.foreground().start_waiting(); + save.await.unwrap(); } #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let mut language = Language::new( LanguageConfig { @@ -9066,7 +9236,7 @@ mod tests { let apply_additional_edits = editor.update(cx, |editor, cx| { editor.move_down(&MoveDown, cx); let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion(None), cx) + .confirm_completion(&ConfirmCompletion::default(), cx) .unwrap(); assert_eq!( editor.text(cx), @@ -9149,7 +9319,7 @@ mod tests { let apply_additional_edits = editor.update(cx, |editor, cx| { let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion(None), cx) + .confirm_completion(&ConfirmCompletion::default(), cx) .unwrap(); assert_eq!( editor.text(cx), @@ -9233,7 +9403,7 @@ mod tests { #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".to_string()), @@ -9313,7 +9483,7 @@ mod tests { #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -9356,7 +9526,7 @@ mod tests { #[gpui::test] fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -9411,7 +9581,7 @@ mod tests { #[gpui::test] fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -9489,7 +9659,7 @@ mod tests { #[gpui::test] fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) { - populate_settings(cx); + cx.set_global(Settings::test(cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -9543,7 +9713,7 @@ mod tests { #[gpui::test] async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { - cx.update(populate_settings); + cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ @@ -9611,7 +9781,8 @@ mod tests { #[gpui::test] fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - populate_settings(cx); + + cx.set_global(Settings::test(cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); editor.update(cx, |editor, cx| { @@ -9631,7 +9802,7 @@ mod tests { anchor_range(Point::new(6, 3)..Point::new(6, 5)), anchor_range(Point::new(8, 4)..Point::new(8, 6)), ], - Color::red(), + |_| Color::red(), cx, ); editor.highlight_background::( @@ -9641,7 +9812,7 @@ mod tests { anchor_range(Point::new(7, 4)..Point::new(7, 7)), anchor_range(Point::new(9, 5)..Point::new(9, 8)), ], - Color::green(), + |_| Color::green(), cx, ); @@ -9649,6 +9820,7 @@ mod tests { let mut highlighted_ranges = editor.background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, + cx.global::().theme.as_ref(), ); // Enforce a consistent ordering based on color without relying on the ordering of the // highlight's `TypeId` which is non-deterministic. @@ -9678,6 +9850,7 @@ mod tests { editor.background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, + cx.global::().theme.as_ref(), ), &[( DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), @@ -9690,7 +9863,8 @@ mod tests { #[gpui::test] fn test_following(cx: &mut gpui::MutableAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - populate_settings(cx); + + cx.set_global(Settings::test(cx)); let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, follower) = cx.add_window( @@ -9857,11 +10031,6 @@ mod tests { Editor::new(EditorMode::Full, buffer, None, None, cx) } - fn populate_settings(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); - } - fn assert_selection_ranges( marked_text: &str, selection_marker_pairs: Vec<(char, char)>, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 18f780dacc..cd53e62964 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -22,6 +22,7 @@ use gpui::{ }; use json::json; use language::{Bias, DiagnosticSeverity}; +use settings::Settings; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, @@ -917,9 +918,11 @@ impl Element for EditorElement { let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx)); highlighted_rows = view.highlighted_rows(); + let theme = cx.global::().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) { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 67d5aee773..9fe6e21b71 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -8,12 +8,11 @@ use gpui::{ use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use project::{File, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; +use settings::Settings; use std::{fmt::Write, path::PathBuf, time::Duration}; use text::{Point, Selection}; use util::TryFutureExt; -use workspace::{ - FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, -}; +use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView}; pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); @@ -248,18 +247,27 @@ impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Some(data) = data.downcast_ref::() { 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::(&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 diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index cf2d772b16..d5f0f480fb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -268,9 +268,11 @@ mod tests { use super::*; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer}; use language::Point; + use settings::Settings; #[gpui::test] fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -297,6 +299,7 @@ mod tests { #[gpui::test] fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -330,6 +333,7 @@ mod tests { #[gpui::test] fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert( marked_text: &str, cx: &mut gpui::MutableAppContext, @@ -361,6 +365,7 @@ mod tests { #[gpui::test] fn test_next_word_end(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -384,6 +389,7 @@ mod tests { #[gpui::test] fn test_next_subword_end(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -416,6 +422,7 @@ mod tests { #[gpui::test] fn test_find_boundary(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert( marked_text: &str, cx: &mut gpui::MutableAppContext, @@ -447,6 +454,7 @@ mod tests { #[gpui::test] fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -467,6 +475,7 @@ mod tests { #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) { + cx.set_global(Settings::test(cx)); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let font_id = cx .font_cache() @@ -487,7 +496,7 @@ mod tests { multibuffer }); let display_map = - cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx)); + cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c07df895c9..ae061196bd 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -11,6 +11,7 @@ use language::{ Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; +use settings::Settings; use std::{ cell::{Ref, RefCell}, cmp, fmt, io, @@ -297,8 +298,10 @@ impl MultiBuffer { .into_iter() .map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot)); return buffer.update(cx, |buffer, cx| { + let language_name = buffer.language().map(|language| language.name()); + let indent_size = cx.global::().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::().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, usize)> { + let snapshot = self.read(cx); + let offset = point.to_offset(&snapshot); + let mut cursor = snapshot.excerpts.cursor::(); + 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, @@ -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> { - 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> { + 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::::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)); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index e80547c9dd..eb23d7e15f 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,8 +1,9 @@ -use util::test::marked_text; +use gpui::ViewContext; +use util::test::{marked_text, marked_text_ranges}; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, - DisplayPoint, MultiBuffer, + DisplayPoint, Editor, MultiBuffer, }; #[cfg(test)] @@ -20,7 +21,6 @@ pub fn marked_display_snapshot( ) -> (DisplaySnapshot, Vec) { 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) { + 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, +) { + 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); +} diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index b946ea48fb..cb85183ef0 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -11,7 +11,9 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } project = { path = "../project" } +settings = { path = "../settings" } util = { path = "../util" } theme = { path = "../theme" } workspace = { path = "../workspace" } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 56fd255d82..b433193456 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,15 +1,12 @@ -use editor::Editor; use fuzzy::PathMatch; use gpui::{ - action, - elements::*, - keymap::{self, Binding}, - AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, }; +use picker::{Picker, PickerDelegate}; use project::{Project, ProjectPath, WorktreeId}; +use settings::Settings; use std::{ - cmp, path::Path, sync::{ atomic::{self, AtomicBool}, @@ -17,15 +14,11 @@ use std::{ }, }; use util::post_inc; -use workspace::{ - menu::{Confirm, SelectNext, SelectPrev}, - Settings, Workspace, -}; +use workspace::Workspace; pub struct FileFinder { - handle: WeakViewHandle, project: ModelHandle, - query_editor: ViewHandle, + picker: ViewHandle>, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, @@ -33,23 +26,13 @@ pub struct FileFinder { matches: Vec, selected: Option<(usize, Arc)>, cancel_flag: Arc, - 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::::init(cx); } pub enum Event { @@ -66,140 +49,16 @@ impl View for FileFinder { "FileFinder" } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.global::(); - 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) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - 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::(); - 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::(); - 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, String, Vec) { 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, cx: &mut ViewContext) -> 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, cx: &mut ViewContext) { - 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 on_query_editor_event( + fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> 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 set_matches( &mut self, - _: ViewHandle, - event: &editor::Event, + search_id: usize, + did_cancel: bool, + query: String, + matches: Vec, cx: &mut ViewContext, ) { - 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) { - 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) { + 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) { - 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) -> 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) { + fn confirm(&mut self, cx: &mut ViewContext) { 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) { - cx.emit(Event::Selected(project_path.clone())); + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); } - #[must_use] - fn spawn_search(&mut self, query: String, cx: &mut ViewContext) -> Option> { - 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), - cx: &mut ViewContext, - ) { - 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::(); + 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::() .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); }); } diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index eaad41e080..76744274c7 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -11,5 +11,6 @@ doctest = false text = { path = "../text" } editor = { path = "../editor" } gpui = { path = "../gpui" } +settings = { path = "../settings" } workspace = { path = "../workspace" } postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 109d33097d..16b633ad72 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,20 +1,15 @@ use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor}; use gpui::{ - action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, - MutableAppContext, RenderContext, View, ViewContext, ViewHandle, + actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext, + RenderContext, View, ViewContext, ViewHandle, }; +use settings::Settings; use text::{Bias, Point}; -use workspace::{Settings, Workspace}; +use workspace::Workspace; -action!(Toggle); -action!(Confirm); +actions!(go_to_line, [Toggle, Confirm]); pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("ctrl-g", Toggle, Some("Editor")), - Binding::new("escape", Toggle, Some("GoToLine")), - Binding::new("enter", Confirm, Some("GoToLine")), - ]); cx.add_action(GoToLine::toggle); cx.add_action(GoToLine::confirm); } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 9973ac6549..442a2b5b2f 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -25,7 +25,7 @@ etagere = "0.2" futures = "0.3" image = "0.23" lazy_static = "1.4.0" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } num_cpus = "1.13" ordered-float = "2.1.1" parking = "2.0.0" @@ -67,6 +67,6 @@ core-graphics = "0.22.2" core-text = "19.2" font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" } foreign-types = "0.3" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } metal = "0.21.0" objc = "0.2" diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index cc849a5e62..74e2c2c2ea 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -104,6 +104,7 @@ impl gpui::Element for TextElement { &mut self, _: &gpui::Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut gpui::EventContext, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7f0c3ffcde..37cf03a3cc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,19 +1,23 @@ +pub mod action; + use crate::{ elements::ElementBox, executor::{self, Task}, - keymap::{self, Keystroke}, + keymap::{self, Binding, Keystroke}, platform::{self, CursorStyle, Platform, PromptLevel, WindowOptions}, presenter::Presenter, util::post_inc, AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; -use anyhow::{anyhow, Result}; +pub use action::*; +use anyhow::{anyhow, Context, Result}; use collections::btree_map; use keymap::MatchResult; use lazy_static::lazy_static; use parking_lot::Mutex; use platform::Event; use postage::oneshot; +use smallvec::SmallVec; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, @@ -59,6 +63,9 @@ pub trait View: Entity + Sized { cx.set.insert(Self::ui_name().into()); cx } + fn debug_json(&self, _: &AppContext) -> serde_json::Value { + serde_json::Value::Null + } } pub trait ReadModel { @@ -142,89 +149,6 @@ pub trait ElementStateContext: DerefMut { } } -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; - fn boxed_clone_as_any(&self) -> Box; -} - -#[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 { - Box::new(self.clone()) - } - - fn boxed_clone_as_any(&self) -> Box { - 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 { - Box::new(self.clone()) - } - - fn boxed_clone_as_any(&self) -> Box { - Box::new(self.clone()) - } - } - }; -} - pub struct Menu<'a> { pub name: &'a str, pub items: Vec>, @@ -234,7 +158,7 @@ pub enum MenuItem<'a> { Action { name: &'a str, keystroke: Option<&'a str>, - action: Box, + action: Box, }, Separator, } @@ -324,7 +248,7 @@ impl App { self } - pub fn on_quit(self, mut callback: F) -> Self + pub fn on_quit(&mut self, mut callback: F) -> &mut Self where F: 'static + FnMut(&mut MutableAppContext), { @@ -336,7 +260,7 @@ impl App { self } - pub fn on_event(self, mut callback: F) -> Self + pub fn on_event(&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(self, mut callback: F) -> Self + pub fn on_open_urls(&mut self, mut callback: F) -> &mut Self where - F: 'static + FnMut(Vec, &mut MutableAppContext), + F: 'static + FnMut(Vec, &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( - &self, - window_id: usize, - responder_chain: Vec, - action: A, - ) { - self.cx - .borrow_mut() - .dispatch_action_any(window_id, &responder_chain, &action); + pub fn dispatch_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(&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> { 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 bool>; type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; +type FocusObservationCallback = Box bool>; type GlobalObservationCallback = Box; -type ReleaseObservationCallback = Box; +type ReleaseObservationCallback = Box; +type DeserializeActionCallback = fn(json: &str) -> anyhow::Result>; pub struct MutableAppContext { weak_self: Option>>, foreground_platform: Rc, assets: Arc, cx: AppContext, + action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>, capture_actions: HashMap>>>, actions: HashMap>>>, global_actions: HashMap>, @@ -802,6 +740,8 @@ pub struct MutableAppContext { global_subscriptions: Arc>>>>, observations: Arc>>>>, + focus_observations: + Arc>>>>, global_observations: Arc>>>>, release_observations: Arc>>>, @@ -809,6 +749,7 @@ pub struct MutableAppContext { HashMap>, Box)>, foreground: Rc, pending_effects: VecDeque, + pending_focus_index: Option, pending_notifications: HashSet, pending_global_notifications: HashSet, 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> { + 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(&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::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::from_json_str)); + if self .global_actions .insert(TypeId::of::(), handler) .is_some() { - panic!("registered multiple global handlers for the same action type"); + panic!( + "registered multiple global handlers for {}", + type_name::() + ); } } @@ -1222,6 +1191,32 @@ impl MutableAppContext { } } + fn observe_focus(&mut self, handle: &ViewHandle, mut callback: F) -> Subscription + where + F: 'static + FnMut(ViewHandle, &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(&mut self, mut observe: F) -> Subscription where G: Any, @@ -1250,12 +1245,12 @@ impl MutableAppContext { } } - pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription + pub fn observe_release(&mut self, handle: &H, callback: F) -> Subscription where E: Entity, E::Event: 'static, H: Handle, - 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, 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( &mut self, window_id: usize, - responder_chain: Vec, + dispatch_path: Vec, 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, + dispatch_path: Vec, 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) { + fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option) { + 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) { + 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(&self, f: F) -> Task where F: FnOnce(AsyncAppContext) -> Fut, @@ -2316,6 +2439,12 @@ pub struct AppContext { } impl AppContext { + pub(crate) fn root_view(&self, window_id: usize) -> Option { + self.windows + .get(&window_id) + .map(|window| window.root_view.clone()) + } + pub fn root_view_id(&self, window_id: usize) -> Option { self.windows .get(&window_id) @@ -2345,11 +2474,11 @@ impl AppContext { } pub fn global(&self) -> &T { - self.globals - .get(&TypeId::of::()) - .expect("no app state has been added for this type") - .downcast_ref() - .unwrap() + if let Some(global) = self.globals.get(&TypeId::of::()) { + global.downcast_ref().unwrap() + } else { + panic!("no global has been added for {}", type_name::()); + } } } @@ -2495,6 +2624,11 @@ pub enum Effect { window_id: usize, view_id: Option, }, + 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 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, { 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(&mut self, build_model: F) -> ModelHandle @@ -3057,6 +3196,24 @@ impl<'a, T: View> ViewContext<'a, T> { }) } + pub fn observe_focus(&mut self, handle: &ViewHandle, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut T, ViewHandle, &mut ViewContext), + 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(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -3301,6 +3458,10 @@ pub trait Handle { 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 ModelHandle { #[cfg(any(test, feature = "test-support"))] pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { - 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 ModelHandle { }; 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 ModelHandle { 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 ModelHandle { 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 ModelHandle { cx: &TestAppContext, mut predicate: impl FnMut(&T, &AppContext) -> bool, ) -> impl Future { - 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 ModelHandle { } 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 { model_type: PhantomData, } +impl WeakHandle for WeakModelHandle { + fn id(&self) -> usize { + self.model_id + } +} + unsafe impl Send for WeakModelHandle {} unsafe impl Sync for WeakModelHandle {} @@ -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 { view_type: PhantomData, } +impl WeakHandle for WeakViewHandle { + fn id(&self) -> usize { + self.view_id + } +} + impl WeakViewHandle { fn new(window_id: usize, view_id: usize) -> Self { Self { @@ -4304,6 +4478,12 @@ pub enum Subscription { Weak>>>>, >, }, + FocusObservation { + id: usize, + view_id: usize, + observations: + Option>>>>>, + }, ReleaseObservation { id: usize, entity_id: usize, @@ -4330,6 +4510,9 @@ impl Subscription { Subscription::ReleaseObservation { observations, .. } => { observations.take(); } + Subscription::FocusObservation { observations, .. } => { + observations.take(); + } } } } @@ -4422,6 +4605,22 @@ impl Drop for Subscription { } } } + Subscription::FocusObservation { + id, + view_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + match observations.lock().entry(*view_id).or_default().entry(*id) { + btree_map::Entry::Vacant(entry) => { + entry.insert(None); + } + btree_map::Entry::Occupied(entry) => { + entry.remove(); + } + } + } + } } } } @@ -4470,11 +4669,36 @@ impl LeakDetector { } } + pub fn assert_dropped(&mut self, entity_id: usize) { + if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) { + for trace in backtraces.values_mut() { + if let Some(trace) = trace { + trace.resolve(); + eprintln!("{:?}", crate::util::CwdBacktrace(trace)); + } + } + + let hint = if *LEAK_BACKTRACE { + "" + } else { + " – set LEAK_BACKTRACE=1 for more information" + }; + + panic!( + "{} handles to {} {} still exist{}", + backtraces.len(), + type_name.unwrap_or("entity"), + entity_id, + hint + ); + } + } + pub fn detect(&mut self) { let mut found_leaks = false; for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() { eprintln!( - "leaked {} handles to {:?} {}", + "leaked {} handles to {} {}", backtraces.len(), type_name.unwrap_or("entity"), id @@ -4606,7 +4830,8 @@ impl RefCounts { #[cfg(test)] mod tests { use super::*; - use crate::elements::*; + use crate::{actions, elements::*, impl_actions}; + use serde::Deserialize; use smol::future::poll_once; use std::{ cell::Cell, @@ -5684,23 +5909,56 @@ mod tests { } } - let events: Arc>> = Default::default(); + let view_events: Arc>> = Default::default(); let (window_id, view_1) = cx.add_window(Default::default(), |_| View { - events: events.clone(), + events: view_events.clone(), name: "view 1".to_string(), }); let view_2 = cx.add_view(window_id, |_| View { - events: events.clone(), + events: view_events.clone(), name: "view 2".to_string(), }); - view_1.update(cx, |_, cx| cx.focus(&view_2)); + let observed_events: Arc>> = Default::default(); + view_1.update(cx, |_, cx| { + cx.observe_focus(&view_2, { + let observed_events = observed_events.clone(); + move |this, view, cx| { + observed_events.lock().push(format!( + "{} observed {}'s focus", + this.name, + view.read(cx).name + )) + } + }) + .detach(); + }); + view_2.update(cx, |_, cx| { + cx.observe_focus(&view_1, { + let observed_events = observed_events.clone(); + move |this, view, cx| { + observed_events.lock().push(format!( + "{} observed {}'s focus", + this.name, + view.read(cx).name + )) + } + }) + .detach(); + }); + + view_1.update(cx, |_, cx| { + // Ensure only the latest focus is honored. + cx.focus(&view_2); + cx.focus(&view_1); + cx.focus(&view_2); + }); view_1.update(cx, |_, cx| cx.focus(&view_1)); view_1.update(cx, |_, cx| cx.focus(&view_2)); view_1.update(cx, |_, _| drop(view_2)); assert_eq!( - *events.lock(), + *view_events.lock(), [ "view 1 focused".to_string(), "view 1 blurred".to_string(), @@ -5712,6 +5970,50 @@ mod tests { "view 1 focused".to_string(), ], ); + assert_eq!( + *observed_events.lock(), + [ + "view 1 observed view 2's focus".to_string(), + "view 2 observed view 1's focus".to_string(), + "view 1 observed view 2's focus".to_string(), + ] + ); + } + + #[crate::test(self)] + fn test_deserialize_actions(cx: &mut MutableAppContext) { + #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] + pub struct ComplexAction { + arg: String, + count: usize, + } + + actions!(test::something, [SimpleAction]); + impl_actions!(test::something, [ComplexAction]); + + cx.add_global_action(move |_: &SimpleAction, _: &mut MutableAppContext| {}); + cx.add_global_action(move |_: &ComplexAction, _: &mut MutableAppContext| {}); + + let action1 = cx + .deserialize_action( + "test::something::ComplexAction", + Some(r#"{"arg": "a", "count": 5}"#), + ) + .unwrap(); + let action2 = cx + .deserialize_action("test::something::SimpleAction", None) + .unwrap(); + assert_eq!( + action1.as_any().downcast_ref::().unwrap(), + &ComplexAction { + arg: "a".to_string(), + count: 5, + } + ); + assert_eq!( + action2.as_any().downcast_ref::().unwrap(), + &SimpleAction + ); } #[crate::test(self)] @@ -5752,29 +6054,32 @@ mod tests { } } - action!(Action, &'static str); + #[derive(Clone, Deserialize)] + pub struct Action(pub String); + + impl_actions!(test, [Action]); let actions = Rc::new(RefCell::new(Vec::new())); - { + cx.add_global_action({ let actions = actions.clone(); - cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { + move |_: &Action, _: &mut MutableAppContext| { actions.borrow_mut().push("global".to_string()); - }); - } + } + }); - { + cx.add_action({ let actions = actions.clone(); - cx.add_action(move |view: &mut ViewA, action: &Action, cx| { + move |view: &mut ViewA, action: &Action, cx| { assert_eq!(action.0, "bar"); cx.propagate_action(); actions.borrow_mut().push(format!("{} a", view.id)); - }); - } + } + }); - { + cx.add_action({ let actions = actions.clone(); - cx.add_action(move |view: &mut ViewA, _: &Action, cx| { + move |view: &mut ViewA, _: &Action, cx| { if view.id != 1 { cx.add_view(|cx| { cx.propagate_action(); // Still works on a nested ViewContext @@ -5782,32 +6087,32 @@ mod tests { }); } actions.borrow_mut().push(format!("{} b", view.id)); - }); - } + } + }); - { + cx.add_action({ let actions = actions.clone(); - cx.add_action(move |view: &mut ViewB, _: &Action, cx| { + move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); actions.borrow_mut().push(format!("{} c", view.id)); - }); - } + } + }); - { + cx.add_action({ let actions = actions.clone(); - cx.add_action(move |view: &mut ViewB, _: &Action, cx| { + move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); actions.borrow_mut().push(format!("{} d", view.id)); - }); - } + } + }); - { + cx.capture_action({ let actions = actions.clone(); - cx.capture_action(move |view: &mut ViewA, _: &Action, cx| { + move |view: &mut ViewA, _: &Action, cx| { cx.propagate_action(); actions.borrow_mut().push(format!("{} capture", view.id)); - }); - } + } + }); let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 }); let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 }); @@ -5817,7 +6122,7 @@ mod tests { cx.dispatch_action( window_id, vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()], - &Action("bar"), + &Action("bar".to_string()), ); assert_eq!( @@ -5840,7 +6145,7 @@ mod tests { cx.dispatch_action( window_id, vec![view_2.id(), view_3.id(), view_4.id()], - &Action("bar"), + &Action("bar".to_string()), ); assert_eq!( @@ -5860,7 +6165,10 @@ mod tests { #[crate::test(self)] fn test_dispatch_keystroke(cx: &mut MutableAppContext) { - action!(Action, &'static str); + #[derive(Clone, Deserialize)] + pub struct Action(String); + + impl_actions!(test, [Action]); struct View { id: usize, @@ -5912,16 +6220,20 @@ mod tests { // "a" and "b" in its context, but not "c". cx.add_bindings(vec![keymap::Binding::new( "a", - Action("a"), + Action("a".to_string()), Some("a && b && !c"), )]); - cx.add_bindings(vec![keymap::Binding::new("b", Action("b"), None)]); + cx.add_bindings(vec![keymap::Binding::new( + "b", + Action("b".to_string()), + None, + )]); let actions = Rc::new(RefCell::new(Vec::new())); - { + cx.add_action({ let actions = actions.clone(); - cx.add_action(move |view: &mut View, action: &Action, cx| { + move |view: &mut View, action: &Action, cx| { if action.0 == "a" { actions.borrow_mut().push(format!("{} a", view.id)); } else { @@ -5930,14 +6242,15 @@ mod tests { .push(format!("{} {}", view.id, action.0)); cx.propagate_action(); } - }); - } - { + } + }); + + cx.add_global_action({ let actions = actions.clone(); - cx.add_global_action(move |action: &Action, _| { + move |action: &Action, _| { actions.borrow_mut().push(format!("global {}", action.0)); - }); - } + } + }); cx.dispatch_keystroke( window_id, diff --git a/crates/gpui/src/app/action.rs b/crates/gpui/src/app/action.rs new file mode 100644 index 0000000000..fc5bd616ee --- /dev/null +++ b/crates/gpui/src/app/action.rs @@ -0,0 +1,109 @@ +use std::any::{Any, TypeId}; + +pub trait Action: 'static { + fn id(&self) -> TypeId; + fn name(&self) -> &'static str; + fn as_any(&self) -> &dyn Any; + fn boxed_clone(&self) -> Box; + + fn qualified_name() -> &'static str + where + Self: Sized; + fn from_json_str(json: &str) -> anyhow::Result> + 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> { + 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> { + Ok(Box::new($crate::serde_json::from_str::(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> { + 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 { + Box::new(self.clone()) + } + + $from_json_fn + } + }; +} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 79112863b5..55c7bf22fe 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -74,6 +74,7 @@ pub trait Element { &mut self, event: &Event, bounds: RectF, + visible_bounds: RectF, layout: &mut Self::LayoutState, paint: &mut Self::PaintState, cx: &mut EventContext, @@ -169,6 +170,7 @@ pub enum Lifecycle { element: T, constraint: SizeConstraint, bounds: RectF, + visible_bounds: RectF, layout: T::LayoutState, paint: T::PaintState, }, @@ -222,6 +224,7 @@ impl AnyElement for Lifecycle { element, constraint, bounds, + visible_bounds, layout, paint, } @@ -242,6 +245,7 @@ impl AnyElement for Lifecycle { element, constraint, bounds, + visible_bounds, layout, paint, } @@ -254,12 +258,13 @@ impl AnyElement for Lifecycle { 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 AnyElement for Lifecycle { element, constraint, bounds, + visible_bounds, layout, paint, } => { @@ -299,6 +305,8 @@ impl AnyElement for Lifecycle { 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 { diff --git a/crates/gpui/src/elements/align.rs b/crates/gpui/src/elements/align.rs index ce99437f3b..5388f7647e 100644 --- a/crates/gpui/src/elements/align.rs +++ b/crates/gpui/src/elements/align.rs @@ -85,7 +85,8 @@ impl Element for Align { fn dispatch_event( &mut self, event: &Event, - _: pathfinder_geometry::rect::RectF, + _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index d6220fd45f..2e10c59049 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -59,6 +59,7 @@ where &mut self, _: &crate::Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut crate::EventContext, diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index c712d71a9b..f12ed6900a 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -81,6 +81,7 @@ impl Element for ConstrainedBox { &mut self, event: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 73a4349ba0..711ab1f6a6 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -247,6 +247,7 @@ impl Element for Container { &mut self, event: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/empty.rs b/crates/gpui/src/elements/empty.rs index d6041bd958..90b2123163 100644 --- a/crates/gpui/src/elements/empty.rs +++ b/crates/gpui/src/elements/empty.rs @@ -52,6 +52,7 @@ impl Element for Empty { &mut self, _: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut EventContext, diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 469bebee1a..340d892d22 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -84,13 +84,14 @@ impl Element for EventHandler { fn dispatch_event( &mut self, event: &Event, - bounds: RectF, + _: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, ) -> bool { if let Some(capture) = self.capture.as_mut() { - if capture(event, bounds, cx) { + if capture(event, visible_bounds, cx) { return true; } } @@ -101,7 +102,7 @@ impl Element for EventHandler { match event { Event::LeftMouseDown { position, .. } => { if let Some(callback) = self.mouse_down.as_mut() { - if bounds.contains_point(*position) { + if visible_bounds.contains_point(*position) { return callback(cx); } } @@ -109,7 +110,7 @@ impl Element for EventHandler { } Event::RightMouseDown { position, .. } => { if let Some(callback) = self.right_mouse_down.as_mut() { - if bounds.contains_point(*position) { + if visible_bounds.contains_point(*position) { return callback(cx); } } @@ -121,7 +122,7 @@ impl Element for EventHandler { .. } => { if let Some(callback) = self.navigate_mouse_down.as_mut() { - if bounds.contains_point(*position) { + if visible_bounds.contains_point(*position) { return callback(*direction, cx); } } diff --git a/crates/gpui/src/elements/expanded.rs b/crates/gpui/src/elements/expanded.rs index cbeef598da..6f69d8a92a 100644 --- a/crates/gpui/src/elements/expanded.rs +++ b/crates/gpui/src/elements/expanded.rs @@ -66,6 +66,7 @@ impl Element for Expanded { &mut self, event: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 2ec307bbc3..4f930dfb46 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -2,8 +2,8 @@ use std::{any::Any, f32::INFINITY}; use crate::{ json::{self, ToJson, Value}, - Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, Vector2FExt, + Axis, DebugContext, Element, ElementBox, ElementStateContext, ElementStateHandle, Event, + EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; use pathfinder_geometry::{ rect::RectF, @@ -11,9 +11,16 @@ use pathfinder_geometry::{ }; use serde_json::json; +#[derive(Default)] +struct ScrollState { + scroll_to: Option, + scroll_position: f32, +} + pub struct Flex { axis: Axis, children: Vec, + scroll_state: Option>, } 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( + mut self, + element_id: usize, + scroll_to: Option, + cx: &mut C, + ) -> Self + where + Tag: 'static, + C: ElementStateContext, + { + let scroll_state = cx.element_state::(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::() { @@ -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, diff --git a/crates/gpui/src/elements/hook.rs b/crates/gpui/src/elements/hook.rs index 994d5fe281..e947c3bac7 100644 --- a/crates/gpui/src/elements/hook.rs +++ b/crates/gpui/src/elements/hook.rs @@ -57,6 +57,7 @@ impl Element for Hook { &mut self, event: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index 5d36828d0c..6b55b567b4 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -81,6 +81,7 @@ impl Element for Image { &mut self, _: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut EventContext, diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index 7aa46f1199..e6ae9cbd51 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -166,6 +166,7 @@ impl Element for Label { &mut self, _: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut EventContext, diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 624ec4ba71..77d37bc3bf 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -253,6 +253,7 @@ impl Element for List { &mut self, event: &Event, bounds: RectF, + _: RectF, scroll_top: &mut ListOffset, _: &mut (), cx: &mut EventContext, @@ -872,6 +873,7 @@ mod tests { &mut self, _: &Event, _: RectF, + _: RectF, _: &mut (), _: &mut (), _: &mut EventContext, diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index a9cfd3334e..1d1e934d03 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -99,7 +99,8 @@ impl Element for MouseEventHandler { fn dispatch_event( &mut self, event: &Event, - bounds: RectF, + _: RectF, + visible_bounds: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, @@ -112,8 +113,8 @@ impl Element for MouseEventHandler { let handled_in_child = self.child.dispatch_event(event, cx); let hit_bounds = RectF::from_points( - bounds.origin() - vec2f(self.padding.left, self.padding.top), - bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom), + visible_bounds.origin() - vec2f(self.padding.left, self.padding.top), + visible_bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom), ) .round_out(); diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 79ab71c07d..0cac2ed863 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -44,6 +44,7 @@ impl Element for Overlay { &mut self, event: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/stack.rs b/crates/gpui/src/elements/stack.rs index dd36b9c4b5..4531734085 100644 --- a/crates/gpui/src/elements/stack.rs +++ b/crates/gpui/src/elements/stack.rs @@ -51,6 +51,7 @@ impl Element for Stack { &mut self, event: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, cx: &mut EventContext, diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index 3e93d3adae..d473e1f0fb 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -76,6 +76,7 @@ impl Element for Svg { &mut self, _: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut EventContext, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 5caa60c0d7..a0c3f6ba9f 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -172,6 +172,7 @@ impl Element for Text { &mut self, _: &Event, _: RectF, + _: RectF, _: &mut Self::LayoutState, _: &mut Self::PaintState, _: &mut EventContext, diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 4fbb9ca420..526a9aea40 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -8,11 +8,10 @@ use crate::{ ElementBox, }; use json::ToJson; -use parking_lot::Mutex; -use std::{cmp, ops::Range, sync::Arc}; +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; #[derive(Clone, Default)] -pub struct UniformListState(Arc>); +pub struct UniformListState(Rc>); #[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, diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2c3a8df8a1..24ab663071 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Result}; use async_task::Runnable; +use futures::channel::mpsc; use smol::{channel, prelude::*, Executor}; use std::{ any::Any, @@ -621,17 +622,13 @@ impl Background { Err(async { *future.await.downcast().unwrap() }) } - pub async fn scoped<'scope, F>(&self, scheduler: F) + pub async fn scoped<'scope, F>(self: &Arc, 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::>(); @@ -668,25 +665,56 @@ impl Background { } pub struct Scope<'a> { + executor: Arc, futures: Vec + Send + 'static>>>, + tx: Option>, + rx: mpsc::Receiver<()>, _phantom: PhantomData<&'a ()>, } impl<'a> Scope<'a> { + fn new(executor: Arc) -> Self { + let (tx, rx) = mpsc::channel(1); + Self { + executor, + tx: Some(tx), + rx, + futures: Default::default(), + _phantom: PhantomData, + } + } + pub fn spawn(&mut self, f: F) where F: Future + 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 + Send + 'a>>, Pin + 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 Task { pub fn ready(value: T) -> Self { Self::Ready(Some(value)) diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 9803a2aa2f..e58bbec1c6 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -33,3 +33,6 @@ pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, Prom pub use presenter::{ Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; + +pub use anyhow; +pub use serde_json; diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 37223d77d1..c42fbff907 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -1,13 +1,13 @@ -use anyhow::anyhow; +use crate::Action; +use anyhow::{anyhow, Result}; +use smallvec::SmallVec; use std::{ - any::Any, + any::{Any, TypeId}, collections::{HashMap, HashSet}, fmt::Debug, }; use tree_sitter::{Language, Node, Parser}; -use crate::{Action, AnyAction}; - extern "C" { fn tree_sitter_context_predicate() -> Language; } @@ -24,11 +24,14 @@ struct Pending { } #[derive(Default)] -pub struct Keymap(Vec); +pub struct Keymap { + bindings: Vec, + binding_indices_by_action_type: HashMap>, +} pub struct Binding { keystrokes: Vec, - action: Box, + action: Box, context: Option, } @@ -73,7 +76,7 @@ where pub enum MatchResult { None, Pending, - Action(Box), + Action(Box), } 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 { + 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) -> 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 { + self.binding_indices_by_action_type + .get(&action_type) + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|ix| &self.bindings[*ix]) } fn add_bindings>(&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(keystrokes: &str, action: A, context: Option<&str>) -> Self { + Self::load(keystrokes, Box::new(action), context).unwrap() + } + + pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { 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::>()?; + + 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", 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::("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", 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", 1, &ctx_b), None); - assert_eq!(matcher.test_keystroke::("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>) -> Option<&'a A> { + action + .as_ref() + .and_then(|action| action.as_any().downcast_ref()) + } + impl Matcher { - fn test_keystroke(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option - where - A: Action + Debug + Eq, - { + fn test_keystroke( + &mut self, + keystroke: &str, + view_id: usize, + cx: &Context, + ) -> Option> { 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 } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 284be36205..7e67d8b752 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -15,7 +15,7 @@ use crate::{ vector::Vector2F, }, text_layout::{LineLayout, RunStyle}, - AnyAction, ClipboardItem, Menu, Scene, + Action, ClipboardItem, Menu, Scene, }; use anyhow::{anyhow, Result}; use async_task::Runnable; @@ -56,7 +56,7 @@ pub trait Platform: Send + Sync { fn local_timezone(&self) -> UtcOffset; - fn path_for_resource(&self, name: Option<&str>, extension: Option<&str>) -> Result; + fn path_for_auxiliary_executable(&self, name: &str) -> Result; fn app_path(&self) -> Result; fn app_version(&self) -> Result; } @@ -66,10 +66,10 @@ pub(crate) trait ForegroundPlatform { fn on_resign_active(&self, callback: Box); fn on_quit(&self, callback: Box); fn on_event(&self, callback: Box bool>); - fn on_open_files(&self, callback: Box)>); + fn on_open_urls(&self, callback: Box)>); fn run(&self, on_finish_launching: Box ()>); - fn on_menu_command(&self, callback: Box); + fn on_menu_command(&self, callback: Box); fn set_menus(&self, menus: Vec); 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>]) -> anyhow::Result<()>; fn load_family(&self, name: &str) -> anyhow::Result>; @@ -183,6 +189,7 @@ pub trait FontSystem: Send + Sync { glyph_id: GlyphId, subpixel_shift: Vector2F, scale_factor: f32, + options: RasterizationOptions, ) -> Option<(RectI, Vec)>; 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; diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 72e1c24d6b..fe353fed4c 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -61,3 +61,20 @@ pub enum Event { left_mouse_down: bool, }, } + +impl Event { + pub fn position(&self) -> Option { + 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), + } + } +} diff --git a/crates/gpui/src/platform/mac/atlas.rs b/crates/gpui/src/platform/mac/atlas.rs index 5e87474bf1..a7a4de1000 100644 --- a/crates/gpui/src/platform/mac/atlas.rs +++ b/crates/gpui/src/platform/mac/atlas.rs @@ -4,6 +4,7 @@ use crate::geometry::{ }; use etagere::BucketedAtlasAllocator; use foreign_types::ForeignType; +use log::warn; use metal::{Device, TextureDescriptor}; use objc::{msg_send, sel, sel_impl}; @@ -40,31 +41,40 @@ impl AtlasAllocator { ) } - pub fn allocate(&mut self, requested_size: Vector2I) -> (AllocId, Vector2I) { - let (alloc_id, origin) = self + pub fn allocate(&mut self, requested_size: Vector2I) -> Option<(AllocId, Vector2I)> { + let allocation = self .atlases .last_mut() .unwrap() .allocate(requested_size) - .unwrap_or_else(|| { + .or_else(|| { let mut atlas = self.new_atlas(requested_size); - let (id, origin) = atlas.allocate(requested_size).unwrap(); + let (id, origin) = atlas.allocate(requested_size)?; self.atlases.push(atlas); - (id, origin) + Some((id, origin)) }); + if allocation.is_none() { + warn!( + "allocation of size {:?} could not be created", + requested_size, + ); + } + + let (alloc_id, origin) = allocation?; + let id = AllocId { atlas_id: self.atlases.len() - 1, alloc_id, }; - (id, origin) + Some((id, origin)) } - pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> (AllocId, RectI) { - let (alloc_id, origin) = self.allocate(size); + pub fn upload(&mut self, size: Vector2I, bytes: &[u8]) -> Option<(AllocId, RectI)> { + let (alloc_id, origin) = self.allocate(size)?; let bounds = RectI::new(origin, size); self.atlases[alloc_id.atlas_id].upload(bounds, bytes); - (alloc_id, bounds) + Some((alloc_id, bounds)) } pub fn deallocate(&mut self, id: AllocId) { diff --git a/crates/gpui/src/platform/mac/fonts.rs b/crates/gpui/src/platform/mac/fonts.rs index 1706134f67..5a08cacf09 100644 --- a/crates/gpui/src/platform/mac/fonts.rs +++ b/crates/gpui/src/platform/mac/fonts.rs @@ -3,26 +3,27 @@ use crate::{ geometry::{ rect::{RectF, RectI}, transform2d::Transform2F, - vector::{vec2f, vec2i, Vector2F}, + vector::{vec2f, Vector2F}, }, - platform, + platform::{self, RasterizationOptions}, text_layout::{Glyph, LineLayout, Run, RunStyle}, }; use cocoa::appkit::{CGFloat, CGPoint}; +use collections::HashMap; use core_foundation::{ array::CFIndex, attributed_string::{CFAttributedStringRef, CFMutableAttributedString}, base::{CFRange, TCFType}, - number::CFNumber, string::CFString, }; use core_graphics::{ - base::CGGlyph, color_space::CGColorSpace, context::CGContext, geometry::CGAffineTransform, + base::{kCGImageAlphaPremultipliedLast, CGGlyph}, + color_space::CGColorSpace, + context::CGContext, }; -use core_text::{line::CTLine, string_attributes::kCTFontAttributeName}; +use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName}; use font_kit::{ - canvas::RasterizationOptions, handle::Handle, hinting::HintingOptions, source::SystemSource, - sources::mem::MemSource, + handle::Handle, hinting::HintingOptions, source::SystemSource, sources::mem::MemSource, }; use parking_lot::RwLock; use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void, sync::Arc}; @@ -36,6 +37,8 @@ struct FontSystemState { memory_source: MemSource, system_source: SystemSource, fonts: Vec, + font_ids_by_postscript_name: HashMap, + postscript_names_by_font_id: HashMap, } 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)> { - 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 { @@ -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)> { 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); - Some((bounds, pixels)) + 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((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::() .unwrap() - .get(&font_id_attr_name) - .downcast::() - .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); diff --git a/crates/gpui/src/platform/mac/image_cache.rs b/crates/gpui/src/platform/mac/image_cache.rs index dac2e1a38b..f16c62462b 100644 --- a/crates/gpui/src/platform/mac/image_cache.rs +++ b/crates/gpui/src/platform/mac/image_cache.rs @@ -1,20 +1,39 @@ -use metal::{MTLPixelFormat, TextureDescriptor, TextureRef}; - use super::atlas::{AllocId, AtlasAllocator}; use crate::{ + fonts::{FontId, GlyphId}, geometry::{rect::RectI, vector::Vector2I}, - ImageData, + platform::RasterizationOptions, + scene::ImageGlyph, + FontSystem, ImageData, }; -use std::{collections::HashMap, mem}; +use anyhow::anyhow; +use metal::{MTLPixelFormat, TextureDescriptor, TextureRef}; +use ordered_float::OrderedFloat; +use std::{collections::HashMap, mem, sync::Arc}; + +#[derive(Hash, Eq, PartialEq)] +struct GlyphDescriptor { + font_id: FontId, + font_size: OrderedFloat, + glyph_id: GlyphId, +} pub struct ImageCache { prev_frame: HashMap, curr_frame: HashMap, + image_glyphs: HashMap>, atlases: AtlasAllocator, + scale_factor: f32, + fonts: Arc, } impl ImageCache { - pub fn new(device: metal::Device, size: Vector2I) -> Self { + pub fn new( + device: metal::Device, + size: Vector2I, + scale_factor: f32, + fonts: Arc, + ) -> 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() { diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index d620c47c15..5554a96dbb 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -3,7 +3,7 @@ use crate::{ executor, keymap::Keystroke, platform::{self, CursorStyle}, - AnyAction, ClipboardItem, Event, Menu, MenuItem, + Action, ClipboardItem, Event, Menu, MenuItem, }; use anyhow::{anyhow, Result}; use block::ConcreteBlock; @@ -38,8 +38,8 @@ use ptr::null_mut; use std::{ cell::{Cell, RefCell}, convert::TryInto, - ffi::{c_void, CStr}, - os::raw::c_char, + ffi::{c_void, CStr, OsStr}, + os::{raw::c_char, unix::ffi::OsStrExt}, path::{Path, PathBuf}, ptr, rc::Rc, @@ -91,8 +91,8 @@ unsafe fn build_classes() { handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); decl.add_method( - sel!(application:openFiles:), - open_files as extern "C" fn(&mut Object, Sel, id, id), + sel!(application:openURLs:), + open_urls as extern "C" fn(&mut Object, Sel, id, id), ); decl.register() } @@ -107,10 +107,10 @@ pub struct MacForegroundPlatformState { resign_active: Option>, quit: Option>, event: Option bool>>, - menu_command: Option>, - open_files: Option)>>, + menu_command: Option>, + open_urls: Option)>>, finish_launching: Option ()>>, - menu_actions: Vec>, + menu_actions: Vec>, } 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)>) { - self.0.borrow_mut().open_files = Some(callback); + fn on_open_urls(&self, callback: Box)>) { + self.0.borrow_mut().open_urls = Some(callback); } fn run(&self, on_finish_launching: Box ()>) { @@ -235,7 +235,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { } } - fn on_menu_command(&self, callback: Box) { + fn on_menu_command(&self, callback: Box) { 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 { + fn path_for_auxiliary_executable(&self, name: &str) -> Result { 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::>() }; 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 { + 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::*; diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 873586b61e..99dbd2030e 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -6,9 +6,10 @@ use crate::{ vector::{vec2f, vec2i, Vector2F}, }, platform, - scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow, Underline}, + scene::{Glyph, Icon, Image, ImageGlyph, Layer, Quad, Scene, Shadow, Underline}, }; use cocoa::foundation::NSUInteger; +use log::warn; use metal::{MTLPixelFormat, MTLResourceOptions, NSRange}; use shaders::ToFloat2 as _; use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec}; @@ -66,8 +67,13 @@ impl Renderer { MTLResourceOptions::StorageModeManaged, ); - let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts); - let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768)); + let sprite_cache = SpriteCache::new( + device.clone(), + vec2i(1024, 768), + scale_factor, + fonts.clone(), + ); + let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts); let path_atlases = AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor()); let quad_pipeline_state = build_pipeline_state( @@ -140,6 +146,9 @@ impl Renderer { command_buffer: &metal::CommandBufferRef, output: &metal::TextureRef, ) { + self.sprite_cache.set_scale_factor(scene.scale_factor()); + self.image_cache.set_scale_factor(scene.scale_factor()); + let mut offset = 0; let path_sprites = self.render_path_atlases(scene, &mut offset, command_buffer); @@ -172,7 +181,14 @@ impl Renderer { for path in layer.paths() { let origin = path.bounds.origin() * scene.scale_factor(); let size = (path.bounds.size() * scene.scale_factor()).ceil(); - let (alloc_id, atlas_origin) = self.path_atlases.allocate(size.to_i32()); + + let path_allocation = self.path_atlases.allocate(size.to_i32()); + if path_allocation.is_none() { + // Path size was likely zero. + warn!("could not allocate path texture of size {:?}", size); + continue; + } + let (alloc_id, atlas_origin) = path_allocation.unwrap(); let atlas_origin = atlas_origin.to_f32(); sprites.push(PathSprite { layer_id, @@ -351,6 +367,7 @@ impl Renderer { ); self.render_images( layer.images(), + layer.image_glyphs(), scale_factor, offset, drawable_size, @@ -533,8 +550,6 @@ impl Renderer { return; } - self.sprite_cache.set_scale_factor(scale_factor); - let mut sprites_by_atlas = HashMap::new(); for glyph in glyphs { @@ -569,6 +584,10 @@ impl Renderer { let sprite = self.sprite_cache .render_icon(source_size, icon.path.clone(), icon.svg.clone()); + if sprite.is_none() { + continue; + } + let sprite = sprite.unwrap(); sprites_by_atlas .entry(sprite.atlas_id) @@ -641,12 +660,13 @@ impl Renderer { fn render_images( &mut self, images: &[Image], + image_glyphs: &[ImageGlyph], scale_factor: f32, offset: &mut usize, drawable_size: Vector2F, command_encoder: &metal::RenderCommandEncoderRef, ) { - if images.is_empty() { + if images.is_empty() && image_glyphs.is_empty() { return; } @@ -674,6 +694,31 @@ impl Renderer { }); } + for image_glyph in image_glyphs { + let origin = (image_glyph.origin * scale_factor).floor(); + if let Some((alloc_id, atlas_bounds, glyph_origin)) = + self.image_cache.render_glyph(image_glyph) + { + images_by_atlas + .entry(alloc_id.atlas_id) + .or_insert_with(Vec::new) + .push(shaders::GPUIImage { + origin: (origin + glyph_origin.to_f32()).to_float2(), + target_size: atlas_bounds.size().to_float2(), + source_size: atlas_bounds.size().to_float2(), + atlas_origin: atlas_bounds.origin().to_float2(), + border_top: 0., + border_right: 0., + border_bottom: 0., + border_left: 0., + border_color: Default::default(), + corner_radius: 0., + }); + } else { + log::warn!("could not render glyph with id {}", image_glyph.id); + } + } + command_encoder.set_render_pipeline_state(&self.image_pipeline_state); command_encoder.set_vertex_buffer( shaders::GPUIImageVertexInputIndex_GPUIImageVertexInputIndexVertices as u64, diff --git a/crates/gpui/src/platform/mac/sprite_cache.rs b/crates/gpui/src/platform/mac/sprite_cache.rs index 17fec02066..ceeb06698c 100644 --- a/crates/gpui/src/platform/mac/sprite_cache.rs +++ b/crates/gpui/src/platform/mac/sprite_cache.rs @@ -2,8 +2,9 @@ use super::atlas::AtlasAllocator; use crate::{ fonts::{FontId, GlyphId}, geometry::vector::{vec2f, Vector2F, Vector2I}, - platform, + platform::{self, RasterizationOptions}, }; +use collections::hash_map::Entry; use metal::{MTLPixelFormat, TextureDescriptor}; use ordered_float::OrderedFloat; use std::{borrow::Cow, collections::HashMap, sync::Arc}; @@ -112,9 +113,12 @@ impl SpriteCache { glyph_id, subpixel_shift, scale_factor, + RasterizationOptions::Alpha, )?; - let (alloc_id, atlas_bounds) = atlases.upload(glyph_bounds.size(), &mask); + let (alloc_id, atlas_bounds) = atlases + .upload(glyph_bounds.size(), &mask) + .expect("could not upload glyph"); Some(GlyphSprite { atlas_id: alloc_id.atlas_id, atlas_origin: atlas_bounds.origin(), @@ -130,31 +134,31 @@ impl SpriteCache { size: Vector2I, path: Cow<'static, str>, svg: usvg::Tree, - ) -> IconSprite { + ) -> Option { 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::>(); - - 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> { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 5281009155..f6798e15af 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -361,9 +361,10 @@ impl platform::Window for Window { } }); let block = block.copy(); + let native_window = self.0.borrow().native_window; let _: () = msg_send![ alert, - beginSheetModalForWindow: self.0.borrow().native_window + beginSheetModalForWindow: native_window completionHandler: block ]; done_rx diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 5bbf7d06c1..6804b39f7c 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -1,7 +1,7 @@ use super::{AppVersion, CursorStyle, WindowBounds}; use crate::{ geometry::vector::{vec2f, Vector2F}, - AnyAction, ClipboardItem, + Action, ClipboardItem, }; use anyhow::{anyhow, Result}; use parking_lot::Mutex; @@ -66,13 +66,13 @@ impl super::ForegroundPlatform for ForegroundPlatform { fn on_event(&self, _: Box bool>) {} - fn on_open_files(&self, _: Box)>) {} + fn on_open_urls(&self, _: Box)>) {} fn run(&self, _on_finish_launching: Box ()>) { unimplemented!() } - fn on_menu_command(&self, _: Box) {} + fn on_menu_command(&self, _: Box) {} fn set_menus(&self, _: Vec) {} @@ -161,7 +161,7 @@ impl super::Platform for Platform { UtcOffset::UTC } - fn path_for_resource(&self, _name: Option<&str>, _extension: Option<&str>) -> Result { + fn path_for_auxiliary_executable(&self, _name: &str) -> Result { Err(anyhow!("app not running inside a bundle")) } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index b4e419107a..793f41f487 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -6,7 +6,7 @@ use crate::{ json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, - Action, AnyAction, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, + Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, ElementStateContext, Entity, FontSystem, ModelHandle, ReadModel, ReadView, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle, }; @@ -51,15 +51,21 @@ impl Presenter { } pub fn dispatch_path(&self, app: &AppContext) -> Vec { - 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(); + 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 { + let mut path = Vec::new(); + 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 { - 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, - pub action: Box, + pub action: Box, } 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 { diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 450edc6d8f..6c23a731bc 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -29,6 +29,7 @@ pub struct Layer { images: Vec, shadows: Vec, glyphs: Vec, + image_glyphs: Vec, icons: Vec, paths: Vec, } @@ -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) -> 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); } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index a866ea0a85..2d8672aab3 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -191,6 +191,7 @@ pub struct Glyph { pub id: GlyphId, pub position: Vector2F, pub index: usize, + pub is_emoji: bool, } impl Line { @@ -323,13 +324,22 @@ impl Line { }); } - cx.scene.push_glyph(scene::Glyph { - font_id: run.font_id, - font_size: self.layout.font_size, - id: glyph.id, - origin: glyph_origin, - color, - }); + if glyph.is_emoji { + cx.scene.push_image_glyph(scene::ImageGlyph { + font_id: run.font_id, + font_size: self.layout.font_size, + id: glyph.id, + origin: glyph_origin, + }); + } else { + cx.scene.push_glyph(scene::Glyph { + font_id: run.font_id, + font_size: self.layout.font_size, + id: glyph.id, + origin: glyph_origin, + color, + }); + } } } @@ -389,13 +399,22 @@ impl Line { .bounding_box(run.font_id, self.layout.font_size), ); if glyph_bounds.intersects(visible_bounds) { - cx.scene.push_glyph(scene::Glyph { - font_id: run.font_id, - font_size: self.layout.font_size, - id: glyph.id, - origin: glyph_bounds.origin() + baseline_origin, - color, - }); + if glyph.is_emoji { + cx.scene.push_image_glyph(scene::ImageGlyph { + font_id: run.font_id, + font_size: self.layout.font_size, + id: glyph.id, + origin: glyph_bounds.origin() + baseline_origin, + }); + } else { + cx.scene.push_glyph(scene::Glyph { + font_id: run.font_id, + font_size: self.layout.font_size, + id: glyph.id, + origin: glyph_bounds.origin() + baseline_origin, + color, + }); + } } } } diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index 944f45f0d4..10cd0cd5a2 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -1,6 +1,8 @@ +use serde::Deserialize; + use crate::{ - action, elements::*, AppContext, Entity, MutableAppContext, RenderContext, View, ViewContext, - WeakViewHandle, + actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View, + ViewContext, WeakViewHandle, }; pub struct Select { @@ -25,8 +27,11 @@ pub enum ItemType { Unselected, } -action!(ToggleSelect); -action!(SelectItem, usize); +#[derive(Clone, Deserialize)] +pub struct SelectItem(pub usize); + +actions!(select, [ToggleSelect]); +impl_actions!(select, [SelectItem]); pub enum Event {} diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index c107175dca..7ec8b4fb7c 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -75,68 +75,65 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { match last_segment.map(|s| s.ident.to_string()).as_deref() { Some("StdRng") => { inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),)); + continue; } Some("bool") => { inner_fn_args.extend(quote!(is_last_iteration,)); + continue; } - _ => { - return TokenStream::from( - syn::Error::new_spanned(arg, "invalid argument") - .into_compile_error(), - ) - } - } - } else if let Type::Reference(ty) = &*arg.ty { - match &*ty.elem { - Type::Path(ty) => { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("TestAppContext") => { - let first_entity_id = ix * 100_000; - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = #namespace::TestAppContext::new( - foreground_platform.clone(), - cx.platform().clone(), - deterministic.build_foreground(#ix), - deterministic.build_background(), - cx.font_cache().clone(), - cx.leak_detector(), - #first_entity_id, - ); - )); - cx_teardowns.extend(quote!( - #cx_varname.update(|cx| cx.remove_all_windows()); - deterministic.run_until_parked(); - #cx_varname.update(|_| {}); // flush effects - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - } - _ => { - return TokenStream::from( - syn::Error::new_spanned(arg, "invalid argument") - .into_compile_error(), - ) + Some("Arc") => { + if let syn::PathArguments::AngleBracketed(args) = + &last_segment.unwrap().arguments + { + if let Some(syn::GenericArgument::Type(syn::Type::Path(ty))) = + args.args.last() + { + let last_segment = ty.path.segments.last(); + if let Some("Deterministic") = + last_segment.map(|s| s.ident.to_string()).as_deref() + { + inner_fn_args.extend(quote!(deterministic.clone(),)); + continue; + } } } } - _ => { - return TokenStream::from( - syn::Error::new_spanned(arg, "invalid argument") - .into_compile_error(), - ) + _ => {} + } + } else if let Type::Reference(ty) = &*arg.ty { + if let Type::Path(ty) = &*ty.elem { + let last_segment = ty.path.segments.last(); + if let Some("TestAppContext") = + last_segment.map(|s| s.ident.to_string()).as_deref() + { + let first_entity_id = ix * 100_000; + let cx_varname = format_ident!("cx_{}", ix); + cx_vars.extend(quote!( + let mut #cx_varname = #namespace::TestAppContext::new( + foreground_platform.clone(), + cx.platform().clone(), + deterministic.build_foreground(#ix), + deterministic.build_background(), + cx.font_cache().clone(), + cx.leak_detector(), + #first_entity_id, + ); + )); + cx_teardowns.extend(quote!( + #cx_varname.update(|cx| cx.remove_all_windows()); + deterministic.run_until_parked(); + #cx_varname.update(|_| {}); // flush effects + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; } } - } else { - return TokenStream::from( - syn::Error::new_spanned(arg, "invalid argument").into_compile_error(), - ); } - } else { - return TokenStream::from( - syn::Error::new_spanned(arg, "invalid argument").into_compile_error(), - ); } + + return TokenStream::from( + syn::Error::new_spanned(arg, "invalid argument").into_compile_error(), + ); } parse_quote! { diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index 8ccd9dc27f..94dcf8e407 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -14,4 +14,4 @@ util = { path = "../util" } workspace = { path = "../workspace" } chrono = "0.4" dirs = "4.0" -log = "0.4" \ No newline at end of file +log = { version = "0.4.16", features = ["kv_unstable_serde"] } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 460423f6dc..7aa8be4d97 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,14 +1,13 @@ use chrono::{Datelike, Local, Timelike}; use editor::{Autoscroll, Editor}; -use gpui::{action, keymap::Binding, MutableAppContext}; +use gpui::{actions, MutableAppContext}; use std::{fs::OpenOptions, sync::Arc}; use util::TryFutureExt as _; use workspace::AppState; -action!(NewJournalEntry); +actions!(journal, [NewJournalEntry]); pub fn init(app_state: Arc, 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, 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; diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 518753fd75..275581f807 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -35,7 +35,7 @@ async-broadcast = "0.3.4" async-trait = "0.1" futures = "0.3" lazy_static = "1.4" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } @@ -57,5 +57,6 @@ util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.8" rand = "0.8.3" +tree-sitter-json = "0.19.0" tree-sitter-rust = "0.20.0" unindent = "0.1.7" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index eb68b8bbdd..95aa27b6d2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -66,7 +66,6 @@ pub struct Buffer { file_update_count: usize, completion_triggers: Vec, deferred_ops: OperationQueue, - indent_size: u32, } pub struct BufferSnapshot { @@ -80,7 +79,6 @@ pub struct BufferSnapshot { selections_update_count: usize, language: Option>, parse_count: usize, - indent_size: u32, } #[derive(Clone, Debug)] @@ -214,6 +212,7 @@ struct AutoindentRequest { before_edit: BufferSnapshot, edited: Vec, inserted: Option>>, + 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>, cx: &mut ModelContext) { + *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, { - self.edit_internal([0..self.len()], text, false, cx) + self.edit_internal([0..self.len()], text, None, cx) } pub fn edit( @@ -1068,13 +1065,14 @@ impl Buffer { S: ToOffset, T: Into, { - self.edit_internal(ranges_iter, new_text, false, cx) + self.edit_internal(ranges_iter, new_text, None, cx) } pub fn edit_with_autoindent( &mut self, ranges_iter: I, new_text: T, + indent_size: u32, cx: &mut ModelContext, ) -> Option where @@ -1082,14 +1080,14 @@ impl Buffer { S: ToOffset, T: Into, { - self.edit_internal(ranges_iter, new_text, true, cx) + self.edit_internal(ranges_iter, new_text, Some(indent_size), cx) } pub fn edit_internal( &mut self, ranges_iter: I, new_text: T, - autoindent: bool, + autoindent_size: Option, cx: &mut ModelContext, ) -> Option 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, } } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 322fd19b9e..37377b6eef 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -234,6 +234,14 @@ impl LanguageRegistry { .cloned() } + pub fn language_names(&self) -> Vec { + self.languages + .read() + .iter() + .map(|language| language.name().to_string()) + .collect() + } + pub fn select_language(&self, path: impl AsRef) -> Option> { let path = path.as_ref(); let filename = path.file_name().and_then(|name| name.to_str()); diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0acd4d7d2f..a194c26625 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -276,12 +276,32 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { "arguments: (arguments (identifier)))))))", ) ); +} - fn get_tree_sexp(buffer: &ModelHandle, 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::>())) .collect::>(); - 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, cx: &gpui::TestAppContext) -> String { + buffer.read_with(cx, |buffer, _| { + buffer.syntax_tree().unwrap().root_node().to_sexp() + }) +} + fn empty(point: Point) -> Range { point..point } diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 199da8c24e..c749261bf5 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -17,7 +17,7 @@ util = { path = "../util" } anyhow = "1.0" async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true } futures = "0.3" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } lsp-types = "0.91" parking_lot = "0.11" postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index f5fc98640d..c0b21eeeda 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -201,6 +201,9 @@ impl LanguageServer { std::str::from_utf8(&buffer)? )); } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; } } .log_err() diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index e5ed300dc7..5b4751e620 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -12,6 +12,8 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } language = { path = "../language" } +picker = { path = "../picker" } +settings = { path = "../settings" } text = { path = "../text" } workspace = { path = "../workspace" } ordered-float = "2.1.1" diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index c33cb60b3e..2ff5c4a57f 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -4,45 +4,31 @@ use editor::{ }; use fuzzy::StringMatch; use gpui::{ - action, - elements::*, - geometry::vector::Vector2F, - keymap::{self, Binding}, - AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, - WeakViewHandle, + actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext, + RenderContext, Task, View, ViewContext, ViewHandle, }; use language::Outline; use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use settings::Settings; use std::cmp::{self, Reverse}; -use workspace::{ - menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, - Settings, Workspace, -}; +use workspace::Workspace; -action!(Toggle); +actions!(outline, [Toggle]); pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("cmd-shift-O", Toggle, Some("Editor")), - Binding::new("escape", Toggle, Some("OutlineView")), - ]); cx.add_action(OutlineView::toggle); - cx.add_action(OutlineView::confirm); - cx.add_action(OutlineView::select_prev); - cx.add_action(OutlineView::select_next); - cx.add_action(OutlineView::select_first); - cx.add_action(OutlineView::select_last); + Picker::::init(cx); } struct OutlineView { - handle: WeakViewHandle, + picker: ViewHandle>, active_editor: ViewHandle, outline: Outline, selected_match_index: usize, prev_scroll_position: Option, matches: Vec, - query_editor: ViewHandle, - 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) -> ElementBox { - let settings = cx.global::(); - - 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) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); + cx.focus(&self.picker); } } @@ -103,24 +63,16 @@ impl OutlineView { editor: ViewHandle, cx: &mut ViewContext, ) -> 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) { @@ -144,34 +96,18 @@ impl OutlineView { } } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if self.selected_match_index > 0 { - self.select(self.selected_match_index - 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_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - 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.select(0, true, false, cx); - } - - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { - self.select(self.matches.len().saturating_sub(1), true, false, cx); - } - - fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext) { - 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.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.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, @@ -219,24 +134,27 @@ impl OutlineView { Event::Dismissed => workspace.dismiss_modal(cx), } } +} - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - 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 update_matches(&mut self, cx: &mut ViewContext) { + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.set_selected_index(ix, true, cx); + } + + fn center_selection_after_match_updates(&self) -> bool { + true + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> 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::(); - 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)), - ); - }, - ); - - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") + fn confirm(&mut self, cx: &mut ViewContext) { + 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 render_match( - &self, - string_match: &StringMatch, - index: usize, - cx: &AppContext, - ) -> ElementBox { + fn dismiss(&mut self, cx: &mut ViewContext) { + self.restore_active_editor(cx); + cx.emit(Event::Dismissed); + } + + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { let settings = cx.global::(); - 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 diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml new file mode 100644 index 0000000000..86e657ecad --- /dev/null +++ b/crates/picker/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "picker" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/picker.rs" +doctest = false + +[dependencies] +editor = { path = "../editor" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +workspace = { path = "../workspace", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs new file mode 100644 index 0000000000..4e4a7e92aa --- /dev/null +++ b/crates/picker/src/picker.rs @@ -0,0 +1,277 @@ +use editor::Editor; +use gpui::{ + elements::{ + ChildView, EventHandler, Flex, Label, ParentElement, ScrollTarget, UniformList, + UniformListState, + }, + geometry::vector::{vec2f, Vector2F}, + keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, +}; +use settings::Settings; +use std::cmp; +use workspace::menu::{ + Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev, +}; + +pub struct Picker { + delegate: WeakViewHandle, + query_editor: ViewHandle, + 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); + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()>; + fn confirm(&mut self, cx: &mut ViewContext); + fn dismiss(&mut self, cx: &mut ViewContext); + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox; + fn center_selection_after_match_updates(&self) -> bool { + false + } +} + +impl Entity for Picker { + type Event = (); +} + +impl View for Picker { + fn ui_name() -> &'static str { + "Picker" + } + + fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { + let settings = cx.global::(); + 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) { + cx.focus(&self.query_editor); + } +} + +impl Picker { + 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, cx: &mut ViewContext) -> 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, + event: &editor::Event, + cx: &mut ViewContext, + ) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + if let Some(delegate) = self.delegate.upgrade(cx) { + delegate.update(cx, |delegate, cx| delegate.dismiss(cx)); + } + } +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 89f6999efa..728dae3128 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -25,6 +25,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } rpc = { path = "../rpc" } +settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } aho-corasick = "0.7" @@ -34,7 +35,7 @@ futures = "0.3" ignore = "0.4" lazy_static = "1.4.0" libc = "0.2" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c9683f39d9..5c1064400e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,6 +28,8 @@ use parking_lot::Mutex; use postage::watch; use rand::prelude::*; use search::SearchQuery; +use serde::Serialize; +use settings::Settings; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use std::{ @@ -73,7 +75,6 @@ pub struct Project { client_state: ProjectClientState, collaborators: HashMap, subscriptions: Vec, - language_servers_with_diagnostics_running: isize, opened_buffer: (Rc>>, watch::Receiver<()>), shared_buffers: HashMap>, loading_buffers: HashMap< @@ -132,16 +133,18 @@ pub enum Event { CollaboratorLeft(PeerId), } +#[derive(Serialize)] pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, - pending_diagnostic_updates: isize, + pub pending_diagnostic_updates: isize, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct LanguageServerProgress { pub message: Option, pub percentage: Option, + #[serde(skip_serializing)] pub last_update_at: Instant, } @@ -151,12 +154,10 @@ pub struct ProjectPath { pub path: Arc, } -#[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 { &self.languages } @@ -815,13 +811,19 @@ impl Project { !self.is_local() } - pub fn create_buffer(&mut self, cx: &mut ModelContext) -> Result> { + pub fn create_buffer( + &mut self, + text: &str, + language: Option>, + cx: &mut ModelContext, + ) -> Result> { 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, ) -> Task> { 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::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::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - uri.clone(), - ), - }, - ) - .log_err(); - } - } - } - }) - .detach(); } } } + fn unregister_buffer_from_language_server( + &mut self, + buffer: &ModelHandle, + old_path: PathBuf, + cx: &mut ModelContext, + ) { + 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::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(old_path).unwrap(), + ), + }, + ) + .log_err(); + } + }); + } + fn on_buffer_event( &mut self, buffer: ModelHandle, @@ -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(), - }, - 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); - self.broadcast_language_server_update( - server_id, - proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + } + 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), + }, + ), + ); + } + } + 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::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::().tab_size(language_name.as_deref()) + }); let lsp_edits = if capabilities .document_formatting_provider .as_ref() @@ -2155,7 +2172,7 @@ impl Project { .request::(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) -> Task>> { 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::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::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - )); - } - cx.spawn_weak(|this, cx| async move { - let responses = futures::future::try_join_all(requests).await?; + 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); + } - 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); - 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) + 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, cx: &mut ModelContext) { @@ -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.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::() + == 1 + { cx.emit(Event::DiskBasedDiagnosticsStarted); } } pub fn disk_based_diagnostics_finished(&mut self, cx: &mut ModelContext) { 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::() + == 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::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test2.rs").unwrap() + ), + ); + assert_eq!( + fake_rust_server + .receive_notification::() + .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::() + .await + .text_document, + lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), + ), + ); + assert_eq!( + fake_json_server + .receive_notification::() + .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::() + .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::() + .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::() .await .text_document, - fake_rust_server + fake_json_server .receive_notification::() .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::() - .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::>(), + &[ + ("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::>(), + &[ + ("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(); @@ -5312,6 +5566,103 @@ mod tests { }] ) }); + + // Ensure publishing empty diagnostics twice only results in one update event. + fake_server.notify::( + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: Default::default(), + }, + ); + assert_eq!( + events.next().await.unwrap(), + Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) + ); + + fake_server.notify::( + lsp::PublishDiagnosticsParams { + uri: Url::from_file_path("/dir/a.rs").unwrap(), + version: None, + diagnostics: Default::default(), + }, + ); + cx.foreground().run_until_parked(); + assert_eq!(futures::poll!(events.next()), Poll::Pending); + } + + #[gpui::test] + async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + let progress_token = "the-progress-token"; + let mut language = Language::new( + LanguageConfig { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_sources: &["disk"], + disk_based_diagnostics_progress_token: Some(progress_token), + ..Default::default() + }); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + 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, "a.rs"), cx) + }) + .await + .unwrap(); + + // Simulate diagnostics starting to update. + let mut fake_server = fake_servers.next().await.unwrap(); + fake_server.start_progress(progress_token).await; + + // Restart the server before the diagnostics finish updating. + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers([buffer], cx); + }); + let mut events = subscribe(&project, cx); + + // Simulate the newly started server sending more diagnostics. + let mut fake_server = fake_servers.next().await.unwrap(); + fake_server.start_progress(progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsStarted + ); + + // All diagnostics are considered done, despite the old server's diagnostic + // task never completing. + fake_server.end_progress(progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdated + ); + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsFinished + ); + project.read_with(cx, |project, _| { + assert!(!project.is_running_disk_based_diagnostics()); + }); } #[gpui::test] @@ -6359,7 +6710,9 @@ mod tests { .unwrap(); let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - let buffer = project.update(cx, |project, cx| project.create_buffer(cx).unwrap()); + let buffer = project.update(cx, |project, cx| { + project.create_buffer("", None, cx).unwrap() + }); buffer.update(cx, |buffer, cx| { buffer.edit([0..0], "abc", cx); assert!(buffer.is_dirty()); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index beacc5a863..19b490b0e2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -11,7 +11,10 @@ use client::{proto, Client, TypedEnvelope}; use clock::ReplicaId; use collections::HashMap; use futures::{ - channel::mpsc::{self, UnboundedSender}, + channel::{ + mpsc::{self, UnboundedSender}, + oneshot, + }, Stream, StreamExt, }; use fuzzy::CharBag; @@ -26,7 +29,6 @@ use language::{ use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ - oneshot, prelude::{Sink as _, Stream as _}, watch, }; @@ -231,8 +233,6 @@ impl Worktree { DiagnosticSummary { error_count: summary.error_count as usize, warning_count: summary.warning_count as usize, - info_count: summary.info_count as usize, - hint_count: summary.hint_count as usize, }, ) }), @@ -564,29 +564,37 @@ impl LocalWorktree { worktree_path: Arc, diagnostics: Vec>, _: &mut ModelContext, - ) -> 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 { + 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); } - Ok(()) + 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(updated) } pub fn scan_complete(&self) -> impl Future { @@ -727,11 +735,11 @@ impl LocalWorktree { pub fn share(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { 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::(); 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, 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); + } } } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 2e50178036..81e975cc73 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] gpui = { path = "../gpui" } project = { path = "../project" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0dd6b08bac..11e9f2db9e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,15 +1,16 @@ use gpui::{ - action, + actions, elements::{ Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Svg, UniformList, UniformListState, }, - keymap::{self, Binding}, + impl_internal_actions, keymap, platform::CursorStyle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle, }; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +use settings::Settings; use std::{ collections::{hash_map, HashMap}, ffi::OsStr, @@ -17,7 +18,7 @@ use std::{ }; use workspace::{ menu::{SelectNext, SelectPrev}, - Settings, Workspace, + Workspace, }; pub struct ProjectPanel { @@ -45,10 +46,14 @@ struct EntryDetails { is_selected: bool, } -action!(ExpandSelectedEntry); -action!(CollapseSelectedEntry); -action!(ToggleExpanded, ProjectEntryId); -action!(Open, ProjectEntryId); +#[derive(Clone)] +pub struct ToggleExpanded(pub ProjectEntryId); + +#[derive(Clone)] +pub struct Open(pub ProjectEntryId); + +actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]); +impl_internal_actions!(project_panel, [Open, ToggleExpanded]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::expand_selected_entry); @@ -57,10 +62,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::open_entry); - cx.add_bindings([ - Binding::new("right", ExpandSelectedEntry, Some("ProjectPanel")), - Binding::new("left", CollapseSelectedEntry, Some("ProjectPanel")), - ]); } pub enum Event { diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index cdaedf109c..cb1f186bde 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -11,11 +11,21 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } project = { path = "../project" } text = { path = "../text" } +settings = { path = "../settings" } workspace = { path = "../workspace" } util = { path = "../util" } anyhow = "1.0.38" ordered-float = "2.1.1" postage = { version = "0.4", features = ["futures-traits"] } smol = "1.2" + +[dev-dependencies] +futures = "0.3" +settings = { path = "../settings", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } \ No newline at end of file diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 74e7d90d68..d2c6850e8b 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -3,49 +3,33 @@ use editor::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - action, - elements::*, - keymap::{self, Binding}, - AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, + View, ViewContext, ViewHandle, }; use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; use project::{Project, Symbol}; -use std::{ - borrow::Cow, - cmp::{self, Reverse}, -}; +use settings::Settings; +use std::{borrow::Cow, cmp::Reverse}; use util::ResultExt; -use workspace::{ - menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, - Settings, Workspace, -}; +use workspace::Workspace; -action!(Toggle); +actions!(project_symbols, [Toggle]); pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("cmd-t", Toggle, None), - Binding::new("escape", Toggle, Some("ProjectSymbolsView")), - ]); cx.add_action(ProjectSymbolsView::toggle); - cx.add_action(ProjectSymbolsView::confirm); - cx.add_action(ProjectSymbolsView::select_prev); - cx.add_action(ProjectSymbolsView::select_next); - cx.add_action(ProjectSymbolsView::select_first); - cx.add_action(ProjectSymbolsView::select_last); + Picker::::init(cx); } pub struct ProjectSymbolsView { - handle: WeakViewHandle, + picker: ViewHandle>, project: ModelHandle, selected_match_index: usize, - list_state: UniformListState, symbols: Vec, match_candidates: Vec, + show_worktree_root_name: bool, + pending_update: Task<()>, matches: Vec, - pending_symbols_task: Task>, - query_editor: ViewHandle, } 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) -> ElementBox { - let settings = cx.global::(); - 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) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); + cx.focus(&self.picker); } } impl ProjectSymbolsView { fn new(project: ModelHandle, cx: &mut ViewContext) -> 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) { @@ -127,72 +79,7 @@ impl ProjectSymbolsView { }); } - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if self.selected_match_index > 0 { - self.select(self.selected_match_index - 1, cx); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - 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.select(0, cx); - } - - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { - self.select(self.matches.len().saturating_sub(1), cx); - } - - fn select(&mut self, index: usize, cx: &mut ViewContext) { - self.selected_match_index = index; - self.list_state.scroll_to(ScrollTarget::Show(index)); - cx.notify(); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - 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.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) { - let query = self.query_editor.read(cx).text(cx); + fn filter(&mut self, query: &str, cx: &mut ViewContext) { 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,112 +118,10 @@ 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::(); - 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()); - - 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) - })); - }, - ); - - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") - } - - fn render_match( - &self, - string_match: &StringMatch, - index: usize, - show_worktree_root_name: bool, - cx: &AppContext, - ) -> ElementBox { - let settings = cx.global::(); - let style = if index == self.selected_match_index { - &settings.theme.selector.active_item - } else { - &settings.theme.selector.item - }; - let symbol = &self.symbols[string_match.candidate_id]; - 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 { - let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) { - path = Cow::Owned(format!( - "{}{}{}", - worktree.read(cx).root_name(), - std::path::MAIN_SEPARATOR, - path.as_ref() - )); - } - } - - Flex::column() - .with_child( - Text::new(symbol.label.text.clone(), style.label.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &symbol.label.text, - style.label.text.clone().into(), - syntax_runs, - &string_match.positions, - )) - .boxed(), - ) - .with_child( - // Avoid styling the path differently when it is selected, since - // the symbol's syntax highlighting doesn't change when selected. - Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(), - ) - .contained() - .with_style(style.container) - .boxed() - } - - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::BufferEdited { .. } => self.update_matches(cx), - _ => {} - } - } - fn on_event( workspace: &mut Workspace, _: ViewHandle, @@ -375,3 +160,244 @@ impl ProjectSymbolsView { } } } + +impl PickerDelegate for ProjectSymbolsView { + fn confirm(&mut self, cx: &mut ViewContext) { + 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) { + cx.emit(Event::Dismissed); + } + + 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.selected_match_index = ix; + cx.notify(); + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> 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, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { + let string_match = &self.matches[ix]; + let settings = cx.global::(); + let style = if selected { + &settings.theme.selector.active_item + } else { + &settings.theme.selector.item + }; + let symbol = &self.symbols[string_match.candidate_id]; + let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax); + + let mut path = symbol.path.to_string_lossy(); + 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!( + "{}{}{}", + worktree.read(cx).root_name(), + std::path::MAIN_SEPARATOR, + path.as_ref() + )); + } + } + + Flex::column() + .with_child( + Text::new(symbol.label.text.clone(), style.label.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &symbol.label.text, + style.label.text.clone().into(), + syntax_runs, + &string_match.positions, + )) + .boxed(), + ) + .with_child( + // Avoid styling the path differently when it is selected, since + // the symbol's syntax highlighting doesn't change when selected. + Label::new(path.to_string(), settings.theme.selector.item.label.clone()).boxed(), + ) + .contained() + .with_style(style.container) + .boxed() + } +} + +#[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()); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree("/dir", json!({ "test.rs": "" })).await; + + let project = Project::test(fs.clone(), cx); + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + + 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::( + 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::>(); + 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)), + ), + } + } +} diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index e773b3f0ba..2750d9078f 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -9,31 +9,32 @@ path = "src/rpc.rs" doctest = false [features] -test-support = ["gpui/test-support"] +test-support = ["collections/test-support", "gpui/test-support"] [dependencies] +clock = { path = "../clock" } +collections = { path = "../collections" } +gpui = { path = "../gpui", optional = true } +util = { path = "../util" } anyhow = "1.0" async-lock = "2.4" async-tungstenite = "0.16" base64 = "0.13" futures = "0.3" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" -postage = { version = "0.4.1", features = ["futures-traits"] } prost = "0.8" rand = "0.8" rsa = "0.4" serde = { version = "1", features = ["derive"] } smol-timeout = "0.6" zstd = "0.9" -clock = { path = "../clock" } -gpui = { path = "../gpui", optional = true } -util = { path = "../util" } [build-dependencies] prost-build = "0.8" [dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } smol = "1.2.5" tempdir = "0.3.7" diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f61d6275c9..79a50e56d4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -453,8 +453,6 @@ message DiagnosticSummary { string path = 1; uint32 error_count = 2; uint32 warning_count = 3; - uint32 info_count = 4; - uint32 hint_count = 5; } message UpdateLanguageServer { diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs index a97797fc9d..53ba00a3c0 100644 --- a/crates/rpc/src/conn.rs +++ b/crates/rpc/src/conn.rs @@ -35,21 +35,24 @@ impl Connection { #[cfg(any(test, feature = "test-support"))] pub fn in_memory( executor: std::sync::Arc, - ) -> (Self, Self, postage::barrier::Sender) { - use postage::prelude::Stream; + ) -> (Self, Self, std::sync::Arc) { + 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, + killed: Arc, + executor: Arc, ) -> ( Box>, 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::(); 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; } diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 726453bea8..a2b88f795c 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -1,16 +1,18 @@ -use super::proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage}; -use super::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 super::{ + proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, RequestMessage}, + Connection, }; -use smol_timeout::TimeoutExt as _; +use anyhow::{anyhow, Context, Result}; +use collections::HashMap; +use futures::{ + channel::{mpsc, oneshot}, + stream::BoxStream, + FutureExt, SinkExt, StreamExt, +}; +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, + outgoing_tx: mpsc::UnboundedSender, next_message_id: Arc, response_channels: - Arc>>>>, + Arc)>>>>>, } 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 diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 9ee18faae2..eac21cc35d 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 13; +pub const PROTOCOL_VERSION: u32 = 14; diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 5553e3b9a2..39dbd54b5c 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -13,12 +13,14 @@ editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } anyhow = "1.0" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } postage = { version = "0.4.1", features = ["futures-traits"] } +serde = { version = "1", features = ["derive"] } [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b5f8eedf80..9893c108c0 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,50 +1,47 @@ -use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch}; +use crate::{ + active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch, + SelectPrevMatch, +}; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor}; use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity, - MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, + actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, AppContext, + Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::OffsetRangeExt; use project::search::SearchQuery; +use serde::Deserialize; +use settings::Settings; use std::ops::Range; -use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView}; +use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView}; -action!(Deploy, bool); -action!(Dismiss); -action!(FocusEditor); -action!(ToggleSearchOption, SearchOption); +#[derive(Clone, Deserialize)] +pub struct Deploy { + pub focus: bool, +} + +#[derive(Clone)] +pub struct ToggleSearchOption(pub SearchOption); + +actions!(buffer_search, [Dismiss, FocusEditor]); +impl_actions!(buffer_search, [Deploy]); +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) { + fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) { + 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) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + 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) { + fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { + self.select_match(Direction::Next, cx); + } + + fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { + self.select_match(Direction::Prev, cx); + } + + fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { 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) { + fn select_next_match_on_pane( + pane: &mut Pane, + action: &SelectNextMatch, + cx: &mut ViewContext, + ) { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - 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, + ) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx)); } } @@ -512,14 +544,14 @@ impl BufferSearchBar { } } - let theme = &cx.global::().theme.search; editor.highlight_background::( 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)] diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 745f23154f..1028d7f77e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,29 +1,25 @@ use crate::{ - active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch, - ToggleSearchOption, + active_match_index, match_index_for_direction, Direction, SearchOption, SelectNextMatch, + SelectPrevMatch, ToggleSearchOption, }; use collections::HashMap; use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, - ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, - ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + actions, elements::*, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext, + ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, + ViewHandle, WeakModelHandle, WeakViewHandle, }; use project::{search::SearchQuery, Project}; +use settings::Settings; use std::{ any::{Any, TypeId}, ops::Range, path::PathBuf, }; use util::ResultExt as _; -use workspace::{ - Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace, -}; +use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace}; -action!(Deploy); -action!(Search); -action!(SearchInNew); -action!(ToggleFocus); +actions!(project_search, [Deploy, Search, SearchInNew, ToggleFocus]); const MAX_TAB_TITLE_LEN: usize = 24; @@ -32,20 +28,12 @@ struct ActiveSearches(HashMap, WeakViewHandle, + 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::().theme.search; - editor.highlight_background::(match_ranges, theme.match_background, cx); + editor.highlight_background::( + 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, - ) { + fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext) { if let Some(search_view) = pane .active_item() .and_then(|item| item.downcast::()) { - 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) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + 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() } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 38d3a5fce8..48cf24b1f3 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,6 +1,6 @@ pub use buffer_search::BufferSearchBar; use editor::{Anchor, MultiBufferSnapshot}; -use gpui::{action, MutableAppContext}; +use gpui::{actions, impl_internal_actions, MutableAppContext}; pub use project_search::{ProjectSearchBar, ProjectSearchView}; use std::{ cmp::{self, Ordering}, @@ -15,8 +15,11 @@ pub fn init(cx: &mut MutableAppContext) { project_search::init(cx); } -action!(ToggleSearchOption, SearchOption); -action!(SelectMatch, Direction); +#[derive(Clone)] +pub struct ToggleSearchOption(pub SearchOption); + +actions!(search, [SelectNextMatch, SelectPrevMatch]); +impl_internal_actions!(search, [ToggleSearchOption]); #[derive(Clone, Copy)] pub enum SearchOption { diff --git a/crates/server/.env.toml b/crates/server/.env.toml deleted file mode 100644 index 931f7c4df2..0000000000 --- a/crates/server/.env.toml +++ /dev/null @@ -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----- -""" diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml new file mode 100644 index 0000000000..cc37b1bcfd --- /dev/null +++ b/crates/settings/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "settings" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/settings.rs" +doctest = false + +[features] +test-support = [] + +[dependencies] +assets = { path = "../assets" } +collections = { path = "../collections" } +gpui = { path = "../gpui" } +theme = { path = "../theme" } +util = { path = "../util" } +anyhow = "1.0.38" +schemars = "0.8" +serde = { version = "1", features = ["derive", "rc"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } +serde_path_to_error = "0.1.4" +toml = "0.5" diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs new file mode 100644 index 0000000000..d82c7ef8f6 --- /dev/null +++ b/crates/settings/src/keymap_file.rs @@ -0,0 +1,62 @@ +use anyhow::{Context, Result}; +use assets::Assets; +use collections::BTreeMap; +use gpui::{keymap::Binding, MutableAppContext}; +use serde::Deserialize; +use serde_json::value::RawValue; + +#[derive(Deserialize, Default, Clone)] +#[serde(transparent)] +pub struct KeymapFile(BTreeMap); + +type ActionsByKeystroke = BTreeMap>; + +#[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::(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::>>()?, + ) + } + Ok(()) + } +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs new file mode 100644 index 0000000000..ccd39baf46 --- /dev/null +++ b/crates/settings/src/settings.rs @@ -0,0 +1,262 @@ +mod keymap_file; + +use anyhow::Result; +use gpui::font_cache::{FamilyId, FontCache}; +use schemars::{ + gen::{SchemaGenerator, SchemaSettings}, + schema::{ + InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation, + }, + JsonSchema, +}; +use serde::Deserialize; +use serde_json::Value; +use std::{collections::HashMap, sync::Arc}; +use theme::{Theme, ThemeRegistry}; +use util::ResultExt as _; + +pub use keymap_file::KeymapFile; + +#[derive(Clone)] +pub struct Settings { + pub buffer_font_family: FamilyId, + pub buffer_font_size: f32, + pub vim_mode: bool, + pub tab_size: u32, + pub soft_wrap: SoftWrap, + pub preferred_line_length: u32, + pub language_overrides: HashMap, LanguageOverride>, + pub theme: Arc, +} + +#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] +pub struct LanguageOverride { + pub tab_size: Option, + pub soft_wrap: Option, + pub preferred_line_length: Option, +} + +#[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, + #[serde(default)] + pub buffer_font_size: Option, + #[serde(default)] + pub vim_mode: Option, + #[serde(flatten)] + pub editor: LanguageOverride, + #[serde(default)] + pub language_overrides: HashMap, LanguageOverride>, + #[serde(default)] + pub theme: Option, +} + +impl Settings { + pub fn new( + buffer_font_family: &str, + font_cache: &FontCache, + theme: Arc, + ) -> Result { + 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, + language_names: Vec, + ) -> 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::(); + + // 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>, + 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(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } +} + +fn merge_option(target: &mut Option, value: Option) { + if value.is_some() { + *target = value; + } +} diff --git a/crates/sum_tree/Cargo.toml b/crates/sum_tree/Cargo.toml index f43cdd43d4..b430f2e6b0 100644 --- a/crates/sum_tree/Cargo.toml +++ b/crates/sum_tree/Cargo.toml @@ -9,7 +9,7 @@ doctest = false [dependencies] arrayvec = "0.7.1" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } [dev-dependencies] ctor = "0.1" diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index f6a887eb27..a7209a7507 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -17,7 +17,7 @@ sum_tree = { path = "../sum_tree" } anyhow = "1.0.38" arrayvec = "0.7.1" lazy_static = "1.4" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11" postage = { version = "0.4.1", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 1c351079a7..ed918cd5c5 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -826,6 +826,8 @@ impl Buffer { edit.timestamp, ); self.snapshot.version.observe(edit.timestamp.local()); + self.local_clock.observe(edit.timestamp.local()); + self.lamport_clock.observe(edit.timestamp.lamport()); self.resolve_edit(edit.timestamp.local()); } } @@ -836,6 +838,7 @@ impl Buffer { if !self.version.observed(undo.id) { self.apply_undo(&undo)?; self.snapshot.version.observe(undo.id); + self.local_clock.observe(undo.id); self.lamport_clock.observe(lamport_timestamp); } } @@ -1033,8 +1036,6 @@ impl Buffer { self.snapshot.visible_text = visible_text; self.snapshot.deleted_text = deleted_text; self.snapshot.insertions.edit(new_insertions, &()); - self.local_clock.observe(timestamp.local()); - self.lamport_clock.observe(timestamp.lamport()); self.subscriptions.publish_mut(&edits); } diff --git a/crates/theme/src/resolution.rs b/crates/theme/src/resolution.rs deleted file mode 100644 index acebf72b86..0000000000 --- a/crates/theme/src/resolution.rs +++ /dev/null @@ -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 { - let tree = Tree::from_json(value)?; - tree.resolve()?; - tree.to_json() -} - -#[derive(Clone)] -enum Node { - Reference { - path: String, - parent: Option>>, - }, - Object { - base: Option, - children: IndexMap, - resolved: bool, - parent: Option>>, - }, - Array { - children: Vec, - resolved: bool, - parent: Option>>, - }, - String { - value: String, - parent: Option>>, - }, - Number { - value: serde_json::Number, - parent: Option>>, - }, - Bool { - value: bool, - parent: Option>>, - }, - Null { - parent: Option>>, - }, -} - -#[derive(Clone)] -struct Tree(Rc>); - -impl Tree { - pub fn new(node: Node) -> Self { - Self(Rc::new(RefCell::new(node))) - } - - fn from_json(value: Value) -> Result { - 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 { - 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 { - 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> { - 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) -> Result { - 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>>) { - 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()); - } -} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ff6eba88ef..dd6a7a79c7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1,4 +1,3 @@ -mod resolution; mod theme_registry; use gpui::{ @@ -12,7 +11,7 @@ use std::{collections::HashMap, sync::Arc}; pub use theme_registry::*; -pub const DEFAULT_THEME_NAME: &'static str = "black"; +pub const DEFAULT_THEME_NAME: &'static str = "dark"; #[derive(Deserialize, Default)] pub struct Theme { @@ -22,6 +21,7 @@ pub struct Theme { pub chat_panel: ChatPanel, pub contacts_panel: ContactsPanel, pub project_panel: ProjectPanel, + pub command_palette: CommandPalette, pub selector: Selector, pub editor: Editor, pub search: Search, @@ -190,6 +190,12 @@ pub struct ProjectPanelEntry { pub icon_spacing: f32, } +#[derive(Debug, Deserialize, Default)] +pub struct CommandPalette { + pub key: ContainedLabel, + pub keystroke_spacing: f32, +} + #[derive(Deserialize, Default)] pub struct ContactsPanel { #[serde(flatten)] @@ -262,7 +268,7 @@ pub struct ContainedText { pub text: TextStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default)] pub struct ContainedLabel { #[serde(flatten)] pub container: ContainerStyle, diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs index c3910dc4d2..219828b650 100644 --- a/crates/theme/src/theme_registry.rs +++ b/crates/theme/src/theme_registry.rs @@ -1,8 +1,8 @@ -use crate::{resolution::resolve_references, Theme}; +use crate::Theme; use anyhow::{Context, Result}; use gpui::{fonts, AssetSource, FontCache}; use parking_lot::Mutex; -use serde_json::{Map, Value}; +use serde_json::Value; use std::{collections::HashMap, sync::Arc}; pub struct ThemeRegistry { @@ -25,12 +25,8 @@ impl ThemeRegistry { pub fn list(&self) -> impl Iterator { 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> { - 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 = 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, extension: Map) { - 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> { - 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> { - self.0 - .iter() - .copied() - .filter_map(|(path, _)| { - if path.starts_with(prefix) { - Some(path.into()) - } else { - None - } - }) - .collect() - } - } } diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index ff3d50454f..804eff2c7a 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -11,9 +11,11 @@ doctest = false editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +picker = { path = "../picker" } theme = { path = "../theme" } +settings = { path = "../settings" } workspace = { path = "../workspace" } -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } smol = "1.2.5" diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index d61cac1c44..f1e933774f 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,44 +1,30 @@ -use editor::Editor; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - action, - elements::*, - keymap::{self, Binding}, - AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, - ViewContext, ViewHandle, + actions, elements::*, AppContext, Element, ElementBox, Entity, MutableAppContext, + RenderContext, View, ViewContext, ViewHandle, }; -use std::{cmp, sync::Arc}; +use picker::{Picker, PickerDelegate}; +use settings::Settings; +use std::sync::Arc; use theme::{Theme, ThemeRegistry}; -use workspace::{ - menu::{Confirm, SelectNext, SelectPrev}, - Settings, Workspace, -}; +use workspace::Workspace; pub struct ThemeSelector { - themes: Arc, + registry: Arc, + theme_names: Vec, matches: Vec, - query_editor: ViewHandle, - list_state: UniformListState, - selected_index: usize, original_theme: Arc, + picker: ViewHandle>, selection_completed: bool, + selected_index: usize, } -action!(Toggle, Arc); -action!(Reload, Arc); +actions!(theme_selector, [Toggle, Reload]); -pub fn init(themes: Arc, 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::::init(cx); } pub enum Event { @@ -47,44 +33,46 @@ pub enum Event { impl ThemeSelector { fn new(registry: Arc, cx: &mut ViewContext) -> 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::().theme.clone(); - + let theme_names = registry.list().collect::>(); + 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) { + fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + 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) { + fn reload(workspace: &mut Workspace, _: &Reload, cx: &mut ViewContext) { let current_theme_name = cx.global::().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.selection_completed = true; - cx.emit(Event::Dismissed); - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - 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) { - 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) { 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) { - 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::>(); - 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, @@ -197,84 +115,6 @@ impl ThemeSelector { } } - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::BufferEdited { .. } => { - self.update_matches(cx); - self.select_if_matching(&cx.global::().theme.name); - self.show_selected_theme(cx); - } - editor::Event::Blurred => cx.emit(Event::Dismissed), - _ => {} - } - } - - fn render_matches(&self, cx: &mut RenderContext) -> ElementBox { - if self.matches.is_empty() { - let settings = cx.global::(); - 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 = 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), - )); - }, - ); - - Container::new(list.boxed()) - .with_margin_top(6.0) - .named("matches") - } - - fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox { - let settings = cx.global::(); - let theme = &settings.theme; - - 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 - }); - - container.boxed() - } - fn set_theme(theme: Arc, cx: &mut MutableAppContext) { cx.update_global::(|settings, cx| { settings.theme = theme; @@ -283,6 +123,99 @@ impl ThemeSelector { } } +impl PickerDelegate for ThemeSelector { + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, cx: &mut ViewContext) { + self.selection_completed = true; + cx.emit(Event::Dismissed); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + if !self.selection_completed { + Self::set_theme(self.original_theme.clone(), cx); + self.selection_completed = true; + } + cx.emit(Event::Dismissed); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + self.selected_index = ix; + self.show_selected_theme(cx); + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> 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::>(); + + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.matches = matches; + this.selected_index = this + .selected_index + .min(this.matches.len().saturating_sub(1)); + this.show_selected_theme(cx); + cx.notify(); + }); + }) + } + + fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox { + let settings = cx.global::(); + 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() + } +} + impl Entity for ThemeSelector { type Event = Event; @@ -298,43 +231,11 @@ impl View for ThemeSelector { "ThemeSelector" } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().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) -> ElementBox { + ChildView::new(self.picker.clone()).boxed() } fn on_focus(&mut self, cx: &mut ViewContext) { - 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); } } diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 9d39fb04e2..cfc68e0b16 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -12,7 +12,7 @@ test-support = ["rand", "serde_json", "tempdir"] [dependencies] anyhow = "1.0.38" futures = "0.3" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } rand = { version = "0.8", optional = true } surf = "2.2" tempdir = { version = "0.3.7", optional = true } diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 28ee7de872..8e74898f4b 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -8,18 +8,22 @@ path = "src/vim.rs" doctest = false [dependencies] +assets = { path = "../assets" } collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +serde = { version = "1", features = ["derive"] } +settings = { path = "../settings" } workspace = { path = "../workspace" } -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } [dev-dependencies] indoc = "1.0.4" editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } +settings = { path = "../settings" } workspace = { path = "../workspace", features = ["test-support"] } \ No newline at end of file diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index e4035197fd..12b90ec8e6 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,7 +1,7 @@ use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased}; use gpui::MutableAppContext; -use crate::{mode::Mode, SwitchMode, VimState}; +use crate::{state::Mode, Vim}; pub fn init(cx: &mut MutableAppContext) { cx.subscribe_global(editor_created).detach(); @@ -11,9 +11,9 @@ pub fn init(cx: &mut MutableAppContext) { } fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { - cx.update_default_global(|vim_state: &mut VimState, cx| { - vim_state.editors.insert(editor.id(), editor.downgrade()); - vim_state.sync_editor_options(cx); + cx.update_default_global(|vim: &mut Vim, cx| { + vim.editors.insert(editor.id(), editor.downgrade()); + vim.sync_editor_options(cx); }) } @@ -21,17 +21,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) { Mode::Insert } else { - Mode::normal() + Mode::Normal }; - VimState::update_global(cx, |state, cx| { + Vim::update(cx, |state, cx| { state.active_editor = Some(editor.downgrade()); - state.switch_mode(&SwitchMode(mode), cx); + state.switch_mode(mode, cx); }); } fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { - VimState::update_global(cx, |state, cx| { + Vim::update(cx, |state, cx| { if let Some(previous_editor) = state.active_editor.clone() { if previous_editor == editor.clone() { state.active_editor = None; @@ -42,11 +42,11 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont } fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { - cx.update_default_global(|vim_state: &mut VimState, _| { - vim_state.editors.remove(&editor.id()); - if let Some(previous_editor) = vim_state.active_editor.clone() { + cx.update_default_global(|vim: &mut Vim, _| { + vim.editors.remove(&editor.id()); + if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { - vim_state.active_editor = None; + vim.active_editor = None; } } }); diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 9c1e36a90e..c98c9db841 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,47 +1,40 @@ +use crate::{state::Mode, Vim}; use editor::Bias; -use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use gpui::{actions, MutableAppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; -use crate::{mode::Mode, SwitchMode, VimState}; - -action!(NormalBefore); +actions!(vim, [NormalBefore]); pub fn init(cx: &mut MutableAppContext) { - let context = Some("Editor && vim_mode == insert"); - cx.add_bindings(vec![ - Binding::new("escape", NormalBefore, context), - Binding::new("ctrl-c", NormalBefore, context), - ]); - cx.add_action(normal_before); } fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { - 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"); } } diff --git a/crates/vim/src/mode.rs b/crates/vim/src/mode.rs deleted file mode 100644 index f00c14e2e8..0000000000 --- a/crates/vim/src/mode.rs +++ /dev/null @@ -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 - } -} diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs new file mode 100644 index 0000000000..530ee1d657 --- /dev/null +++ b/crates/vim/src/motion.rs @@ -0,0 +1,271 @@ +use editor::{ + char_kind, + display_map::{DisplaySnapshot, ToDisplayPoint}, + movement, Bias, DisplayPoint, +}; +use gpui::{actions, impl_actions, MutableAppContext}; +use language::SelectionGoal; +use serde::Deserialize; +use workspace::Workspace; + +use crate::{ + normal::normal_motion, + state::{Mode, Operator}, + Vim, +}; + +#[derive(Copy, Clone)] +pub enum Motion { + Left, + Down, + Up, + Right, + NextWordStart { + ignore_punctuation: bool, + stop_at_newline: bool, + }, + NextWordEnd { + ignore_punctuation: bool, + }, + PreviousWordStart { + ignore_punctuation: bool, + }, + StartOfLine, + EndOfLine, + StartOfDocument, + EndOfDocument, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NextWordStart { + #[serde(default)] + ignore_punctuation: bool, + #[serde(default)] + stop_at_newline: bool, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NextWordEnd { + #[serde(default)] + ignore_punctuation: bool, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PreviousWordStart { + #[serde(default)] + ignore_punctuation: bool, +} + +actions!( + vim, + [ + Left, + Down, + Up, + Right, + StartOfLine, + EndOfLine, + StartOfDocument, + EndOfDocument + ] +); +impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); + cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); + cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); + cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); + cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); + cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); + cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { + motion(Motion::StartOfDocument, cx) + }); + cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx)); + + cx.add_action( + |_: &mut Workspace, + &NextWordStart { + ignore_punctuation, + stop_at_newline, + }: &NextWordStart, + cx: _| { + motion( + Motion::NextWordStart { + ignore_punctuation, + stop_at_newline, + }, + cx, + ) + }, + ); + cx.add_action( + |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| { + motion(Motion::NextWordEnd { ignore_punctuation }, cx) + }, + ); + cx.add_action( + |_: &mut Workspace, + &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, + cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) }, + ); +} + +fn motion(motion: Motion, cx: &mut MutableAppContext) { + Vim::update(cx, |vim, cx| { + if let Some(Operator::Namespace(_)) = vim.active_operator() { + vim.pop_operator(cx); + } + }); + match Vim::read(cx).state.mode { + Mode::Normal => normal_motion(motion, cx), + Mode::Insert => { + // Shouldn't execute a motion in insert mode. Ignoring + } + } +} + +impl Motion { + pub fn move_point( + self, + map: &DisplaySnapshot, + point: DisplayPoint, + goal: SelectionGoal, + block_cursor_positioning: bool, + ) -> (DisplayPoint, SelectionGoal) { + use Motion::*; + match self { + Left => (left(map, point), SelectionGoal::None), + Down => movement::down(map, point, goal), + Up => movement::up(map, point, goal), + Right => (right(map, point), SelectionGoal::None), + NextWordStart { + ignore_punctuation, + stop_at_newline, + } => ( + next_word_start(map, point, ignore_punctuation, stop_at_newline), + SelectionGoal::None, + ), + NextWordEnd { ignore_punctuation } => ( + next_word_end(map, point, ignore_punctuation, block_cursor_positioning), + SelectionGoal::None, + ), + PreviousWordStart { ignore_punctuation } => ( + previous_word_start(map, point, ignore_punctuation), + SelectionGoal::None, + ), + StartOfLine => (start_of_line(map, point), SelectionGoal::None), + EndOfLine => (end_of_line(map, point), SelectionGoal::None), + StartOfDocument => (start_of_document(map, point), SelectionGoal::None), + EndOfDocument => (end_of_document(map, point), SelectionGoal::None), + } + } + + pub fn line_wise(self) -> bool { + use Motion::*; + match self { + Down | Up | StartOfDocument | EndOfDocument => true, + _ => false, + } + } +} + +fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + *point.column_mut() = point.column().saturating_sub(1); + map.clip_point(point, Bias::Left) +} + +fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + *point.column_mut() += 1; + map.clip_point(point, Bias::Right) +} + +fn next_word_start( + map: &DisplaySnapshot, + point: DisplayPoint, + ignore_punctuation: bool, + stop_at_newline: bool, +) -> DisplayPoint { + let mut crossed_newline = false; + movement::find_boundary(map, point, |left, right| { + let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + let at_newline = right == '\n'; + + let found = (left_kind != right_kind && !right.is_whitespace()) + || (at_newline && (crossed_newline || stop_at_newline)) + || (at_newline && left == '\n'); // Prevents skipping repeated empty lines + + if at_newline { + crossed_newline = true; + } + found + }) +} + +fn next_word_end( + map: &DisplaySnapshot, + mut point: DisplayPoint, + ignore_punctuation: bool, + before_end_character: bool, +) -> DisplayPoint { + *point.column_mut() += 1; + point = movement::find_boundary(map, point, |left, right| { + let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + + left_kind != right_kind && !left.is_whitespace() + }); + // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know + // we have backtraced already + if before_end_character + && !map + .chars_at(point) + .skip(1) + .next() + .map(|c| c == '\n') + .unwrap_or(true) + { + *point.column_mut() = point.column().saturating_sub(1); + } + map.clip_point(point, Bias::Left) +} + +fn previous_word_start( + map: &DisplaySnapshot, + mut point: DisplayPoint, + ignore_punctuation: bool, +) -> DisplayPoint { + // This works even though find_preceding_boundary is called for every character in the line containing + // cursor because the newline is checked only once. + point = movement::find_preceding_boundary(map, point, |left, right| { + let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation); + + (left_kind != right_kind && !right.is_whitespace()) || left == '\n' + }); + point +} + +fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + map.prev_line_boundary(point.to_point(map)).1 +} + +fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) +} + +fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let mut new_point = 0usize.to_display_point(map); + *new_point.column_mut() = point.column(); + map.clip_point(new_point, Bias::Left) +} + +fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + let mut new_point = map.max_point(); + *new_point.column_mut() = point.column(); + map.clip_point(new_point, Bias::Left) +} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 5bf1a3c417..d0ca7ae870 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,187 +1,100 @@ -mod g_prefix; - -use editor::{char_kind, movement, Bias}; -use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use crate::{ + motion::Motion, + state::{Mode, Operator}, + Vim, +}; +use editor::Bias; +use gpui::MutableAppContext; use language::SelectionGoal; -use workspace::Workspace; -use crate::{mode::NormalState, Mode, SwitchMode, VimState}; - -action!(GPrefix); -action!(MoveLeft); -action!(MoveDown); -action!(MoveUp); -action!(MoveRight); -action!(MoveToStartOfLine); -action!(MoveToEndOfLine); -action!(MoveToEnd); -action!(MoveToNextWordStart, bool); -action!(MoveToNextWordEnd, bool); -action!(MoveToPreviousWordStart, bool); - -pub fn init(cx: &mut MutableAppContext) { - let context = Some("Editor && vim_mode == normal"); - cx.add_bindings(vec![ - Binding::new("i", SwitchMode(Mode::Insert), context), - Binding::new("g", SwitchMode(Mode::Normal(NormalState::GPrefix)), context), - Binding::new("h", MoveLeft, context), - Binding::new("j", MoveDown, context), - Binding::new("k", MoveUp, context), - Binding::new("l", MoveRight, context), - Binding::new("0", MoveToStartOfLine, context), - Binding::new("shift-$", MoveToEndOfLine, context), - Binding::new("shift-G", MoveToEnd, context), - Binding::new("w", MoveToNextWordStart(false), context), - Binding::new("shift-W", MoveToNextWordStart(true), context), - Binding::new("e", MoveToNextWordEnd(false), context), - Binding::new("shift-E", MoveToNextWordEnd(true), context), - Binding::new("b", MoveToPreviousWordStart(false), context), - Binding::new("shift-B", MoveToPreviousWordStart(true), context), - ]); - g_prefix::init(cx); - - cx.add_action(move_left); - cx.add_action(move_down); - cx.add_action(move_up); - cx.add_action(move_right); - cx.add_action(move_to_start_of_line); - cx.add_action(move_to_end_of_line); - cx.add_action(move_to_end); - cx.add_action(move_to_next_word_start); - cx.add_action(move_to_next_word_end); - cx.add_action(move_to_previous_word_start); +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_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { - 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_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 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; + } }); - }); - }) -} - -fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext) { - VimState::update_global(cx, |state, cx| { - state.update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, movement::down); + editor.set_clip_at_line_ends(true, cx); + editor.insert(&"", cx); }); }); + vim.switch_mode(Mode::Insert, cx) } -fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext) { - VimState::update_global(cx, |state, cx| { - state.update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, movement::up); - }); - }); -} +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())); -fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext) { - 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) - }); - }); - }); -} - -fn move_to_start_of_line( - _: &mut Workspace, - _: &MoveToStartOfLine, - cx: &mut ViewContext, -) { - 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_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext) { - 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 move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext) { - 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, -) { - 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; + 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) + } }); - }); - }); -} + editor.insert(&"", cx); -fn move_to_next_word_end( - _: &mut Workspace, - &MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd, - cx: &mut ViewContext, -) { - 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); + // 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, -) { - 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, + ); + } } diff --git a/crates/vim/src/normal/g_prefix.rs b/crates/vim/src/normal/g_prefix.rs deleted file mode 100644 index 5b71089245..0000000000 --- a/crates/vim/src/normal/g_prefix.rs +++ /dev/null @@ -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) { - 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()); - } -} diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs new file mode 100644 index 0000000000..73769eafbc --- /dev/null +++ b/crates/vim/src/state.rs @@ -0,0 +1,82 @@ +use editor::CursorShape; +use gpui::keymap::Context; +use serde::Deserialize; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] +pub enum Mode { + Normal, + Insert, +} + +impl Default for Mode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] +pub enum Namespace { + G, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] +pub enum Operator { + Namespace(Namespace), + Change, + Delete, +} + +#[derive(Default)] +pub struct VimState { + pub mode: Mode, + pub operator_stack: Vec, +} + +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()); + } +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 609843d969..65acce7a42 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1,45 +1,63 @@ mod editor_events; mod insert; -mod mode; +mod motion; mod normal; +mod state; #[cfg(test)] mod vim_test_context; use collections::HashMap; use editor::{CursorShape, Editor}; -use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle}; +use gpui::{impl_actions, MutableAppContext, ViewContext, WeakViewHandle}; +use serde::Deserialize; -use mode::Mode; -use workspace::{self, Settings, Workspace}; +use settings::Settings; +use state::{Mode, Operator, VimState}; +use workspace::{self, Workspace}; -action!(SwitchMode, Mode); +#[derive(Clone, Deserialize)] +pub struct SwitchMode(pub Mode); + +#[derive(Clone, Deserialize)] +pub struct PushOperator(pub Operator); + +impl_actions!(vim, [SwitchMode, PushOperator]); pub fn init(cx: &mut MutableAppContext) { editor_events::init(cx); insert::init(cx); - normal::init(cx); + motion::init(cx); - cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| { - VimState::update_global(cx, |state, cx| state.switch_mode(action, cx)) + cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| { + Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx)) }); + cx.add_action( + |_: &mut Workspace, &PushOperator(operator): &PushOperator, cx| { + Vim::update(cx, |vim, cx| vim.push_operator(operator, cx)) + }, + ); cx.observe_global::(|settings, 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>, active_editor: Option>, enabled: bool, - mode: Mode, + state: VimState, } -impl VimState { - fn update_global(cx: &mut MutableAppContext, update: F) -> S +impl Vim { + fn read(cx: &mut MutableAppContext) -> &Self { + cx.default_global() + } + + fn update(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 { + 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::(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); } } diff --git a/crates/vim/src/vim_test_context.rs b/crates/vim/src/vim_test_context.rs index 91acc8de6c..1e10b5e206 100644 --- a/crates/vim/src/vim_test_context.rs +++ b/crates/vim/src/vim_test_context.rs @@ -6,7 +6,7 @@ use language::{Point, Selection}; use util::test::marked_text; use workspace::{WorkspaceHandle, WorkspaceParams}; -use crate::*; +use crate::{state::Operator, *}; pub struct VimTestContext<'a> { cx: &'a mut gpui::TestAppContext, @@ -23,7 +23,10 @@ impl<'a> VimTestContext<'a> { cx.update(|cx| { editor::init(cx); crate::init(cx); + + settings::KeymapFile::load("keymaps/vim.json", cx).unwrap(); }); + let params = cx.update(WorkspaceParams::test); cx.update(|cx| { @@ -97,7 +100,12 @@ impl<'a> VimTestContext<'a> { } pub fn mode(&mut self) -> Mode { - self.cx.update(|cx| cx.global::().mode) + self.cx.read(|cx| cx.global::().state.mode) + } + + pub fn active_operator(&mut self) -> Option { + self.cx + .read(|cx| cx.global::().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(&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( + &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> { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 7ef0bd8543..4260174644 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -8,7 +8,7 @@ path = "src/workspace.rs" doctest = false [features] -test-support = ["client/test-support", "project/test-support"] +test-support = ["client/test-support", "project/test-support", "settings/test-support"] [dependencies] client = { path = "../client" } @@ -17,14 +17,14 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } anyhow = "1.0.38" futures = "0.3" -log = "0.4" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } -schemars = "0.8" serde = { version = "1", features = ["derive", "rc"] } serde_json = { version = "1", features = ["preserve_order"] } smallvec = { version = "1.6", features = ["union"] } @@ -33,3 +33,4 @@ smallvec = { version = "1.6", features = ["union"] } client = { path = "../client", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } \ No newline at end of file diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index a12f81857f..1accbcad3c 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,18 +1,19 @@ -use crate::{ItemHandle, Settings, StatusItemView}; +use crate::{ItemHandle, StatusItemView}; use futures::StreamExt; -use gpui::AppContext; +use gpui::{actions, AppContext}; use gpui::{ - action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, - RenderContext, View, ViewContext, + elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext, + View, ViewContext, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use project::{LanguageServerProgress, Project}; +use settings::Settings; use smallvec::SmallVec; use std::cmp::Reverse; use std::fmt::Write; use std::sync::Arc; -action!(DismissErrorMessage); +actions!(lsp_status, [DismissErrorMessage]); pub struct LspStatus { checking_for_update: Vec, diff --git a/crates/workspace/src/menu.rs b/crates/workspace/src/menu.rs index e4ce82276a..c37ad530bb 100644 --- a/crates/workspace/src/menu.rs +++ b/crates/workspace/src/menu.rs @@ -1,19 +1,16 @@ -use gpui::{action, keymap::Binding, MutableAppContext}; +#[derive(Clone)] +pub struct SelectIndex(pub usize); -action!(Confirm); -action!(SelectPrev); -action!(SelectNext); -action!(SelectFirst); -action!(SelectLast); +gpui::actions!( + menu, + [ + Cancel, + Confirm, + SelectPrev, + SelectNext, + SelectFirst, + SelectLast + ] +); -pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("up", SelectPrev, Some("menu")), - Binding::new("ctrl-p", SelectPrev, Some("menu")), - Binding::new("down", SelectNext, Some("menu")), - Binding::new("ctrl-n", SelectNext, Some("menu")), - Binding::new("cmd-up", SelectFirst, Some("menu")), - Binding::new("cmd-down", SelectLast, Some("menu")), - Binding::new("enter", Confirm, Some("menu")), - ]); -} +gpui::impl_internal_actions!(menu, [SelectIndex]); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1421072b13..a992897c11 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,29 +1,59 @@ use super::{ItemHandle, SplitDirection}; -use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace}; +use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace}; +use anyhow::Result; use collections::{HashMap, VecDeque}; use futures::StreamExt; use gpui::{ - action, + actions, elements::*, geometry::{rect::RectF, vector::vec2f}, - keymap::Binding, + impl_actions, impl_internal_actions, platform::{CursorStyle, NavigationDirection}, - AppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, RenderContext, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Project, ProjectEntryId, ProjectPath}; +use project::{ProjectEntryId, ProjectPath}; +use serde::Deserialize; +use settings::Settings; use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; use util::ResultExt; -action!(Split, SplitDirection); -action!(ActivateItem, usize); -action!(ActivatePrevItem); -action!(ActivateNextItem); -action!(CloseActiveItem); -action!(CloseInactiveItems); -action!(CloseItem, usize); -action!(GoBack, Option>); -action!(GoForward, Option>); +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, +} + +#[derive(Clone, Deserialize)] +pub struct ActivateItem(pub usize); + +#[derive(Clone, Deserialize)] +pub struct GoBack { + #[serde(skip_deserializing)] + pub pane: Option>, +} + +#[derive(Clone, Deserialize)] +pub struct GoForward { + #[serde(skip_deserializing)] + pub pane: Option>, +} + +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>, active_item_index: usize, + autoscroll: bool, nav_history: Rc>, toolbar: ViewHandle, - project: ModelHandle, } pub struct ItemNavHistory { @@ -134,13 +148,13 @@ pub struct NavigationEntry { } impl Pane { - pub fn new(project: ModelHandle, cx: &mut ViewContext) -> Self { + pub fn new(cx: &mut ViewContext) -> 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_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 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 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) { + 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) -> Task<()> { - if self.items.is_empty() { - Task::ready(()) + fn close_active_item( + workspace: &mut Workspace, + _: &CloseActiveItem, + cx: &mut ViewContext, + ) -> Option>> { + 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) -> Task<()> { - if self.items.is_empty() { - Task::ready(()) + pub fn close_inactive_items( + workspace: &mut Workspace, + _: &CloseInactiveItems, + cx: &mut ViewContext, + ) -> Option>> { + 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) -> Task<()> { - self.close_items(cx, move |view_id| view_id == view_id_to_close) + pub fn close_item( + workspace: &mut Workspace, + pane: ViewHandle, + item_id_to_close: usize, + cx: &mut ViewContext, + ) -> Task> { + Self::close_items(workspace, pane, cx, move |view_id| { + view_id == item_id_to_close + }) } pub fn close_items( - &mut self, - cx: &mut ViewContext, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, should_close: impl 'static + Fn(usize) -> bool, - ) -> Task<()> { + ) -> Task> { 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()); + 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"], + ) + }); - 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) => { + 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) -> ElementBox { + fn render_tabs(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = cx.global::().theme.clone(); enum Tabs {} + let pane = cx.handle(); let tabs = MouseEventHandler::new::(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::(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(); - 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).reload_count, 0); - assert_eq!(pane.items.len(), 2); - assert_eq!(pane.active_item().unwrap().id(), item5.id()); - }); - cx.simulate_prompt_answer(window_id, 0); cx.foreground().run_until_parked(); cx.simulate_new_path_selection(|_| Some(Default::default())); - close_items.await; + close_items.await.unwrap(); 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!(item4.read(cx).save_count, 0); + assert_eq!(item4.read(cx).save_as_count, 1); + assert_eq!(item4.read(cx).reload_count, 0); assert_eq!(pane.items.len(), 1); assert_eq!(pane.active_item().unwrap().id(), item2.id()); }); } + #[gpui::test] + async fn test_prompting_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_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, } 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 { - None + self.project_entry_id } fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext) {} + fn clone_on_split(&self, _: &mut ViewContext) -> Option + where + Self: Sized, + { + Some(self.clone()) + } + fn is_dirty(&self, _: &AppContext) -> bool { self.is_dirty } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 258d644148..a17805822f 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -4,6 +4,7 @@ use client::PeerId; use collections::HashMap; use gpui::{elements::*, Axis, Border, ViewHandle}; use project::Collaborator; +use serde::Deserialize; use theme::Theme; #[derive(Clone, Debug, Eq, PartialEq)] @@ -254,7 +255,7 @@ impl PaneAxis { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Deserialize)] pub enum SplitDirection { Up, Down, diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs deleted file mode 100644 index 5ccf8056e6..0000000000 --- a/crates/workspace/src/settings.rs +++ /dev/null @@ -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, LanguageOverride>, - pub theme: Arc, -} - -#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] -pub struct LanguageOverride { - pub tab_size: Option, - pub soft_wrap: Option, - pub preferred_line_length: Option, -} - -#[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); - -#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] -struct SettingsFileContent { - #[serde(default)] - buffer_font_family: Option, - #[serde(default)] - buffer_font_size: Option, - #[serde(default)] - vim_mode: Option, - #[serde(flatten)] - editor: LanguageOverride, - #[serde(default)] - language_overrides: HashMap, LanguageOverride>, - #[serde(default)] - theme: Option, -} - -impl SettingsFile { - pub async fn new( - fs: Arc, - executor: &executor::Background, - path: impl Into>, - ) -> 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, path: &Path) -> Option { - 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, - theme_registry: Arc, - font_cache: Arc, - ) -> impl futures::stream::Stream { - 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, - ) -> Result { - 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>, - overrides: LanguageOverride, - ) -> Self { - self.language_overrides - .insert(language_name.into(), overrides); - self - } - - pub fn tab_size(&self, language: Option<&Arc>) -> 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>) -> 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>) -> 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(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } -} - -fn merge_option(target: &mut Option, value: Option) { - 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); - } -} diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 7a7ad4e272..cd5ec32201 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -1,5 +1,6 @@ use super::Workspace; -use gpui::{action, elements::*, platform::CursorStyle, AnyViewHandle, RenderContext}; +use gpui::{elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, RenderContext}; +use serde::Deserialize; use std::{cell::RefCell, rc::Rc}; use theme::Theme; @@ -10,7 +11,7 @@ pub struct Sidebar { width: Rc>, } -#[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, diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index a91dd645a0..c9a2c819e2 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,4 +1,5 @@ -use crate::{ItemHandle, Pane, Settings}; +use crate::{ItemHandle, Pane}; +use settings::Settings; use gpui::{ elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 8212b25082..e9b20bf3a0 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -1,8 +1,9 @@ -use crate::{ItemHandle, Settings}; +use crate::ItemHandle; use gpui::{ elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; +use settings::Settings; pub trait ToolbarItemView: View { fn set_active_pane_item( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c79713a353..5be00513a5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2,7 +2,6 @@ pub mod lsp_status; pub mod menu; pub mod pane; pub mod pane_group; -pub mod settings; pub mod sidebar; mod status_bar; mod toolbar; @@ -14,16 +13,16 @@ use client::{ use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; use gpui::{ - action, + actions, color::Color, elements::*, - geometry::{vector::vec2f, PathBuilder}, - json::{self, to_string_pretty, ToJson}, - keymap::Binding, + geometry::{rect::RectF, vector::vec2f, PathBuilder}, + impl_internal_actions, + json::{self, ToJson}, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity, - ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, - View, ViewContext, ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, + ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -31,8 +30,8 @@ pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree}; -pub use settings::Settings; -use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus}; +use settings::Settings; +use sidebar::{Side, Sidebar, ToggleSidebarItem, ToggleSidebarItemFocus}; use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ @@ -70,32 +69,56 @@ type FollowableItemBuilders = HashMap< ), >; -action!(Open, Arc); -action!(OpenNew, Arc); -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); + +#[derive(Clone)] +pub struct OpenNew(pub Arc); + +#[derive(Clone)] +pub struct OpenPaths { + pub paths: Vec, + pub app_state: Arc, +} + +#[derive(Clone)] +pub struct ToggleFollow(pub PeerId); + +#[derive(Clone)] +pub struct JoinProject { + pub project_id: u64, + pub app_state: Arc, +} + +impl_internal_actions!( + workspace, + [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject] +); pub fn init(client: &Arc, 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, 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, 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, &Arc, &mut ViewContext) -> Workspace, } -#[derive(Clone)] -pub struct OpenParams { - pub paths: Vec, - pub app_state: Arc, -} - -#[derive(Clone)] -pub struct JoinProjectParams { - pub project_id: u64, - pub app_state: Arc, -} - pub trait Item: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { @@ -386,6 +373,11 @@ pub trait ItemHandle: 'static + fmt::Debug { -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; + fn on_release( + &self, + cx: &mut MutableAppContext, + callback: Box, + ) -> gpui::Subscription; } pub trait WeakItemHandle { @@ -421,6 +413,12 @@ impl ItemHandle for ViewHandle { Box::new(self.clone()) } + fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext) { + self.update(cx, |item, cx| { + item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx); + }) + } + fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { self.update(cx, |item, cx| { cx.add_option_view(|cx| item.clone_on_split(cx)) @@ -428,12 +426,6 @@ impl ItemHandle for ViewHandle { .map(|handle| Box::new(handle) as Box) } - fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext) { - self.update(cx, |item, cx| { - item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx); - }) - } - fn added_to_pane( &self, workspace: &mut Workspace, @@ -494,8 +486,7 @@ impl ItemHandle for ViewHandle { } 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 ItemHandle for ViewHandle { 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, cx: &mut MutableAppContext) -> Task> { self.update(cx, |item, cx| item.save(project, cx)) } @@ -544,30 +559,6 @@ impl ItemHandle for ViewHandle { 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 { self.read(cx).act_as_type(type_id, self, cx) } @@ -581,6 +572,14 @@ impl ItemHandle for ViewHandle { None } } + + fn on_release( + &self, + cx: &mut MutableAppContext, + callback: Box, + ) -> gpui::Subscription { + cx.observe_release(self, move |_, cx| callback(cx)) + } } impl Into for Box { @@ -611,6 +610,7 @@ pub struct WorkspaceParams { pub client: Arc, pub fs: Arc, pub languages: Arc, + pub themes: Arc, pub user_store: ModelHandle, pub channel_list: ModelHandle, } @@ -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, remote_entity_subscription: Option, fs: Arc, + themes: Arc, modal: Option, 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 { + 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) { - 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) -> ViewHandle { - 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, cx: &mut MutableAppContext, -) -> Task> { +) -> Task<( + ViewHandle, + Vec, Arc>>>, +)> { 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) }) } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 71f65bd417..0d4d43c414 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] 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" diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml deleted file mode 100644 index 890825992b..0000000000 --- a/crates/zed/assets/themes/_base.toml +++ /dev/null @@ -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" } diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml deleted file mode 100644 index 34de16627e..0000000000 --- a/crates/zed/assets/themes/black.toml +++ /dev/null @@ -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" diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml deleted file mode 100644 index fa673ac446..0000000000 --- a/crates/zed/assets/themes/dark.toml +++ /dev/null @@ -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" diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml deleted file mode 100644 index 2884515e09..0000000000 --- a/crates/zed/assets/themes/light.toml +++ /dev/null @@ -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" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 75a5030ec6..e90a1cd737 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -47,6 +47,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi tree_sitter_typescript::language_typescript(), Some(Arc::new(typescript::TypeScriptLspAdapter)), ), + ( + "javascript", + tree_sitter_typescript::language_tsx(), + Some(Arc::new(typescript::TypeScriptLspAdapter)), + ), ] { languages.add(Arc::new(language(name, grammar, lsp_adapter))); } diff --git a/crates/zed/src/languages/javascript/brackets.scm b/crates/zed/src/languages/javascript/brackets.scm new file mode 100644 index 0000000000..63395f81d8 --- /dev/null +++ b/crates/zed/src/languages/javascript/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml new file mode 100644 index 0000000000..3688da705c --- /dev/null +++ b/crates/zed/src/languages/javascript/config.toml @@ -0,0 +1,12 @@ +name = "JavaScript" +path_suffixes = ["js", "jsx"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/src/languages/javascript/highlights.scm b/crates/zed/src/languages/javascript/highlights.scm new file mode 100644 index 0000000000..cb4e82b33d --- /dev/null +++ b/crates/zed/src/languages/javascript/highlights.scm @@ -0,0 +1,219 @@ +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Special identifiers + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^[A-Z_][A-Z\\d_]+$")) + +; Literals + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + ";" + "?." + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + +; Types + +(type_identifier) @type +(predefined_type) @type.builtin + +((identifier) @type + (#match? @type "^[A-Z]")) + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" +] @keyword \ No newline at end of file diff --git a/crates/zed/src/languages/javascript/indents.scm b/crates/zed/src/languages/javascript/indents.scm new file mode 100644 index 0000000000..107e6ff8e0 --- /dev/null +++ b/crates/zed/src/languages/javascript/indents.scm @@ -0,0 +1,15 @@ +[ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/javascript/outline.scm b/crates/zed/src/languages/javascript/outline.scm new file mode 100644 index 0000000000..f8691fa41d --- /dev/null +++ b/crates/zed/src/languages/javascript/outline.scm @@ -0,0 +1,55 @@ +(internal_module + "namespace" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item + +(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(interface_declaration + "interface" @context + name: (_) @name) @item + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(class_declaration + "class" @context + name: (_) @name) @item + +(method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(public_field_definition + [ + "declare" + "readonly" + "abstract" + "static" + (accessibility_modifier) + ]* @context + name: (_) @name) @item diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index a6f4a6d2d0..62717266df 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -1,5 +1,5 @@ name = "TSX" -path_suffixes = ["tsx", "js"] +path_suffixes = ["tsx"] line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fe8fadd13d..371cb5a506 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -2,30 +2,38 @@ #![allow(non_snake_case)] use anyhow::{anyhow, Context, Result}; +use assets::Assets; +use cli::{ + ipc::{self, IpcSender}, + CliRequest, CliResponse, IpcHandshake, +}; use client::{self, http, ChannelList, UserStore}; use fs::OpenOptions; -use futures::{channel::oneshot, StreamExt}; -use gpui::{App, AssetSource, Task}; +use futures::{ + channel::{mpsc, oneshot}, + FutureExt, SinkExt, StreamExt, +}; +use gpui::{App, AssetSource, AsyncAppContext, Task}; use log::LevelFilter; use parking_lot::Mutex; use project::Fs; +use settings::{self, KeymapFile, Settings, SettingsFileContent}; use smol::process::Command; -use std::{env, fs, path::PathBuf, sync::Arc}; +use std::{env, fs, path::PathBuf, sync::Arc, thread, time::Duration}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use util::ResultExt; -use workspace::{ - self, - settings::{self, SettingsFile}, - AppState, OpenNew, OpenParams, OpenPaths, Settings, -}; +use workspace::{self, AppState, OpenNew, OpenPaths}; use zed::{ - self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, + self, build_window_options, build_workspace, + fs::RealFs, + languages, menus, + settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile}, }; fn main() { init_logger(); - let app = gpui::App::new(Assets).unwrap(); + let mut app = gpui::App::new(Assets).unwrap(); load_embedded_fonts(&app); let fs = Arc::new(RealFs); @@ -46,8 +54,37 @@ fn main() { soft_wrap: Some(settings::SoftWrap::PreferredLineLength), ..Default::default() }, + ) + .with_overrides( + "Rust", + settings::LanguageOverride { + tab_size: Some(4), + ..Default::default() + }, + ) + .with_overrides( + "JavaScript", + settings::LanguageOverride { + tab_size: Some(2), + ..Default::default() + }, + ) + .with_overrides( + "TypeScript", + settings::LanguageOverride { + tab_size: Some(2), + ..Default::default() + }, + ) + .with_overrides( + "TSX", + settings::LanguageOverride { + tab_size: Some(2), + ..Default::default() + }, ); - let settings_file = load_settings_file(&app, fs.clone()); + + let config_files = load_config_files(&app, fs.clone()); let login_shell_env_loaded = if stdout_is_a_pty() { Task::ready(()) @@ -57,6 +94,18 @@ fn main() { }) }; + let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded(); + app.on_open_urls(move |urls, _| { + if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { + if let Some(cli_connection) = connect_to_cli(server_name).log_err() { + cli_connections_tx + .unbounded_send(cli_connection) + .map_err(|_| anyhow!("no listener for cli connections")) + .log_err(); + }; + } + }); + app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); @@ -69,6 +118,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); + command_palette::init(cx); workspace::init(&client, cx); editor::init(cx); go_to_line::init(cx); @@ -97,13 +147,16 @@ fn main() { }) .detach_and_log_err(cx); - let settings_file = cx.background().block(settings_file).unwrap(); - let mut settings_rx = Settings::from_files( + let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); + let mut settings_rx = settings_from_files( default_settings, vec![settings_file], themes.clone(), cx.font_cache().clone(), ); + + cx.spawn(|cx| watch_keymap_file(keymap_file, cx)).detach(); + let settings = cx.background().block(settings_rx.next()).unwrap(); cx.spawn(|mut cx| async move { while let Some(settings) = settings_rx.next().await { @@ -130,20 +183,32 @@ fn main() { build_workspace, }); journal::init(app_state.clone(), cx); + theme_selector::init(cx); zed::init(&app_state, cx); - theme_selector::init(app_state.themes.clone(), cx); cx.set_menus(menus::menus(&app_state.clone())); if stdout_is_a_pty() { cx.platform().activate(true); - } - - let paths = collect_path_args(); - if paths.is_empty() { - cx.dispatch_global_action(OpenNew(app_state.clone())); + let paths = collect_path_args(); + if paths.is_empty() { + cx.dispatch_global_action(OpenNew(app_state.clone())); + } else { + cx.dispatch_global_action(OpenPaths { paths, app_state }); + } } else { - cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })); + if let Ok(Some(connection)) = cli_connections_rx.try_next() { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } else { + cx.dispatch_global_action(OpenNew(app_state.clone())); + } + cx.spawn(|cx| async move { + while let Some(connection) = cli_connections_rx.next().await { + handle_cli_connection(connection, app_state.clone(), cx.clone()).await; + } + }) + .detach(); } }); } @@ -239,15 +304,137 @@ fn load_embedded_fonts(app: &App) { .unwrap(); } -fn load_settings_file(app: &App, fs: Arc) -> oneshot::Receiver { +fn load_config_files( + app: &App, + fs: Arc, +) -> oneshot::Receiver<( + WatchedJsonFile, + WatchedJsonFile, +)> { 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, IpcSender)> { + let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) + .context("error connecting to cli")?; + let (request_tx, request_rx) = ipc::channel::()?; + let (response_tx, response_rx) = ipc::channel::()?; + + 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::(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, IpcSender), + app_state: Arc, + 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(); + } + } + } +} diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 8d23dc4f58..3f19dcbdac 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -16,7 +16,13 @@ pub fn menus(state: &Arc) -> Vec> { 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 { diff --git a/crates/zed/src/settings_file.rs b/crates/zed/src/settings_file.rs new file mode 100644 index 0000000000..d805e7490d --- /dev/null +++ b/crates/zed/src/settings_file.rs @@ -0,0 +1,171 @@ +use futures::{stream, StreamExt}; +use gpui::{executor, AsyncAppContext, FontCache}; +use postage::sink::Sink as _; +use postage::{prelude::Stream, watch}; +use project::Fs; +use serde::Deserialize; +use settings::{KeymapFile, Settings, SettingsFileContent}; +use std::{path::Path, sync::Arc, time::Duration}; +use theme::ThemeRegistry; +use util::ResultExt; + +#[derive(Clone)] +pub struct WatchedJsonFile(watch::Receiver); + +impl WatchedJsonFile +where + T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync, +{ + pub async fn new( + fs: Arc, + executor: &executor::Background, + path: impl Into>, + ) -> 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, path: &Path) -> Option { + 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>, + theme_registry: Arc, + font_cache: Arc, +) -> impl futures::stream::Stream { + 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, 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); + } +} diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index a48e3d461e..d4f24297e4 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -1,11 +1,12 @@ -use crate::{assets::Assets, build_window_options, build_workspace, AppState}; +use crate::{build_window_options, build_workspace, AppState}; +use assets::Assets; use client::{test::FakeHttpClient, ChannelList, Client, UserStore}; use gpui::MutableAppContext; use language::LanguageRegistry; use project::fs::FakeFs; +use settings::Settings; use std::sync::Arc; use theme::ThemeRegistry; -use workspace::Settings; #[cfg(test)] #[ctor::ctor] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6d904842be..14816d22b0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,21 +1,22 @@ -pub mod assets; pub mod languages; pub mod menus; +pub mod settings_file; #[cfg(any(test, feature = "test-support"))] pub mod test; +use anyhow::{anyhow, Context, Result}; use breadcrumbs::Breadcrumbs; use chat_panel::ChatPanel; pub use client; pub use contacts_panel; use contacts_panel::ContactsPanel; pub use editor; +use editor::Editor; use gpui::{ - action, + actions, geometry::vector::vec2f, - keymap::Binding, platform::{WindowBounds, WindowOptions}, - ModelHandle, ViewContext, + AsyncAppContext, ModelHandle, ViewContext, }; use lazy_static::lazy_static; pub use lsp; @@ -23,15 +24,28 @@ use project::Project; pub use project::{self, fs}; use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; -use std::{path::PathBuf, sync::Arc}; +use serde_json::to_string_pretty; +use settings::Settings; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; pub use workspace; -use workspace::{AppState, Settings, Workspace, WorkspaceParams}; +use workspace::{AppState, Workspace, WorkspaceParams}; -action!(About); -action!(Quit); -action!(OpenSettings); -action!(AdjustBufferFontSize, f32); -action!(CheckForUpdates); +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, 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, 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, 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, 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| { @@ -93,14 +113,32 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { .detach_and_log_err(cx); } }); + cx.add_action( + |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + 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::().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::().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>> = Default::default(); static ref FONTS: Vec>> = 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(), ]; } diff --git a/script/build-css b/script/build-css index 5dc9b33ec8..08b205495d 100755 --- a/script/build-css +++ b/script/build-css @@ -7,4 +7,4 @@ cd ./script if [[ $1 == --release ]]; then export NODE_ENV=production # Purge unused styles in --release mode fi -npx tailwindcss build ../crates/server/styles.css --output ../crates/server/static/styles.css +npx tailwindcss build ../crates/collab/styles.css --output ../crates/collab/static/styles.css diff --git a/script/build-themes b/script/build-themes new file mode 100755 index 0000000000..aef3a4250c --- /dev/null +++ b/script/build-themes @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +cd styles +npm install +npm run build diff --git a/script/bundle b/script/bundle index bcaa68c1e9..fc6bab355f 100755 --- a/script/bundle +++ b/script/bundle @@ -4,24 +4,33 @@ set -e export ZED_BUNDLE=true -# Install cargo-bundle 0.5.0 if it's not already installed +echo "Installing cargo bundle" cargo install cargo-bundle --version 0.5.0 # Deal with versions of macOS that don't include libstdc++ headers export CXXFLAGS="-stdlib=libc++" -# Build the app bundle for x86_64 -pushd crates/zed > /dev/null -cargo bundle --release --target x86_64-apple-darwin -popd > /dev/null +echo "Compiling binaries" +cargo build --release --package zed --target aarch64-apple-darwin +cargo build --release --package zed --target x86_64-apple-darwin +cargo build --release --package cli --target aarch64-apple-darwin +cargo build --release --package cli --target x86_64-apple-darwin -# Build the binary for aarch64 (Apple M1) -cargo build --release --target aarch64-apple-darwin +echo "Creating application bundle" +(cd crates/zed && cargo bundle --release --target x86_64-apple-darwin) -# Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries -lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed +echo "Creating fat binaries" +lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/Zed \ + -output \ + target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed +lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/release/cli \ + -output \ + target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/cli -# Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now. if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" security create-keychain -p $MACOS_CERTIFICATE_PASSWORD zed.keychain || echo "" @@ -39,7 +48,6 @@ else codesign --force --deep --sign - target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v fi -# Create a DMG echo "Creating DMG" mkdir -p target/release hdiutil create -volname Zed -srcfolder target/x86_64-apple-darwin/release/bundle/osx -ov -format UDZO target/release/Zed.dmg diff --git a/script/deploy b/script/deploy index ca32576281..ce50737170 100755 --- a/script/deploy +++ b/script/deploy @@ -16,7 +16,7 @@ if [[ $# < 1 ]]; then fi export ZED_KUBE_NAMESPACE=$1 -ENV_FILE="crates/server/k8s/environments/${ZED_KUBE_NAMESPACE}.sh" +ENV_FILE="crates/collab/k8s/environments/${ZED_KUBE_NAMESPACE}.sh" if [[ ! -f $ENV_FILE ]]; then echo "Invalid environment name '${ZED_KUBE_NAMESPACE}'" exit 1 @@ -28,10 +28,10 @@ if [[ $ZED_KUBE_NAMESPACE == "production" && -n $(git status --short) ]]; then fi git_sha=$(git rev-parse HEAD) -export ZED_IMAGE_ID="registry.digitalocean.com/zed/zed-server:${ZED_KUBE_NAMESPACE}-${git_sha}" +export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${ZED_KUBE_NAMESPACE}-${git_sha}" export $(cat $ENV_FILE) docker build . --tag "$ZED_IMAGE_ID" docker push "$ZED_IMAGE_ID" -envsubst < crates/server/k8s/manifest.template.yml | kubectl apply -f - +envsubst < crates/collab/k8s/manifest.template.yml | kubectl apply -f - diff --git a/script/seed-db b/script/seed-db index 437d1bcac8..c69af799dd 100755 --- a/script/seed-db +++ b/script/seed-db @@ -1,9 +1,9 @@ #!/bin/bash set -e -cd crates/server +cd crates/collab # Export contents of .env.toml eval "$(cargo run --bin dotenv)" -cargo run --package=zed-server --features seed-support --bin seed +cargo run --package=collab --features seed-support --bin seed diff --git a/script/sqlx b/script/sqlx index 3d3ea00cc4..2241f4fa9a 100755 --- a/script/sqlx +++ b/script/sqlx @@ -5,7 +5,7 @@ set -e # Install sqlx-cli if needed [[ "$(sqlx --version)" == "sqlx-cli 0.5.7" ]] || cargo install sqlx-cli --version 0.5.7 -cd crates/server +cd crates/collab # Export contents of .env.toml eval "$(cargo run --bin dotenv)" diff --git a/script/tailwind.config.js b/script/tailwind.config.js index f237516eff..3c1670b0a4 100644 --- a/script/tailwind.config.js +++ b/script/tailwind.config.js @@ -40,7 +40,7 @@ module.exports = { }, darkMode: false, purge: [ - "../crates/server/templates/**/*.hbs", - "../crates/server/templates/*.hbs" + "../crates/collab/templates/**/*.hbs", + "../crates/collab/templates/*.hbs" ] } \ No newline at end of file diff --git a/styles/.gitignore b/styles/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/styles/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/styles/dist/core.json b/styles/dist/core.json new file mode 100644 index 0000000000..f44b1d210c --- /dev/null +++ b/styles/dist/core.json @@ -0,0 +1,1155 @@ +{ + "color": { + "neutral": { + "0": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "25": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + }, + "50": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "75": { + "value": "#eaeaea", + "step": 75, + "type": "color" + }, + "100": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "125": { + "value": "#dcdcdc", + "step": 125, + "type": "color" + }, + "150": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "175": { + "value": "#cdcdcd", + "step": 175, + "type": "color" + }, + "200": { + "value": "#c6c6c6", + "step": 200, + "type": "color" + }, + "225": { + "value": "#bfbfbf", + "step": 225, + "type": "color" + }, + "250": { + "value": "#b8b8b8", + "step": 250, + "type": "color" + }, + "275": { + "value": "#b1b1b1", + "step": 275, + "type": "color" + }, + "300": { + "value": "#aaaaaa", + "step": 300, + "type": "color" + }, + "325": { + "value": "#a3a3a3", + "step": 325, + "type": "color" + }, + "350": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "375": { + "value": "#959595", + "step": 375, + "type": "color" + }, + "400": { + "value": "#8e8e8e", + "step": 400, + "type": "color" + }, + "425": { + "value": "#878787", + "step": 425, + "type": "color" + }, + "450": { + "value": "#808080", + "step": 450, + "type": "color" + }, + "475": { + "value": "#787878", + "step": 475, + "type": "color" + }, + "500": { + "value": "#717171", + "step": 500, + "type": "color" + }, + "525": { + "value": "#6a6a6a", + "step": 525, + "type": "color" + }, + "550": { + "value": "#636363", + "step": 550, + "type": "color" + }, + "575": { + "value": "#5c5c5c", + "step": 575, + "type": "color" + }, + "600": { + "value": "#555555", + "step": 600, + "type": "color" + }, + "625": { + "value": "#4e4e4e", + "step": 625, + "type": "color" + }, + "650": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "675": { + "value": "#404040", + "step": 675, + "type": "color" + }, + "700": { + "value": "#393939", + "step": 700, + "type": "color" + }, + "725": { + "value": "#323232", + "step": 725, + "type": "color" + }, + "750": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "775": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "800": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "825": { + "value": "#151515", + "step": 825, + "type": "color" + }, + "850": { + "value": "#0e0e0e", + "step": 850, + "type": "color" + }, + "875": { + "value": "#070707", + "step": 875, + "type": "color" + }, + "900": { + "value": "#000000", + "step": 900, + "type": "color" + } + }, + "rose": { + "0": { + "value": "#feecef", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fcc5cf", + "step": 100, + "type": "color" + }, + "200": { + "value": "#fa9fae", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f8788e", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f5526e", + "step": 400, + "type": "color" + }, + "500": { + "value": "#f0284a", + "step": 500, + "type": "color" + }, + "600": { + "value": "#cd1434", + "step": 600, + "type": "color" + }, + "700": { + "value": "#97142a", + "step": 700, + "type": "color" + }, + "800": { + "value": "#64101e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#330a11", + "step": 900, + "type": "color" + } + }, + "red": { + "0": { + "value": "#feecec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f9a0a0", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f57b7b", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "500": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "600": { + "value": "#c91818", + "step": 600, + "type": "color" + }, + "700": { + "value": "#951515", + "step": 700, + "type": "color" + }, + "800": { + "value": "#631111", + "step": 800, + "type": "color" + }, + "900": { + "value": "#330a0a", + "step": 900, + "type": "color" + } + }, + "orange": { + "0": { + "value": "#fef3ec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fcd6bd", + "step": 100, + "type": "color" + }, + "200": { + "value": "#fab98e", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f99d5f", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f9812e", + "step": 400, + "type": "color" + }, + "500": { + "value": "#ee670a", + "step": 500, + "type": "color" + }, + "600": { + "value": "#bb550e", + "step": 600, + "type": "color" + }, + "700": { + "value": "#8b4210", + "step": 700, + "type": "color" + }, + "800": { + "value": "#5d2f0e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#331b0a", + "step": 900, + "type": "color" + } + }, + "amber": { + "0": { + "value": "#fef7ec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fce2ba", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f9ce89", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f6a724", + "step": 400, + "type": "color" + }, + "500": { + "value": "#de900c", + "step": 500, + "type": "color" + }, + "600": { + "value": "#b0740f", + "step": 600, + "type": "color" + }, + "700": { + "value": "#845910", + "step": 700, + "type": "color" + }, + "800": { + "value": "#5a3e0e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#33240a", + "step": 900, + "type": "color" + } + }, + "yellow": { + "0": { + "value": "#fef9ec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f9da82", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f8cc4d", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "500": { + "value": "#d3a20b", + "step": 500, + "type": "color" + }, + "600": { + "value": "#a8820e", + "step": 600, + "type": "color" + }, + "700": { + "value": "#7e630f", + "step": 700, + "type": "color" + }, + "800": { + "value": "#58460e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#33290a", + "step": 900, + "type": "color" + } + }, + "lime": { + "0": { + "value": "#f7feec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#dffab5", + "step": 100, + "type": "color" + }, + "200": { + "value": "#c7f57f", + "step": 200, + "type": "color" + }, + "300": { + "value": "#aeef4b", + "step": 300, + "type": "color" + }, + "400": { + "value": "#96e818", + "step": 400, + "type": "color" + }, + "500": { + "value": "#79ba16", + "step": 500, + "type": "color" + }, + "600": { + "value": "#639714", + "step": 600, + "type": "color" + }, + "700": { + "value": "#4e7412", + "step": 700, + "type": "color" + }, + "800": { + "value": "#38530f", + "step": 800, + "type": "color" + }, + "900": { + "value": "#23330a", + "step": 900, + "type": "color" + } + }, + "green": { + "0": { + "value": "#ecfef2", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "200": { + "value": "#84f2ab", + "step": 200, + "type": "color" + }, + "300": { + "value": "#54e989", + "step": 300, + "type": "color" + }, + "400": { + "value": "#27dd69", + "step": 400, + "type": "color" + }, + "500": { + "value": "#20b456", + "step": 500, + "type": "color" + }, + "600": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "700": { + "value": "#157338", + "step": 700, + "type": "color" + }, + "800": { + "value": "#105328", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a3319", + "step": 900, + "type": "color" + } + }, + "emerald": { + "0": { + "value": "#ecfef8", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b0fae1", + "step": 100, + "type": "color" + }, + "200": { + "value": "#74f6cb", + "step": 200, + "type": "color" + }, + "300": { + "value": "#39f0b3", + "step": 300, + "type": "color" + }, + "400": { + "value": "#12d796", + "step": 400, + "type": "color" + }, + "500": { + "value": "#10a977", + "step": 500, + "type": "color" + }, + "600": { + "value": "#118a62", + "step": 600, + "type": "color" + }, + "700": { + "value": "#106c4e", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0d4f3a", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a3326", + "step": 900, + "type": "color" + } + }, + "teal": { + "0": { + "value": "#ecfefc", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b1faf2", + "step": 100, + "type": "color" + }, + "200": { + "value": "#76f5e7", + "step": 200, + "type": "color" + }, + "300": { + "value": "#3eeeda", + "step": 300, + "type": "color" + }, + "400": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "500": { + "value": "#14a898", + "step": 500, + "type": "color" + }, + "600": { + "value": "#138a7d", + "step": 600, + "type": "color" + }, + "700": { + "value": "#116c62", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0e4f48", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a332f", + "step": 900, + "type": "color" + } + }, + "cyan": { + "0": { + "value": "#ecfcfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b2f3fb", + "step": 100, + "type": "color" + }, + "200": { + "value": "#78eaf9", + "step": 200, + "type": "color" + }, + "300": { + "value": "#3de2f8", + "step": 300, + "type": "color" + }, + "400": { + "value": "#07d5f1", + "step": 400, + "type": "color" + }, + "500": { + "value": "#09aac0", + "step": 500, + "type": "color" + }, + "600": { + "value": "#0c8a9a", + "step": 600, + "type": "color" + }, + "700": { + "value": "#0e6a75", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0d4c53", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a2f33", + "step": 900, + "type": "color" + } + }, + "sky": { + "0": { + "value": "#ecf8fe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b9e5fb", + "step": 100, + "type": "color" + }, + "200": { + "value": "#86d3f8", + "step": 200, + "type": "color" + }, + "300": { + "value": "#53c1f5", + "step": 300, + "type": "color" + }, + "400": { + "value": "#20b0f2", + "step": 400, + "type": "color" + }, + "500": { + "value": "#1096d3", + "step": 500, + "type": "color" + }, + "600": { + "value": "#1179a8", + "step": 600, + "type": "color" + }, + "700": { + "value": "#115c7f", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0e4158", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a2633", + "step": 900, + "type": "color" + } + }, + "blue": { + "0": { + "value": "#ecf3fe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#9ec1fa", + "step": 200, + "type": "color" + }, + "300": { + "value": "#76a8f8", + "step": 300, + "type": "color" + }, + "400": { + "value": "#4f8ff7", + "step": 400, + "type": "color" + }, + "500": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "600": { + "value": "#135acd", + "step": 600, + "type": "color" + }, + "700": { + "value": "#134697", + "step": 700, + "type": "color" + }, + "800": { + "value": "#103063", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a1a33", + "step": 900, + "type": "color" + } + }, + "indigo": { + "0": { + "value": "#ececfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#cdcdfc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#aeaff9", + "step": 200, + "type": "color" + }, + "300": { + "value": "#9091f6", + "step": 300, + "type": "color" + }, + "400": { + "value": "#7274f3", + "step": 400, + "type": "color" + }, + "500": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "600": { + "value": "#1b1edc", + "step": 600, + "type": "color" + }, + "700": { + "value": "#1819a1", + "step": 700, + "type": "color" + }, + "800": { + "value": "#121269", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a0a33", + "step": 900, + "type": "color" + } + }, + "violet": { + "0": { + "value": "#f1ecfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#daccfc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#c3acfb", + "step": 200, + "type": "color" + }, + "300": { + "value": "#ac8cf9", + "step": 300, + "type": "color" + }, + "400": { + "value": "#966cf7", + "step": 400, + "type": "color" + }, + "500": { + "value": "#7741f2", + "step": 500, + "type": "color" + }, + "600": { + "value": "#5316e0", + "step": 600, + "type": "color" + }, + "700": { + "value": "#3f15a3", + "step": 700, + "type": "color" + }, + "800": { + "value": "#2b116a", + "step": 800, + "type": "color" + }, + "900": { + "value": "#160a33", + "step": 900, + "type": "color" + } + }, + "purple": { + "0": { + "value": "#f5ecfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#e4cbfc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#d2a9fb", + "step": 200, + "type": "color" + }, + "300": { + "value": "#c188f9", + "step": 300, + "type": "color" + }, + "400": { + "value": "#b066f8", + "step": 400, + "type": "color" + }, + "500": { + "value": "#993bf3", + "step": 500, + "type": "color" + }, + "600": { + "value": "#7b14dd", + "step": 600, + "type": "color" + }, + "700": { + "value": "#5c14a1", + "step": 700, + "type": "color" + }, + "800": { + "value": "#3e1169", + "step": 800, + "type": "color" + }, + "900": { + "value": "#1f0a33", + "step": 900, + "type": "color" + } + }, + "fuschia": { + "0": { + "value": "#fdecfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#f8c5fb", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f19ff6", + "step": 200, + "type": "color" + }, + "300": { + "value": "#e87af0", + "step": 300, + "type": "color" + }, + "400": { + "value": "#de57e8", + "step": 400, + "type": "color" + }, + "500": { + "value": "#d430e0", + "step": 500, + "type": "color" + }, + "600": { + "value": "#b31fbc", + "step": 600, + "type": "color" + }, + "700": { + "value": "#87198e", + "step": 700, + "type": "color" + }, + "800": { + "value": "#5c1260", + "step": 800, + "type": "color" + }, + "900": { + "value": "#310a33", + "step": 900, + "type": "color" + } + }, + "pink": { + "0": { + "value": "#feecf5", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fbc6e1", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f8a1cc", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f47db8", + "step": 300, + "type": "color" + }, + "400": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "500": { + "value": "#e8318c", + "step": 500, + "type": "color" + }, + "600": { + "value": "#c71a71", + "step": 600, + "type": "color" + }, + "700": { + "value": "#941756", + "step": 700, + "type": "color" + }, + "800": { + "value": "#63113b", + "step": 800, + "type": "color" + }, + "900": { + "value": "#330a1f", + "step": 900, + "type": "color" + } + } + }, + "text": { + "family": { + "sans": { + "value": "Zed Sans", + "type": "fontFamily" + }, + "mono": { + "value": "Zed Mono", + "type": "fontFamily" + } + }, + "weight": { + "thin": { + "value": "thin", + "type": "fontWeight" + }, + "extra_light": { + "value": "extra_light", + "type": "fontWeight" + }, + "light": { + "value": "light", + "type": "fontWeight" + }, + "normal": { + "value": "normal", + "type": "fontWeight" + }, + "medium": { + "value": "medium", + "type": "fontWeight" + }, + "semibold": { + "value": "semibold", + "type": "fontWeight" + }, + "bold": { + "value": "bold", + "type": "fontWeight" + }, + "extra_bold": { + "value": "extra_bold", + "type": "fontWeight" + }, + "black": { + "value": "black", + "type": "fontWeight" + } + } + }, + "size": { + "3xs": { + "value": 8, + "type": "fontSize" + }, + "2xs": { + "value": 10, + "type": "fontSize" + }, + "xs": { + "value": 12, + "type": "fontSize" + }, + "sm": { + "value": 14, + "type": "fontSize" + }, + "md": { + "value": 16, + "type": "fontSize" + }, + "lg": { + "value": 18, + "type": "fontSize" + }, + "xl": { + "value": 20, + "type": "fontSize" + } + } +} \ No newline at end of file diff --git a/styles/dist/dark.json b/styles/dist/dark.json new file mode 100644 index 0000000000..872e1d1280 --- /dev/null +++ b/styles/dist/dark.json @@ -0,0 +1,681 @@ +{ + "meta": { + "themeName": "dark" + }, + "text": { + "primary": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "secondary": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "muted": { + "value": "#808080", + "step": 450, + "type": "color" + }, + "placeholder": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "feature": { + "value": "#4f8ff7", + "step": 400, + "type": "color" + }, + "ok": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "error": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "warning": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "info": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#c6c6c6", + "step": 200, + "type": "color" + }, + "secondary": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "muted": { + "value": "#555555", + "step": 600, + "type": "color" + }, + "placeholder": { + "value": "#393939", + "step": 700, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "feature": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "ok": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "error": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "warning": { + "value": "#f6a724", + "step": 400, + "type": "color" + }, + "info": { + "value": "#135acd", + "step": 600, + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "hovered": { + "value": "#323232", + "step": 725, + "type": "color" + }, + "active": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "focused": { + "value": "#404040", + "step": 675, + "type": "color" + } + }, + "300": { + "base": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "hovered": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "active": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "focused": { + "value": "#232323", + "step": 775, + "type": "color" + } + }, + "500": { + "base": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "hovered": { + "value": "#ffffff14", + "step": 0, + "type": "color" + }, + "active": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "focused": { + "value": "#151515", + "step": 825, + "type": "color" + } + }, + "on300": { + "base": { + "value": "#0e0e0e80", + "step": 850, + "type": "color" + }, + "hovered": { + "value": "#070707", + "step": 875, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "focused": { + "value": "#070707", + "step": 875, + "type": "color" + } + }, + "on500": { + "base": { + "value": "#0e0e0e", + "step": 850, + "type": "color" + }, + "hovered": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "active": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "focused": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + } + }, + "ok": { + "base": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "hovered": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "active": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "focused": { + "value": "#1b9447", + "step": 600, + "type": "color" + } + }, + "error": { + "base": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "hovered": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "active": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "focused": { + "value": "#f15656", + "step": 400, + "type": "color" + } + }, + "warning": { + "base": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "hovered": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "active": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "focused": { + "value": "#f7bb57", + "step": 300, + "type": "color" + } + }, + "info": { + "base": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "hovered": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "active": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "focused": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#070707", + "step": 875, + "type": "color" + }, + "secondary": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "muted": { + "value": "#404040", + "step": 675, + "type": "color" + }, + "focused": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "ok": { + "value": "#20b456", + "step": 500, + "type": "color" + }, + "error": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "warning": { + "value": "#de900c", + "step": 500, + "type": "color" + }, + "info": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "editor": { + "background": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "indent_guide": { + "value": "#404040", + "step": 675, + "type": "color" + }, + "indent_guide_active": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "line": { + "active": { + "value": "#ffffff12", + "step": 0, + "type": "color" + }, + "highlighted": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "inserted": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "deleted": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "modified": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "occurrence": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "activeOccurrence": { + "value": "#ffffff29", + "step": 0, + "type": "color" + }, + "matchingBracket": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "match": { + "value": "#3f15a380", + "step": 700, + "type": "color" + }, + "activeMatch": { + "value": "#5316e0b3", + "step": 600, + "type": "color" + }, + "related": { + "value": "#151515", + "step": 825, + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#d5d5d5", + "type": "color" + }, + "comment": { + "value": "#aaaaaa", + "type": "color" + }, + "keyword": { + "value": "#4f8ff7", + "type": "color" + }, + "function": { + "value": "#f9da82", + "type": "color" + }, + "type": { + "value": "#3eeeda", + "type": "color" + }, + "variant": { + "value": "#53c1f5", + "type": "color" + }, + "property": { + "value": "#4f8ff7", + "type": "color" + }, + "enum": { + "value": "#ee670a", + "type": "color" + }, + "operator": { + "value": "#ee670a", + "type": "color" + }, + "string": { + "value": "#f99d5f", + "type": "color" + }, + "number": { + "value": "#aeef4b", + "type": "color" + }, + "boolean": { + "value": "#aeef4b", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#2472f2cc", + "step": 500, + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#79ba16", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#79ba16", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#79ba163d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#79ba16cc", + "step": 500, + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#d430e0", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#d430e0", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#d430e03d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#d430e0cc", + "step": 500, + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#ee670a", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#ee670a", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#ee670a3d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#ee670acc", + "step": 500, + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#993bf3", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#993bf3", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#993bf33d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#993bf3cc", + "step": 500, + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#16d6c13d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#16d6c1cc", + "step": 400, + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#ef59a33d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#ef59a3cc", + "step": 400, + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#f7bf173d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#f7bf17cc", + "step": 400, + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.32, + "type": "number" + } +} \ No newline at end of file diff --git a/styles/dist/light.json b/styles/dist/light.json new file mode 100644 index 0000000000..cb94077e95 --- /dev/null +++ b/styles/dist/light.json @@ -0,0 +1,681 @@ +{ + "meta": { + "themeName": "light" + }, + "text": { + "primary": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "secondary": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "muted": { + "value": "#636363", + "step": 550, + "type": "color" + }, + "placeholder": { + "value": "#808080", + "step": 450, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "feature": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "ok": { + "value": "#20b456", + "step": 500, + "type": "color" + }, + "error": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "warning": { + "value": "#d3a20b", + "step": 500, + "type": "color" + }, + "info": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#393939", + "step": 700, + "type": "color" + }, + "secondary": { + "value": "#717171", + "step": 500, + "type": "color" + }, + "muted": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "placeholder": { + "value": "#aaaaaa", + "step": 300, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "feature": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "ok": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "error": { + "value": "#c91818", + "step": 600, + "type": "color" + }, + "warning": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "info": { + "value": "#135acd", + "step": 600, + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#eaeaea", + "step": 75, + "type": "color" + }, + "hovered": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "active": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "focused": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + } + }, + "300": { + "base": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + }, + "hovered": { + "value": "#eaeaea", + "step": 75, + "type": "color" + }, + "active": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#eaeaea", + "step": 75, + "type": "color" + } + }, + "500": { + "base": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "hovered": { + "value": "#00000008", + "step": 900, + "type": "color" + }, + "active": { + "value": "#0000000f", + "step": 900, + "type": "color" + }, + "focused": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + } + }, + "on300": { + "base": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "hovered": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "active": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "focused": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + } + }, + "on500": { + "base": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "hovered": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "focused": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + } + }, + "ok": { + "base": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "active": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + } + }, + "error": { + "base": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "active": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + } + }, + "warning": { + "base": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "active": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#fce9b7", + "step": 100, + "type": "color" + } + }, + "info": { + "base": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "active": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#c5dafc", + "step": 100, + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "secondary": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "muted": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "active": { + "value": "#b8b8b8", + "step": 250, + "type": "color" + }, + "ok": { + "value": "#84f2ab", + "step": 200, + "type": "color" + }, + "error": { + "value": "#f9a0a0", + "step": 200, + "type": "color" + }, + "warning": { + "value": "#f9da82", + "step": 200, + "type": "color" + }, + "info": { + "value": "#9ec1fa", + "step": 200, + "type": "color" + } + }, + "editor": { + "background": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "indent_guide": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "indent_guide_active": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "line": { + "active": { + "value": "#0000000f", + "step": 900, + "type": "color" + }, + "highlighted": { + "value": "#0000001f", + "step": 900, + "type": "color" + }, + "inserted": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "deleted": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "modified": { + "value": "#c5dafc", + "step": 100, + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "occurrence": { + "value": "#0000000f", + "step": 900, + "type": "color" + }, + "activeOccurrence": { + "value": "#00000029", + "step": 900, + "type": "color" + }, + "matchingBracket": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "match": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "activeMatch": { + "value": "#f9da82", + "step": 200, + "type": "color" + }, + "related": { + "value": "#ffffff", + "step": 0, + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#aaaaaa", + "step": 300, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#1c1c1c", + "type": "color" + }, + "comment": { + "value": "#717171", + "type": "color" + }, + "keyword": { + "value": "#1819a1", + "type": "color" + }, + "function": { + "value": "#bb550e", + "type": "color" + }, + "type": { + "value": "#a8820e", + "type": "color" + }, + "variant": { + "value": "#97142a", + "type": "color" + }, + "property": { + "value": "#106c4e", + "type": "color" + }, + "enum": { + "value": "#eb2d2d", + "type": "color" + }, + "operator": { + "value": "#eb2d2d", + "type": "color" + }, + "string": { + "value": "#eb2d2d", + "type": "color" + }, + "number": { + "value": "#484bed", + "type": "color" + }, + "boolean": { + "value": "#eb2d2d", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#2472f2cc", + "step": 500, + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#12d796", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#12d796", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#12d7963d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#12d796cc", + "step": 400, + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#de57e8", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#de57e8", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#de57e83d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#de57e8cc", + "step": 400, + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#f9812e", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#f9812e", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#f9812e3d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#f9812ecc", + "step": 400, + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#b066f8", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#b066f8", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#b066f83d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#b066f8cc", + "step": 400, + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#16d6c13d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#16d6c1cc", + "step": 400, + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#ef59a33d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#ef59a3cc", + "step": 400, + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#f7bf173d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#f7bf17cc", + "step": 400, + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.12, + "type": "number" + } +} \ No newline at end of file diff --git a/styles/dist/tokens.json b/styles/dist/tokens.json new file mode 100644 index 0000000000..2d52abc280 --- /dev/null +++ b/styles/dist/tokens.json @@ -0,0 +1,2519 @@ +{ + "core": { + "color": { + "neutral": { + "0": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "25": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + }, + "50": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "75": { + "value": "#eaeaea", + "step": 75, + "type": "color" + }, + "100": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "125": { + "value": "#dcdcdc", + "step": 125, + "type": "color" + }, + "150": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "175": { + "value": "#cdcdcd", + "step": 175, + "type": "color" + }, + "200": { + "value": "#c6c6c6", + "step": 200, + "type": "color" + }, + "225": { + "value": "#bfbfbf", + "step": 225, + "type": "color" + }, + "250": { + "value": "#b8b8b8", + "step": 250, + "type": "color" + }, + "275": { + "value": "#b1b1b1", + "step": 275, + "type": "color" + }, + "300": { + "value": "#aaaaaa", + "step": 300, + "type": "color" + }, + "325": { + "value": "#a3a3a3", + "step": 325, + "type": "color" + }, + "350": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "375": { + "value": "#959595", + "step": 375, + "type": "color" + }, + "400": { + "value": "#8e8e8e", + "step": 400, + "type": "color" + }, + "425": { + "value": "#878787", + "step": 425, + "type": "color" + }, + "450": { + "value": "#808080", + "step": 450, + "type": "color" + }, + "475": { + "value": "#787878", + "step": 475, + "type": "color" + }, + "500": { + "value": "#717171", + "step": 500, + "type": "color" + }, + "525": { + "value": "#6a6a6a", + "step": 525, + "type": "color" + }, + "550": { + "value": "#636363", + "step": 550, + "type": "color" + }, + "575": { + "value": "#5c5c5c", + "step": 575, + "type": "color" + }, + "600": { + "value": "#555555", + "step": 600, + "type": "color" + }, + "625": { + "value": "#4e4e4e", + "step": 625, + "type": "color" + }, + "650": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "675": { + "value": "#404040", + "step": 675, + "type": "color" + }, + "700": { + "value": "#393939", + "step": 700, + "type": "color" + }, + "725": { + "value": "#323232", + "step": 725, + "type": "color" + }, + "750": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "775": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "800": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "825": { + "value": "#151515", + "step": 825, + "type": "color" + }, + "850": { + "value": "#0e0e0e", + "step": 850, + "type": "color" + }, + "875": { + "value": "#070707", + "step": 875, + "type": "color" + }, + "900": { + "value": "#000000", + "step": 900, + "type": "color" + } + }, + "rose": { + "0": { + "value": "#feecef", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fcc5cf", + "step": 100, + "type": "color" + }, + "200": { + "value": "#fa9fae", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f8788e", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f5526e", + "step": 400, + "type": "color" + }, + "500": { + "value": "#f0284a", + "step": 500, + "type": "color" + }, + "600": { + "value": "#cd1434", + "step": 600, + "type": "color" + }, + "700": { + "value": "#97142a", + "step": 700, + "type": "color" + }, + "800": { + "value": "#64101e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#330a11", + "step": 900, + "type": "color" + } + }, + "red": { + "0": { + "value": "#feecec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f9a0a0", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f57b7b", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "500": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "600": { + "value": "#c91818", + "step": 600, + "type": "color" + }, + "700": { + "value": "#951515", + "step": 700, + "type": "color" + }, + "800": { + "value": "#631111", + "step": 800, + "type": "color" + }, + "900": { + "value": "#330a0a", + "step": 900, + "type": "color" + } + }, + "orange": { + "0": { + "value": "#fef3ec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fcd6bd", + "step": 100, + "type": "color" + }, + "200": { + "value": "#fab98e", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f99d5f", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f9812e", + "step": 400, + "type": "color" + }, + "500": { + "value": "#ee670a", + "step": 500, + "type": "color" + }, + "600": { + "value": "#bb550e", + "step": 600, + "type": "color" + }, + "700": { + "value": "#8b4210", + "step": 700, + "type": "color" + }, + "800": { + "value": "#5d2f0e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#331b0a", + "step": 900, + "type": "color" + } + }, + "amber": { + "0": { + "value": "#fef7ec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fce2ba", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f9ce89", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f6a724", + "step": 400, + "type": "color" + }, + "500": { + "value": "#de900c", + "step": 500, + "type": "color" + }, + "600": { + "value": "#b0740f", + "step": 600, + "type": "color" + }, + "700": { + "value": "#845910", + "step": 700, + "type": "color" + }, + "800": { + "value": "#5a3e0e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#33240a", + "step": 900, + "type": "color" + } + }, + "yellow": { + "0": { + "value": "#fef9ec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f9da82", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f8cc4d", + "step": 300, + "type": "color" + }, + "400": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "500": { + "value": "#d3a20b", + "step": 500, + "type": "color" + }, + "600": { + "value": "#a8820e", + "step": 600, + "type": "color" + }, + "700": { + "value": "#7e630f", + "step": 700, + "type": "color" + }, + "800": { + "value": "#58460e", + "step": 800, + "type": "color" + }, + "900": { + "value": "#33290a", + "step": 900, + "type": "color" + } + }, + "lime": { + "0": { + "value": "#f7feec", + "step": 0, + "type": "color" + }, + "100": { + "value": "#dffab5", + "step": 100, + "type": "color" + }, + "200": { + "value": "#c7f57f", + "step": 200, + "type": "color" + }, + "300": { + "value": "#aeef4b", + "step": 300, + "type": "color" + }, + "400": { + "value": "#96e818", + "step": 400, + "type": "color" + }, + "500": { + "value": "#79ba16", + "step": 500, + "type": "color" + }, + "600": { + "value": "#639714", + "step": 600, + "type": "color" + }, + "700": { + "value": "#4e7412", + "step": 700, + "type": "color" + }, + "800": { + "value": "#38530f", + "step": 800, + "type": "color" + }, + "900": { + "value": "#23330a", + "step": 900, + "type": "color" + } + }, + "green": { + "0": { + "value": "#ecfef2", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "200": { + "value": "#84f2ab", + "step": 200, + "type": "color" + }, + "300": { + "value": "#54e989", + "step": 300, + "type": "color" + }, + "400": { + "value": "#27dd69", + "step": 400, + "type": "color" + }, + "500": { + "value": "#20b456", + "step": 500, + "type": "color" + }, + "600": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "700": { + "value": "#157338", + "step": 700, + "type": "color" + }, + "800": { + "value": "#105328", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a3319", + "step": 900, + "type": "color" + } + }, + "emerald": { + "0": { + "value": "#ecfef8", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b0fae1", + "step": 100, + "type": "color" + }, + "200": { + "value": "#74f6cb", + "step": 200, + "type": "color" + }, + "300": { + "value": "#39f0b3", + "step": 300, + "type": "color" + }, + "400": { + "value": "#12d796", + "step": 400, + "type": "color" + }, + "500": { + "value": "#10a977", + "step": 500, + "type": "color" + }, + "600": { + "value": "#118a62", + "step": 600, + "type": "color" + }, + "700": { + "value": "#106c4e", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0d4f3a", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a3326", + "step": 900, + "type": "color" + } + }, + "teal": { + "0": { + "value": "#ecfefc", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b1faf2", + "step": 100, + "type": "color" + }, + "200": { + "value": "#76f5e7", + "step": 200, + "type": "color" + }, + "300": { + "value": "#3eeeda", + "step": 300, + "type": "color" + }, + "400": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "500": { + "value": "#14a898", + "step": 500, + "type": "color" + }, + "600": { + "value": "#138a7d", + "step": 600, + "type": "color" + }, + "700": { + "value": "#116c62", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0e4f48", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a332f", + "step": 900, + "type": "color" + } + }, + "cyan": { + "0": { + "value": "#ecfcfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b2f3fb", + "step": 100, + "type": "color" + }, + "200": { + "value": "#78eaf9", + "step": 200, + "type": "color" + }, + "300": { + "value": "#3de2f8", + "step": 300, + "type": "color" + }, + "400": { + "value": "#07d5f1", + "step": 400, + "type": "color" + }, + "500": { + "value": "#09aac0", + "step": 500, + "type": "color" + }, + "600": { + "value": "#0c8a9a", + "step": 600, + "type": "color" + }, + "700": { + "value": "#0e6a75", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0d4c53", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a2f33", + "step": 900, + "type": "color" + } + }, + "sky": { + "0": { + "value": "#ecf8fe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#b9e5fb", + "step": 100, + "type": "color" + }, + "200": { + "value": "#86d3f8", + "step": 200, + "type": "color" + }, + "300": { + "value": "#53c1f5", + "step": 300, + "type": "color" + }, + "400": { + "value": "#20b0f2", + "step": 400, + "type": "color" + }, + "500": { + "value": "#1096d3", + "step": 500, + "type": "color" + }, + "600": { + "value": "#1179a8", + "step": 600, + "type": "color" + }, + "700": { + "value": "#115c7f", + "step": 700, + "type": "color" + }, + "800": { + "value": "#0e4158", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a2633", + "step": 900, + "type": "color" + } + }, + "blue": { + "0": { + "value": "#ecf3fe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#9ec1fa", + "step": 200, + "type": "color" + }, + "300": { + "value": "#76a8f8", + "step": 300, + "type": "color" + }, + "400": { + "value": "#4f8ff7", + "step": 400, + "type": "color" + }, + "500": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "600": { + "value": "#135acd", + "step": 600, + "type": "color" + }, + "700": { + "value": "#134697", + "step": 700, + "type": "color" + }, + "800": { + "value": "#103063", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a1a33", + "step": 900, + "type": "color" + } + }, + "indigo": { + "0": { + "value": "#ececfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#cdcdfc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#aeaff9", + "step": 200, + "type": "color" + }, + "300": { + "value": "#9091f6", + "step": 300, + "type": "color" + }, + "400": { + "value": "#7274f3", + "step": 400, + "type": "color" + }, + "500": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "600": { + "value": "#1b1edc", + "step": 600, + "type": "color" + }, + "700": { + "value": "#1819a1", + "step": 700, + "type": "color" + }, + "800": { + "value": "#121269", + "step": 800, + "type": "color" + }, + "900": { + "value": "#0a0a33", + "step": 900, + "type": "color" + } + }, + "violet": { + "0": { + "value": "#f1ecfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#daccfc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#c3acfb", + "step": 200, + "type": "color" + }, + "300": { + "value": "#ac8cf9", + "step": 300, + "type": "color" + }, + "400": { + "value": "#966cf7", + "step": 400, + "type": "color" + }, + "500": { + "value": "#7741f2", + "step": 500, + "type": "color" + }, + "600": { + "value": "#5316e0", + "step": 600, + "type": "color" + }, + "700": { + "value": "#3f15a3", + "step": 700, + "type": "color" + }, + "800": { + "value": "#2b116a", + "step": 800, + "type": "color" + }, + "900": { + "value": "#160a33", + "step": 900, + "type": "color" + } + }, + "purple": { + "0": { + "value": "#f5ecfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#e4cbfc", + "step": 100, + "type": "color" + }, + "200": { + "value": "#d2a9fb", + "step": 200, + "type": "color" + }, + "300": { + "value": "#c188f9", + "step": 300, + "type": "color" + }, + "400": { + "value": "#b066f8", + "step": 400, + "type": "color" + }, + "500": { + "value": "#993bf3", + "step": 500, + "type": "color" + }, + "600": { + "value": "#7b14dd", + "step": 600, + "type": "color" + }, + "700": { + "value": "#5c14a1", + "step": 700, + "type": "color" + }, + "800": { + "value": "#3e1169", + "step": 800, + "type": "color" + }, + "900": { + "value": "#1f0a33", + "step": 900, + "type": "color" + } + }, + "fuschia": { + "0": { + "value": "#fdecfe", + "step": 0, + "type": "color" + }, + "100": { + "value": "#f8c5fb", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f19ff6", + "step": 200, + "type": "color" + }, + "300": { + "value": "#e87af0", + "step": 300, + "type": "color" + }, + "400": { + "value": "#de57e8", + "step": 400, + "type": "color" + }, + "500": { + "value": "#d430e0", + "step": 500, + "type": "color" + }, + "600": { + "value": "#b31fbc", + "step": 600, + "type": "color" + }, + "700": { + "value": "#87198e", + "step": 700, + "type": "color" + }, + "800": { + "value": "#5c1260", + "step": 800, + "type": "color" + }, + "900": { + "value": "#310a33", + "step": 900, + "type": "color" + } + }, + "pink": { + "0": { + "value": "#feecf5", + "step": 0, + "type": "color" + }, + "100": { + "value": "#fbc6e1", + "step": 100, + "type": "color" + }, + "200": { + "value": "#f8a1cc", + "step": 200, + "type": "color" + }, + "300": { + "value": "#f47db8", + "step": 300, + "type": "color" + }, + "400": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "500": { + "value": "#e8318c", + "step": 500, + "type": "color" + }, + "600": { + "value": "#c71a71", + "step": 600, + "type": "color" + }, + "700": { + "value": "#941756", + "step": 700, + "type": "color" + }, + "800": { + "value": "#63113b", + "step": 800, + "type": "color" + }, + "900": { + "value": "#330a1f", + "step": 900, + "type": "color" + } + } + }, + "text": { + "family": { + "sans": { + "value": "Zed Sans", + "type": "fontFamily" + }, + "mono": { + "value": "Zed Mono", + "type": "fontFamily" + } + }, + "weight": { + "thin": { + "value": "thin", + "type": "fontWeight" + }, + "extra_light": { + "value": "extra_light", + "type": "fontWeight" + }, + "light": { + "value": "light", + "type": "fontWeight" + }, + "normal": { + "value": "normal", + "type": "fontWeight" + }, + "medium": { + "value": "medium", + "type": "fontWeight" + }, + "semibold": { + "value": "semibold", + "type": "fontWeight" + }, + "bold": { + "value": "bold", + "type": "fontWeight" + }, + "extra_bold": { + "value": "extra_bold", + "type": "fontWeight" + }, + "black": { + "value": "black", + "type": "fontWeight" + } + } + }, + "size": { + "3xs": { + "value": 8, + "type": "fontSize" + }, + "2xs": { + "value": 10, + "type": "fontSize" + }, + "xs": { + "value": 12, + "type": "fontSize" + }, + "sm": { + "value": 14, + "type": "fontSize" + }, + "md": { + "value": 16, + "type": "fontSize" + }, + "lg": { + "value": 18, + "type": "fontSize" + }, + "xl": { + "value": 20, + "type": "fontSize" + } + } + }, + "dark": { + "meta": { + "themeName": "dark" + }, + "text": { + "primary": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "secondary": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "muted": { + "value": "#808080", + "step": 450, + "type": "color" + }, + "placeholder": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "feature": { + "value": "#4f8ff7", + "step": 400, + "type": "color" + }, + "ok": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "error": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "warning": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "info": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#c6c6c6", + "step": 200, + "type": "color" + }, + "secondary": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "muted": { + "value": "#555555", + "step": 600, + "type": "color" + }, + "placeholder": { + "value": "#393939", + "step": 700, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "feature": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "ok": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "error": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "warning": { + "value": "#f6a724", + "step": 400, + "type": "color" + }, + "info": { + "value": "#135acd", + "step": 600, + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "hovered": { + "value": "#323232", + "step": 725, + "type": "color" + }, + "active": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "focused": { + "value": "#404040", + "step": 675, + "type": "color" + } + }, + "300": { + "base": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "hovered": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "active": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "focused": { + "value": "#232323", + "step": 775, + "type": "color" + } + }, + "500": { + "base": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "hovered": { + "value": "#ffffff14", + "step": 0, + "type": "color" + }, + "active": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "focused": { + "value": "#151515", + "step": 825, + "type": "color" + } + }, + "on300": { + "base": { + "value": "#0e0e0e80", + "step": 850, + "type": "color" + }, + "hovered": { + "value": "#070707", + "step": 875, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "focused": { + "value": "#070707", + "step": 875, + "type": "color" + } + }, + "on500": { + "base": { + "value": "#0e0e0e", + "step": 850, + "type": "color" + }, + "hovered": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + }, + "active": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "focused": { + "value": "#1c1c1c", + "step": 800, + "type": "color" + } + }, + "ok": { + "base": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "hovered": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "active": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "focused": { + "value": "#1b9447", + "step": 600, + "type": "color" + } + }, + "error": { + "base": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "hovered": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "active": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "focused": { + "value": "#f15656", + "step": 400, + "type": "color" + } + }, + "warning": { + "base": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "hovered": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "active": { + "value": "#f7bb57", + "step": 300, + "type": "color" + }, + "focused": { + "value": "#f7bb57", + "step": 300, + "type": "color" + } + }, + "info": { + "base": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "hovered": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "active": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "focused": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#070707", + "step": 875, + "type": "color" + }, + "secondary": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "muted": { + "value": "#404040", + "step": 675, + "type": "color" + }, + "focused": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "ok": { + "value": "#20b456", + "step": 500, + "type": "color" + }, + "error": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "warning": { + "value": "#de900c", + "step": 500, + "type": "color" + }, + "info": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "editor": { + "background": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "indent_guide": { + "value": "#404040", + "step": 675, + "type": "color" + }, + "indent_guide_active": { + "value": "#232323", + "step": 775, + "type": "color" + }, + "line": { + "active": { + "value": "#ffffff12", + "step": 0, + "type": "color" + }, + "highlighted": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "inserted": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "deleted": { + "value": "#f15656", + "step": 400, + "type": "color" + }, + "modified": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "occurrence": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "activeOccurrence": { + "value": "#ffffff29", + "step": 0, + "type": "color" + }, + "matchingBracket": { + "value": "#ffffff1f", + "step": 0, + "type": "color" + }, + "match": { + "value": "#3f15a380", + "step": 700, + "type": "color" + }, + "activeMatch": { + "value": "#5316e0b3", + "step": 600, + "type": "color" + }, + "related": { + "value": "#151515", + "step": 825, + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#d5d5d5", + "type": "color" + }, + "comment": { + "value": "#aaaaaa", + "type": "color" + }, + "keyword": { + "value": "#4f8ff7", + "type": "color" + }, + "function": { + "value": "#f9da82", + "type": "color" + }, + "type": { + "value": "#3eeeda", + "type": "color" + }, + "variant": { + "value": "#53c1f5", + "type": "color" + }, + "property": { + "value": "#4f8ff7", + "type": "color" + }, + "enum": { + "value": "#ee670a", + "type": "color" + }, + "operator": { + "value": "#ee670a", + "type": "color" + }, + "string": { + "value": "#f99d5f", + "type": "color" + }, + "number": { + "value": "#aeef4b", + "type": "color" + }, + "boolean": { + "value": "#aeef4b", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#2472f2cc", + "step": 500, + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#79ba16", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#79ba16", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#79ba163d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#79ba16cc", + "step": 500, + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#d430e0", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#d430e0", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#d430e03d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#d430e0cc", + "step": 500, + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#ee670a", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#ee670a", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#ee670a3d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#ee670acc", + "step": 500, + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#993bf3", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#993bf3", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#993bf33d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#993bf3cc", + "step": 500, + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#16d6c13d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#16d6c1cc", + "step": 400, + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#ef59a33d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#ef59a3cc", + "step": 400, + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#f7bf173d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#f7bf17cc", + "step": 400, + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.32, + "type": "number" + } + }, + "light": { + "meta": { + "themeName": "light" + }, + "text": { + "primary": { + "value": "#2b2b2b", + "step": 750, + "type": "color" + }, + "secondary": { + "value": "#474747", + "step": 650, + "type": "color" + }, + "muted": { + "value": "#636363", + "step": 550, + "type": "color" + }, + "placeholder": { + "value": "#808080", + "step": 450, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "feature": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "ok": { + "value": "#20b456", + "step": 500, + "type": "color" + }, + "error": { + "value": "#eb2d2d", + "step": 500, + "type": "color" + }, + "warning": { + "value": "#d3a20b", + "step": 500, + "type": "color" + }, + "info": { + "value": "#2472f2", + "step": 500, + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#393939", + "step": 700, + "type": "color" + }, + "secondary": { + "value": "#717171", + "step": 500, + "type": "color" + }, + "muted": { + "value": "#9c9c9c", + "step": 350, + "type": "color" + }, + "placeholder": { + "value": "#aaaaaa", + "step": 300, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + }, + "feature": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "ok": { + "value": "#1b9447", + "step": 600, + "type": "color" + }, + "error": { + "value": "#c91818", + "step": 600, + "type": "color" + }, + "warning": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "info": { + "value": "#135acd", + "step": 600, + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#eaeaea", + "step": 75, + "type": "color" + }, + "hovered": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "active": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "focused": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + } + }, + "300": { + "base": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + }, + "hovered": { + "value": "#eaeaea", + "step": 75, + "type": "color" + }, + "active": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#eaeaea", + "step": 75, + "type": "color" + } + }, + "500": { + "base": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "hovered": { + "value": "#00000008", + "step": 900, + "type": "color" + }, + "active": { + "value": "#0000000f", + "step": 900, + "type": "color" + }, + "focused": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + } + }, + "on300": { + "base": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "hovered": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "active": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "focused": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + } + }, + "on500": { + "base": { + "value": "#f1f1f1", + "step": 50, + "type": "color" + }, + "hovered": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + }, + "active": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "focused": { + "value": "#f8f8f8", + "step": 25, + "type": "color" + } + }, + "ok": { + "base": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "active": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + } + }, + "error": { + "base": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "active": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + } + }, + "warning": { + "base": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "active": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#fce9b7", + "step": 100, + "type": "color" + } + }, + "info": { + "base": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "hovered": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "active": { + "value": "#c5dafc", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#c5dafc", + "step": 100, + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "secondary": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "muted": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "focused": { + "value": "#484bed", + "step": 500, + "type": "color" + }, + "active": { + "value": "#b8b8b8", + "step": 250, + "type": "color" + }, + "ok": { + "value": "#84f2ab", + "step": 200, + "type": "color" + }, + "error": { + "value": "#f9a0a0", + "step": 200, + "type": "color" + }, + "warning": { + "value": "#f9da82", + "step": 200, + "type": "color" + }, + "info": { + "value": "#9ec1fa", + "step": 200, + "type": "color" + } + }, + "editor": { + "background": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "indent_guide": { + "value": "#e3e3e3", + "step": 100, + "type": "color" + }, + "indent_guide_active": { + "value": "#d5d5d5", + "step": 150, + "type": "color" + }, + "line": { + "active": { + "value": "#0000000f", + "step": 900, + "type": "color" + }, + "highlighted": { + "value": "#0000001f", + "step": 900, + "type": "color" + }, + "inserted": { + "value": "#b7f9ce", + "step": 100, + "type": "color" + }, + "deleted": { + "value": "#fcc6c6", + "step": 100, + "type": "color" + }, + "modified": { + "value": "#c5dafc", + "step": 100, + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "occurrence": { + "value": "#0000000f", + "step": 900, + "type": "color" + }, + "activeOccurrence": { + "value": "#00000029", + "step": 900, + "type": "color" + }, + "matchingBracket": { + "value": "#ffffff", + "step": 0, + "type": "color" + }, + "match": { + "value": "#fce9b7", + "step": 100, + "type": "color" + }, + "activeMatch": { + "value": "#f9da82", + "step": 200, + "type": "color" + }, + "related": { + "value": "#ffffff", + "step": 0, + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#aaaaaa", + "step": 300, + "type": "color" + }, + "active": { + "value": "#000000", + "step": 900, + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#1c1c1c", + "type": "color" + }, + "comment": { + "value": "#717171", + "type": "color" + }, + "keyword": { + "value": "#1819a1", + "type": "color" + }, + "function": { + "value": "#bb550e", + "type": "color" + }, + "type": { + "value": "#a8820e", + "type": "color" + }, + "variant": { + "value": "#97142a", + "type": "color" + }, + "property": { + "value": "#106c4e", + "type": "color" + }, + "enum": { + "value": "#eb2d2d", + "type": "color" + }, + "operator": { + "value": "#eb2d2d", + "type": "color" + }, + "string": { + "value": "#eb2d2d", + "type": "color" + }, + "number": { + "value": "#484bed", + "type": "color" + }, + "boolean": { + "value": "#eb2d2d", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "cursorColor": { + "value": "#2472f2", + "step": 500, + "type": "color" + }, + "selectionColor": { + "value": "#2472f23d", + "step": 500, + "type": "color" + }, + "borderColor": { + "value": "#2472f2cc", + "step": 500, + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#12d796", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#12d796", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#12d7963d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#12d796cc", + "step": 400, + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#de57e8", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#de57e8", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#de57e83d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#de57e8cc", + "step": 400, + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#f9812e", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#f9812e", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#f9812e3d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#f9812ecc", + "step": 400, + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#b066f8", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#b066f8", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#b066f83d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#b066f8cc", + "step": 400, + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#16d6c1", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#16d6c13d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#16d6c1cc", + "step": 400, + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#ef59a3", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#ef59a33d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#ef59a3cc", + "step": 400, + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "cursorColor": { + "value": "#f7bf17", + "step": 400, + "type": "color" + }, + "selectionColor": { + "value": "#f7bf173d", + "step": 400, + "type": "color" + }, + "borderColor": { + "value": "#f7bf17cc", + "step": 400, + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.12, + "type": "number" + } + } +} \ No newline at end of file diff --git a/styles/nodemon.json b/styles/nodemon.json new file mode 100644 index 0000000000..47facd6322 --- /dev/null +++ b/styles/nodemon.json @@ -0,0 +1,8 @@ +{ + "watch": [ + "./**/*" + ], + "ext": "ts", + "ignore": [], + "exec": "ts-node src/buildThemes.ts" +} diff --git a/styles/package-lock.json b/styles/package-lock.json new file mode 100644 index 0000000000..63bf3d5a57 --- /dev/null +++ b/styles/package-lock.json @@ -0,0 +1,2321 @@ +{ + "name": "styles", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "styles", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/chroma-js": "^2.1.3", + "@types/node": "^17.0.23", + "case-anything": "^2.1.10", + "chroma-js": "^2.4.2", + "nodemon": "^2.0.15", + "ts-node": "^10.7.0" + } + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "node_modules/@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, + "node_modules/@types/node": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/case-anything": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", + "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemon": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", + "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==", + "hasInstallScript": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5", + "update-notifier": "^5.1.0" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "engines": { + "node": ">=4" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==" + }, + "@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==" + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, + "@types/node": { + "version": "17.0.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, + "ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "requires": { + "string-width": "^4.1.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + }, + "boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + }, + "case-anything": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", + "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "requires": { + "ini": "2.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "requires": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + } + }, + "is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "requires": { + "package-json": "^6.3.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nodemon": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", + "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==", + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5", + "update-notifier": "^5.1.0" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "requires": { + "escape-goat": "^2.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" + } + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "requires": { + "nopt": "~1.0.10" + } + }, + "ts-node": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "requires": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", + "yn": "3.1.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "peer": true + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "requires": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==" + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } + } +} diff --git a/styles/package.json b/styles/package.json new file mode 100644 index 0000000000..727300e2cc --- /dev/null +++ b/styles/package.json @@ -0,0 +1,22 @@ +{ + "name": "styles", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "npm run build-themes && npm run build-tokens", + "build-themes": "ts-node ./src/buildThemes.ts", + "build-tokens": "ts-node ./src/buildTokens.ts", + "watch": "nodemon" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@types/chroma-js": "^2.1.3", + "@types/node": "^17.0.23", + "case-anything": "^2.1.10", + "chroma-js": "^2.4.2", + "ts-node": "^10.7.0", + "nodemon": "^2.0.15" + } +} diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts new file mode 100644 index 0000000000..0e8d90d96a --- /dev/null +++ b/styles/src/buildThemes.ts @@ -0,0 +1,17 @@ +import * as fs from "fs"; +import * as path from "path"; +import app from "./styleTree/app"; +import dark from "./themes/dark"; +import light from "./themes/light"; +import snakeCase from "./utils/snakeCase"; + +const themes = [dark, light]; +for (let theme of themes) { + let styleTree = snakeCase(app(theme)); + let styleTreeJSON = JSON.stringify(styleTree, null, 2); + let outPath = path.resolve( + `${__dirname}/../../assets/themes/${theme.name}.json` + ); + fs.writeFileSync(outPath, styleTreeJSON); + console.log(`- ${outPath} created`); +} diff --git a/styles/src/buildTokens.ts b/styles/src/buildTokens.ts new file mode 100644 index 0000000000..ceedd27b21 --- /dev/null +++ b/styles/src/buildTokens.ts @@ -0,0 +1,110 @@ +import * as fs from "fs"; +import * as path from "path"; +import dark from "./themes/dark"; +import light from "./themes/light"; +import Theme from "./themes/theme"; +import { colors, fontFamilies, fontSizes, fontWeights } from "./tokens"; + +// Organize theme tokens +function themeTokens(theme: Theme) { + return { + meta: { + themeName: theme.name, + }, + text: theme.textColor, + icon: theme.iconColor, + background: theme.backgroundColor, + border: theme.borderColor, + editor: theme.editor, + syntax: { + primary: { + value: theme.syntax.primary.color.value, + type: "color", + }, + comment: { + value: theme.syntax.comment.color.value, + type: "color", + }, + keyword: { + value: theme.syntax.keyword.color.value, + type: "color", + }, + function: { + value: theme.syntax.function.color.value, + type: "color", + }, + type: { + value: theme.syntax.type.color.value, + type: "color", + }, + variant: { + value: theme.syntax.variant.color.value, + type: "color", + }, + property: { + value: theme.syntax.property.color.value, + type: "color", + }, + enum: { + value: theme.syntax.enum.color.value, + type: "color", + }, + operator: { + value: theme.syntax.operator.color.value, + type: "color", + }, + string: { + value: theme.syntax.string.color.value, + type: "color", + }, + number: { + value: theme.syntax.number.color.value, + type: "color", + }, + boolean: { + value: theme.syntax.boolean.color.value, + type: "color", + }, + }, + player: theme.player, + shadowAlpha: theme.shadowAlpha, + }; +} + +// Organize core tokens +const coreTokens = { + color: { + ...colors, + }, + text: { + family: fontFamilies, + weight: fontWeights, + }, + size: fontSizes, +}; + +const combinedTokens: any = {}; + +const distPath = path.resolve(`${__dirname}/../dist`); + +// Add core tokens to the combined tokens and write `core.json`. +// We write `core.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. +const corePath = path.join(distPath, "core.json"); +fs.writeFileSync(corePath, JSON.stringify(coreTokens, null, 2)); +console.log(`- ${corePath} created`); +combinedTokens.core = coreTokens; + +// Add each theme to the combined tokens and write ${theme}.json. +// We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. +let themes = [dark, light]; +themes.forEach((theme) => { + const themePath = `${distPath}/${theme.name}.json` + fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2)); + console.log(`- ${themePath} created`); + combinedTokens[theme.name] = themeTokens(theme); +}); + +// Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app. +const combinedPath = path.resolve(`${distPath}/tokens.json`); +fs.writeFileSync(combinedPath, JSON.stringify(combinedTokens, null, 2)); +console.log(`- ${combinedPath} created`); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts new file mode 100644 index 0000000000..9596e31937 --- /dev/null +++ b/styles/src/styleTree/app.ts @@ -0,0 +1,45 @@ +import Theme from "../themes/theme"; +import chatPanel from "./chatPanel"; +import { text } from "./components"; +import contactsPanel from "./contactsPanel"; +import commandPalette from "./commandPalette"; +import editor from "./editor"; +import projectPanel from "./projectPanel"; +import search from "./search"; +import selectorModal from "./selectorModal"; +import workspace from "./workspace"; + +export const panel = { + padding: { top: 12, left: 12, bottom: 12, right: 12 }, +}; + +export default function app(theme: Theme): Object { + return { + selector: selectorModal(theme), + workspace: workspace(theme), + editor: editor(theme), + projectDiagnostics: { + tabIconSpacing: 4, + tabIconWidth: 13, + tabSummarySpacing: 10, + emptyMessage: text(theme, "sans", "primary", { size: "lg" }), + statusBarItem: { + ...text(theme, "sans", "muted"), + margin: { + right: 10, + }, + }, + }, + commandPalette: commandPalette(theme), + projectPanel: projectPanel(theme), + chatPanel: chatPanel(theme), + contactsPanel: contactsPanel(theme), + search: search(theme), + breadcrumbs: { + ...text(theme, "sans", "secondary"), + padding: { + left: 6, + }, + } + }; +} diff --git a/styles/src/styleTree/chatPanel.ts b/styles/src/styleTree/chatPanel.ts new file mode 100644 index 0000000000..69b5f3baa0 --- /dev/null +++ b/styles/src/styleTree/chatPanel.ts @@ -0,0 +1,108 @@ +import Theme from "../themes/theme"; +import { panel } from "./app"; +import { + backgroundColor, + border, + player, + shadow, + text, + TextColor +} from "./components"; + +export default function chatPanel(theme: Theme) { + function channelSelectItem( + theme: Theme, + textColor: TextColor, + hovered: boolean + ) { + return { + name: text(theme, "sans", textColor), + padding: 4, + hash: { + ...text(theme, "sans", "muted"), + margin: { + right: 8, + }, + }, + background: hovered ? backgroundColor(theme, 300, "hovered") : undefined, + cornerRadius: hovered ? 6 : 0, + }; + } + + const message = { + body: text(theme, "sans", "secondary"), + timestamp: text(theme, "sans", "muted", { size: "sm" }), + padding: { + bottom: 6, + }, + sender: { + ...text(theme, "sans", "primary", { weight: "bold" }), + margin: { + right: 8, + }, + }, + }; + + return { + ...panel, + channelName: text(theme, "sans", "primary", { weight: "bold" }), + channelNameHash: { + ...text(theme, "sans", "muted"), + padding: { + right: 8, + }, + }, + channelSelect: { + header: { + ...channelSelectItem(theme, "primary", false), + padding: { + bottom: 4, + left: 0, + }, + }, + item: channelSelectItem(theme, "secondary", false), + hoveredItem: channelSelectItem(theme, "secondary", true), + activeItem: channelSelectItem(theme, "primary", false), + hoveredActiveItem: channelSelectItem(theme, "primary", true), + menu: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + padding: 4, + border: border(theme, "primary"), + shadow: shadow(theme), + }, + }, + signInPrompt: text(theme, "sans", "secondary", { underline: true }), + hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }), + message, + pendingMessage: { + ...message, + body: { + ...message.body, + color: theme.textColor.muted.value, + }, + sender: { + ...message.sender, + color: theme.textColor.muted.value, + }, + timestamp: { + ...message.timestamp, + color: theme.textColor.muted.value, + }, + }, + inputEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 7, + left: 8, + right: 8, + top: 7, + }, + }, + }; +} diff --git a/styles/src/styleTree/commandPalette.ts b/styles/src/styleTree/commandPalette.ts new file mode 100644 index 0000000000..fbd9bea9b0 --- /dev/null +++ b/styles/src/styleTree/commandPalette.ts @@ -0,0 +1,23 @@ +import Theme from "../themes/theme"; +import { text, backgroundColor, border } from "./components"; + +export default function commandPalette(theme: Theme) { + return { + keystrokeSpacing: 8, + key: { + text: text(theme, "mono", "secondary", { size: "xs" }), + cornerRadius: 4, + background: backgroundColor(theme, "on300"), + border: border(theme, "secondary"), + padding: { + top: 2, + bottom: 2, + left: 8, + right: 8, + }, + margin: { + left: 2 + }, + } + } +} diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts new file mode 100644 index 0000000000..d0412af02e --- /dev/null +++ b/styles/src/styleTree/components.ts @@ -0,0 +1,93 @@ +import chroma from "chroma-js"; +import Theme, { BackgroundColorSet } from "../themes/theme"; +import { fontFamilies, fontSizes, FontWeight } from "../tokens"; +import { Color } from "../utils/color"; + +export type TextColor = keyof Theme["textColor"]; +export function text( + theme: Theme, + fontFamily: keyof typeof fontFamilies, + color: TextColor, + properties?: { + size?: keyof typeof fontSizes; + weight?: FontWeight; + underline?: boolean; + } +) { + let size = fontSizes[properties?.size || "sm"].value; + return { + family: fontFamilies[fontFamily].value, + color: theme.textColor[color].value, + ...properties, + size, + }; +} +export function textColor(theme: Theme, color: TextColor) { + return theme.textColor[color].value; +} + +export type BorderColor = keyof Theme["borderColor"]; +export interface BorderOptions { + width?: number; + top?: boolean; + bottom?: boolean; + left?: boolean; + right?: boolean; + overlay?: boolean; +} +export function border( + theme: Theme, + color: BorderColor, + options?: BorderOptions +) { + return { + color: borderColor(theme, color), + width: 1, + ...options, + }; +} +export function borderColor(theme: Theme, color: BorderColor) { + return theme.borderColor[color].value; +} + +export type IconColor = keyof Theme["iconColor"]; +export function iconColor(theme: Theme, color: IconColor) { + return theme.iconColor[color].value; +} + +export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; +export interface Player { + selection: { + cursor: Color; + selection: Color; + }; +} +export function player( + theme: Theme, + playerNumber: PlayerIndex, +): Player { + return { + selection: { + cursor: theme.player[playerNumber].cursorColor.value, + selection: theme.player[playerNumber].selectionColor.value, + }, + }; +} + +export type BackgroundColor = keyof Theme["backgroundColor"]; +export type BackgroundState = keyof BackgroundColorSet; +export function backgroundColor( + theme: Theme, + name: BackgroundColor, + state?: BackgroundState, +): Color { + return theme.backgroundColor[name][state || "base"].value; +} + +export function shadow(theme: Theme) { + return { + blur: 16, + color: chroma("black").alpha(theme.shadowAlpha.value).hex(), + offset: [0, 2], + }; +} diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts new file mode 100644 index 0000000000..e22a09e25f --- /dev/null +++ b/styles/src/styleTree/contactsPanel.ts @@ -0,0 +1,62 @@ +import Theme from "../themes/theme"; +import { panel } from "./app"; +import { backgroundColor, borderColor, text } from "./components"; + +export default function(theme: Theme) { + const project = { + guestAvatarSpacing: 4, + height: 24, + guestAvatar: { + cornerRadius: 8, + width: 14, + }, + name: { + ...text(theme, "mono", "placeholder", { size: "sm" }), + margin: { + right: 6, + }, + }, + padding: { + left: 8, + }, + }; + + const sharedProject = { + ...project, + background: backgroundColor(theme, 300), + cornerRadius: 6, + name: { + ...project.name, + ...text(theme, "mono", "secondary", { size: "sm" }), + }, + }; + + return { + ...panel, + hostRowHeight: 28, + treeBranchColor: borderColor(theme, "muted"), + treeBranchWidth: 1, + hostAvatar: { + cornerRadius: 10, + width: 18, + }, + hostUsername: { + ...text(theme, "mono", "primary", { size: "sm" }), + padding: { + left: 8, + }, + }, + project, + sharedProject, + hoveredSharedProject: { + ...sharedProject, + background: backgroundColor(theme, 300, "hovered"), + cornerRadius: 6, + }, + unsharedProject: project, + hoveredUnsharedProject: { + ...project, + cornerRadius: 6, + }, + } +} diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts new file mode 100644 index 0000000000..db7ebd63f7 --- /dev/null +++ b/styles/src/styleTree/editor.ts @@ -0,0 +1,146 @@ +import Theme from "../themes/theme"; +import { + backgroundColor, + border, + iconColor, + player, + text, + TextColor +} from "./components"; + +export default function editor(theme: Theme) { + const autocompleteItem = { + cornerRadius: 6, + padding: { + bottom: 2, + left: 6, + right: 6, + top: 2, + }, + }; + + function diagnostic(theme: Theme, color: TextColor) { + return { + textScaleFactor: 0.857, + header: { + border: border(theme, "primary", { + top: true, + }), + }, + message: { + text: text(theme, "sans", color, { size: "sm" }), + highlightText: text(theme, "sans", color, { + size: "sm", + weight: "bold", + }), + }, + }; + } + + return { + // textColor: theme.syntax.primary.color, + textColor: theme.syntax.primary.color.value, + background: backgroundColor(theme, 500), + activeLineBackground: theme.editor.line.active.value, + codeActionsIndicator: iconColor(theme, "muted"), + diffBackgroundDeleted: backgroundColor(theme, "error"), + diffBackgroundInserted: backgroundColor(theme, "ok"), + documentHighlightReadBackground: theme.editor.highlight.occurrence.value, + documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value, + errorColor: theme.textColor.error.value, + gutterBackground: backgroundColor(theme, 500), + gutterPaddingFactor: 3.5, + highlightedLineBackground: theme.editor.line.highlighted.value, + lineNumber: theme.editor.gutter.primary.value, + lineNumberActive: theme.editor.gutter.active.value, + renameFade: 0.6, + unnecessaryCodeFade: 0.5, + selection: player(theme, 1).selection, + guestSelections: [ + player(theme, 2).selection, + player(theme, 3).selection, + player(theme, 4).selection, + player(theme, 5).selection, + player(theme, 6).selection, + player(theme, 7).selection, + player(theme, 8).selection, + ], + autocomplete: { + background: backgroundColor(theme, 500), + cornerRadius: 8, + padding: 4, + border: border(theme, "secondary"), + item: autocompleteItem, + hoveredItem: { + ...autocompleteItem, + background: backgroundColor(theme, 500, "hovered"), + }, + margin: { + left: -14, + }, + matchHighlight: text(theme, "mono", "feature"), + selectedItem: { + ...autocompleteItem, + background: backgroundColor(theme, 500, "active"), + }, + }, + diagnosticHeader: { + background: backgroundColor(theme, 300), + iconWidthFactor: 1.5, + textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these. + border: border(theme, "secondary", { + bottom: true, + top: true, + }), + code: { + ...text(theme, "mono", "muted", { size: "sm" }), + margin: { + left: 10, + }, + }, + message: { + highlightText: text(theme, "sans", "primary", { + size: "sm", + weight: "bold", + }), + text: text(theme, "sans", "secondary", { size: "sm" }), + }, + }, + diagnosticPathHeader: { + background: theme.editor.line.active.value, + textScaleFactor: 0.857, + filename: text(theme, "mono", "primary", { size: "sm" }), + path: { + ...text(theme, "mono", "muted", { size: "sm" }), + margin: { + left: 12, + }, + }, + }, + errorDiagnostic: diagnostic(theme, "error"), + warningDiagnostic: diagnostic(theme, "warning"), + informationDiagnostic: diagnostic(theme, "info"), + hintDiagnostic: diagnostic(theme, "info"), + invalidErrorDiagnostic: diagnostic(theme, "muted"), + invalidHintDiagnostic: diagnostic(theme, "muted"), + invalidInformationDiagnostic: diagnostic(theme, "muted"), + invalidWarningDiagnostic: diagnostic(theme, "muted"), + syntax: { + keyword: theme.syntax.keyword.color.value, + function: theme.syntax.function.color.value, + string: theme.syntax.string.color.value, + type: theme.syntax.type.color.value, + number: theme.syntax.number.color.value, + comment: theme.syntax.comment.color.value, + property: theme.syntax.property.color.value, + variant: theme.syntax.variant.color.value, + constant: theme.syntax.constant.color.value, + title: { color: theme.syntax.title.color.value, weight: "bold" }, + emphasis: theme.textColor.feature.value, + "emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" }, + link_uri: { color: theme.syntax.linkUrl.color.value, underline: true }, + link_text: { color: theme.syntax.linkText.color.value, italic: true }, + list_marker: theme.syntax.punctuation.color.value, + }, + }; +} diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts new file mode 100644 index 0000000000..de9b1d88f3 --- /dev/null +++ b/styles/src/styleTree/projectPanel.ts @@ -0,0 +1,37 @@ +import Theme from "../themes/theme"; +import { Color } from "../utils/color"; +import { panel } from "./app"; +import { backgroundColor, iconColor, text, TextColor } from "./components"; + +export default function projectPanel(theme: Theme) { + function entry(theme: Theme, textColor: TextColor, background?: Color) { + return { + height: 22, + background, + iconColor: iconColor(theme, "muted"), + iconSize: 8, + iconSpacing: 8, + text: text(theme, "mono", textColor, { size: "sm" }), + }; + } + + return { + ...panel, + entry: entry(theme, "secondary"), + hoveredEntry: entry( + theme, + "secondary", + backgroundColor(theme, 300, "hovered") + ), + selectedEntry: entry(theme, "primary"), + hoveredSelectedEntry: entry( + theme, + "primary", + backgroundColor(theme, 300, "hovered") + ), + padding: { + top: 6, + left: 12, + }, + }; +} diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts new file mode 100644 index 0000000000..57aeff0430 --- /dev/null +++ b/styles/src/styleTree/search.ts @@ -0,0 +1,84 @@ +import Theme from "../themes/theme"; +import { backgroundColor, border, player, text } from "./components"; + +export default function search(theme: Theme) { + const optionButton = { + ...text(theme, "mono", "secondary"), + background: backgroundColor(theme, "on500"), + cornerRadius: 4, + border: border(theme, "secondary"), + margin: { + left: 2, + right: 2, + }, + padding: { + bottom: 3, + left: 8, + right: 8, + top: 3, + }, + }; + + const editor = { + background: backgroundColor(theme, 500), + cornerRadius: 8, + minWidth: 200, + maxWidth: 500, + placeholderText: text(theme, "mono", "placeholder"), + selection: player(theme, 1).selection, + text: text(theme, "mono", "active"), + border: border(theme, "secondary"), + margin: { + right: 6, + }, + padding: { + top: 3, + bottom: 3, + left: 12, + right: 8, + }, + }; + + return { + matchBackground: theme.editor.highlight.match.value, + tabIconSpacing: 8, + tabIconWidth: 14, + activeHoveredOptionButton: { + ...optionButton, + ...text(theme, "mono", "active"), + background: backgroundColor(theme, "on500", "active"), + border: border(theme, "muted"), + }, + activeOptionButton: { + ...optionButton, + ...text(theme, "mono", "active"), + background: backgroundColor(theme, "on500", "active"), + border: border(theme, "muted"), + }, + editor, + hoveredOptionButton: { + ...optionButton, + ...text(theme, "mono", "active"), + border: border(theme, "muted"), + }, + invalidEditor: { + ...editor, + border: border(theme, "error"), + }, + matchIndex: { + ...text(theme, "mono", "muted"), + padding: 6, + }, + optionButton, + optionButtonGroup: { + padding: { + left: 4, + right: 4, + }, + }, + resultsStatus: { + ...text(theme, "mono", "primary"), + size: 18, + }, + }; +} diff --git a/styles/src/styleTree/selectorModal.ts b/styles/src/styleTree/selectorModal.ts new file mode 100644 index 0000000000..b4dce880a7 --- /dev/null +++ b/styles/src/styleTree/selectorModal.ts @@ -0,0 +1,59 @@ +import Theme from "../themes/theme"; +import { backgroundColor, border, player, shadow, text } from "./components"; + +export default function selectorModal(theme: Theme): Object { + const item = { + padding: { + bottom: 4, + left: 12, + right: 12, + top: 4, + }, + cornerRadius: 8, + text: text(theme, "sans", "secondary"), + highlightText: text(theme, "sans", "feature", { weight: "bold" }), + }; + + const activeItem = { + ...item, + background: backgroundColor(theme, 300, "active"), + text: text(theme, "sans", "primary"), + }; + + return { + background: backgroundColor(theme, 300), + cornerRadius: 8, + padding: 8, + item, + activeItem, + border: border(theme, "primary"), + empty: { + text: text(theme, "sans", "placeholder"), + padding: { + bottom: 4, + left: 12, + right: 12, + top: 8, + }, + }, + inputEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 8, + placeholderText: text(theme, "sans", "placeholder"), + selection: player(theme, 1).selection, + text: text(theme, "mono", "primary"), + border: border(theme, "secondary"), + padding: { + bottom: 7, + left: 16, + right: 16, + top: 7, + }, + }, + margin: { + bottom: 52, + top: 52, + }, + shadow: shadow(theme), + }; +} diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts new file mode 100644 index 0000000000..57de2f01f4 --- /dev/null +++ b/styles/src/styleTree/workspace.ts @@ -0,0 +1,158 @@ +import Theme from "../themes/theme"; +import { backgroundColor, border, iconColor, text } from "./components"; + +export default function workspace(theme: Theme) { + const signInPrompt = { + ...text(theme, "sans", "secondary", { size: "xs" }), + border: border(theme, "primary"), + cornerRadius: 6, + margin: { + top: 1, + right: 6, + }, + padding: { + left: 6, + right: 6, + }, + }; + + const tab = { + height: 32, + background: backgroundColor(theme, 300), + iconClose: iconColor(theme, "muted"), + iconCloseActive: iconColor(theme, "active"), + iconConflict: iconColor(theme, "warning"), + iconDirty: iconColor(theme, "info"), + iconWidth: 8, + spacing: 8, + text: text(theme, "sans", "secondary", { size: "sm" }), + border: border(theme, "primary", { + left: true, + bottom: true, + overlay: true, + }), + padding: { + left: 8, + right: 8, + }, + }; + + const activeTab = { + ...tab, + background: backgroundColor(theme, 500), + text: text(theme, "sans", "active", { size: "sm" }), + border: { + ...tab.border, + bottom: false, + }, + }; + + const sidebarItem = { + height: 32, + iconColor: iconColor(theme, "secondary"), + iconSize: 18, + }; + const sidebar = { + width: 30, + background: backgroundColor(theme, 300), + border: border(theme, "primary", { right: true }), + item: sidebarItem, + activeItem: { + ...sidebarItem, + iconColor: iconColor(theme, "active"), + }, + resizeHandle: { + background: border(theme, "primary").color, + padding: { + left: 1, + }, + }, + }; + + return { + background: backgroundColor(theme, 300), + leaderBorderOpacity: 0.7, + leaderBorderWidth: 2.0, + tab, + activeTab, + leftSidebar: { + ...sidebar, + border: border(theme, "primary", { right: true }), + }, + rightSidebar: { + ...sidebar, + border: border(theme, "primary", { left: true }), + }, + paneDivider: { + color: border(theme, "secondary").color, + width: 1, + }, + status_bar: { + height: 24, + itemSpacing: 8, + padding: { + left: 6, + right: 6, + }, + border: border(theme, "primary", { top: true, overlay: true }), + cursorPosition: text(theme, "sans", "muted"), + diagnosticMessage: text(theme, "sans", "muted"), + lspMessage: text(theme, "sans", "muted"), + autoUpdateProgressMessage: text(theme, "sans", "muted"), + autoUpdateDoneMessage: text(theme, "sans", "muted"), + }, + titlebar: { + avatarWidth: 18, + height: 32, + background: backgroundColor(theme, 100), + shareIconColor: iconColor(theme, "secondary"), + shareIconActiveColor: iconColor(theme, "feature"), + title: text(theme, "sans", "primary"), + avatar: { + cornerRadius: 10, + border: { + color: "#00000088", + width: 1, + }, + }, + avatarRibbon: { + height: 3, + width: 12, + // TODO: The background for this ideally should be + // set with a token, not hardcoded in rust + }, + border: border(theme, "primary", { bottom: true }), + signInPrompt, + hoveredSignInPrompt: { + ...signInPrompt, + ...text(theme, "sans", "active", { size: "xs" }), + }, + offlineIcon: { + color: iconColor(theme, "secondary"), + width: 16, + padding: { + right: 4, + }, + }, + outdatedWarning: { + ...text(theme, "sans", "warning"), + size: 13, + }, + }, + toolbar: { + height: 34, + background: backgroundColor(theme, 500), + border: border(theme, "secondary", { bottom: true }), + itemSpacing: 8, + padding: { left: 16, right: 8, top: 4, bottom: 4 }, + }, + breadcrumbs: { + ...text(theme, "mono", "secondary"), + padding: { left: 6 }, + }, + disconnectedOverlay: { + ...text(theme, "sans", "active"), + background: "#000000aa", + }, + }; +} diff --git a/styles/src/themes/dark.ts b/styles/src/themes/dark.ts new file mode 100644 index 0000000000..f7407699dd --- /dev/null +++ b/styles/src/themes/dark.ts @@ -0,0 +1,241 @@ +import { colors, fontWeights, NumberToken } from "../tokens"; +import { withOpacity } from "../utils/color"; +import Theme, { buildPlayer, Syntax } from "./theme"; + +const backgroundColor = { + 100: { + base: colors.neutral[750], + hovered: colors.neutral[725], + active: colors.neutral[800], + focused: colors.neutral[675], + }, + 300: { + base: colors.neutral[800], + hovered: colors.neutral[775], + active: colors.neutral[750], + focused: colors.neutral[775], + }, + 500: { + base: colors.neutral[900], + hovered: withOpacity(colors.neutral[0], 0.08), + active: withOpacity(colors.neutral[0], 0.12), + focused: colors.neutral[825], + }, + on300: { + base: withOpacity(colors.neutral[850], 0.5), + hovered: colors.neutral[875], + active: colors.neutral[900], + focused: colors.neutral[875], + }, + on500: { + base: colors.neutral[850], + hovered: colors.neutral[800], + active: colors.neutral[775], + focused: colors.neutral[800], + }, + ok: { + base: colors.green[600], + hovered: colors.green[600], + active: colors.green[600], + focused: colors.green[600], + }, + error: { + base: colors.red[400], + hovered: colors.red[400], + active: colors.red[400], + focused: colors.red[400], + }, + warning: { + base: colors.amber[300], + hovered: colors.amber[300], + active: colors.amber[300], + focused: colors.amber[300], + }, + info: { + base: colors.blue[500], + hovered: colors.blue[500], + active: colors.blue[500], + focused: colors.blue[500], + }, +}; + +const borderColor = { + primary: colors.neutral[875], + secondary: colors.neutral[775], + muted: colors.neutral[675], + focused: colors.indigo[500], + active: colors.neutral[900], + ok: colors.green[500], + error: colors.red[500], + warning: colors.amber[500], + info: colors.blue[500], +}; + +const textColor = { + primary: colors.neutral[50], + secondary: colors.neutral[350], + muted: colors.neutral[450], + placeholder: colors.neutral[650], + active: colors.neutral[0], + //TODO: (design) define feature and it's correct value + feature: colors.blue[400], + ok: colors.green[600], + error: colors.red[400], + warning: colors.amber[300], + info: colors.blue[500], +}; + +const iconColor = { + primary: colors.neutral[200], + secondary: colors.neutral[350], + muted: colors.neutral[600], + placeholder: colors.neutral[700], + active: colors.neutral[0], + //TODO: (design) define feature and it's correct value + feature: colors.blue[500], + ok: colors.green[600], + error: colors.red[500], + warning: colors.amber[400], + info: colors.blue[600], +}; + +const player = { + 1: buildPlayer(colors.blue[500]), + 2: buildPlayer(colors.lime[500]), + 3: buildPlayer(colors.fuschia[500]), + 4: buildPlayer(colors.orange[500]), + 5: buildPlayer(colors.purple[500]), + 6: buildPlayer(colors.teal[400]), + 7: buildPlayer(colors.pink[400]), + 8: buildPlayer(colors.yellow[400]), +}; + +const editor = { + background: backgroundColor[500].base, + indent_guide: borderColor.muted, + indent_guide_active: borderColor.secondary, + line: { + active: withOpacity(colors.neutral[0], 0.07), + highlighted: withOpacity(colors.neutral[0], 0.12), + inserted: backgroundColor.ok.active, + deleted: backgroundColor.error.active, + modified: backgroundColor.info.active, + }, + highlight: { + selection: player[1].selectionColor, + occurrence: withOpacity(colors.neutral[0], 0.12), + activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side + matchingBracket: backgroundColor[500].active, + match: withOpacity(colors.violet[700], 0.5), + activeMatch: withOpacity(colors.violet[600], 0.7), + related: backgroundColor[500].focused, + }, + gutter: { + primary: textColor.placeholder, + active: textColor.active, + }, +}; + +const syntax: Syntax = { + primary: { + color: colors.neutral[150], + weight: fontWeights.normal, + }, + comment: { + color: colors.neutral[300], + weight: fontWeights.normal, + }, + punctuation: { + color: colors.neutral[200], + weight: fontWeights.normal, + }, + constant: { + color: colors.neutral[150], + weight: fontWeights.normal, + }, + keyword: { + color: colors.blue[400], + weight: fontWeights.normal, + }, + function: { + color: colors.yellow[200], + weight: fontWeights.normal, + }, + type: { + color: colors.teal[300], + weight: fontWeights.normal, + }, + variant: { + color: colors.sky[300], + weight: fontWeights.normal, + }, + property: { + color: colors.blue[400], + weight: fontWeights.normal, + }, + enum: { + color: colors.orange[500], + weight: fontWeights.normal, + }, + operator: { + color: colors.orange[500], + weight: fontWeights.normal, + }, + string: { + color: colors.orange[300], + weight: fontWeights.normal, + }, + number: { + color: colors.lime[300], + weight: fontWeights.normal, + }, + boolean: { + color: colors.lime[300], + weight: fontWeights.normal, + }, + predictive: { + color: textColor.muted, + weight: fontWeights.normal, + }, + title: { + color: colors.amber[500], + weight: fontWeights.bold, + }, + emphasis: { + color: textColor.active, + weight: fontWeights.normal, + }, + emphasisStrong: { + color: textColor.active, + weight: fontWeights.bold, + }, + linkUrl: { + color: colors.lime[500], + weight: fontWeights.normal, + // TODO: add underline + }, + linkText: { + color: colors.orange[500], + weight: fontWeights.normal, + // TODO: add italic + }, +}; + +const shadowAlpha: NumberToken = { + value: 0.32, + type: "number", +}; + +const theme: Theme = { + name: "dark", + backgroundColor, + borderColor, + textColor, + iconColor, + editor, + syntax, + player, + shadowAlpha, +}; + +export default theme; diff --git a/styles/src/themes/light.ts b/styles/src/themes/light.ts new file mode 100644 index 0000000000..397c165341 --- /dev/null +++ b/styles/src/themes/light.ts @@ -0,0 +1,239 @@ +import { colors, fontWeights, NumberToken } from "../tokens"; +import { withOpacity } from "../utils/color"; +import Theme, { buildPlayer, Syntax } from "./theme"; + +const backgroundColor = { + 100: { + base: colors.neutral[75], + hovered: colors.neutral[100], + active: colors.neutral[150], + focused: colors.neutral[100], + }, + 300: { + base: colors.neutral[25], + hovered: colors.neutral[75], + active: colors.neutral[100], + focused: colors.neutral[75], + }, + 500: { + base: colors.neutral[0], + hovered: withOpacity(colors.neutral[900], 0.03), + active: withOpacity(colors.neutral[900], 0.06), + focused: colors.neutral[50], + }, + on300: { + base: colors.neutral[50], + hovered: colors.neutral[100], + active: colors.neutral[150], + focused: colors.neutral[100], + }, + on500: { + base: colors.neutral[50], + hovered: colors.neutral[25], + active: colors.neutral[0], + focused: colors.neutral[25], + }, + ok: { + base: colors.green[100], + hovered: colors.green[100], + active: colors.green[100], + focused: colors.green[100], + }, + error: { + base: colors.red[100], + hovered: colors.red[100], + active: colors.red[100], + focused: colors.red[100], + }, + warning: { + base: colors.yellow[100], + hovered: colors.yellow[100], + active: colors.yellow[100], + focused: colors.yellow[100], + }, + info: { + base: colors.blue[100], + hovered: colors.blue[100], + active: colors.blue[100], + focused: colors.blue[100], + }, +}; + +const borderColor = { + primary: colors.neutral[150], + secondary: colors.neutral[150], + muted: colors.neutral[100], + focused: colors.indigo[500], + active: colors.neutral[250], + ok: colors.green[200], + error: colors.red[200], + warning: colors.yellow[200], + info: colors.blue[200], +}; + +const textColor = { + primary: colors.neutral[750], + secondary: colors.neutral[650], + muted: colors.neutral[550], + placeholder: colors.neutral[450], + active: colors.neutral[900], + feature: colors.indigo[500], + ok: colors.green[500], + error: colors.red[500], + warning: colors.yellow[500], + info: colors.blue[500], +}; + +const iconColor = { + primary: colors.neutral[700], + secondary: colors.neutral[500], + muted: colors.neutral[350], + placeholder: colors.neutral[300], + active: colors.neutral[900], + feature: colors.indigo[500], + ok: colors.green[600], + error: colors.red[600], + warning: colors.yellow[400], + info: colors.blue[600], +}; + +const player = { + 1: buildPlayer(colors.blue[500]), + 2: buildPlayer(colors.emerald[400]), + 3: buildPlayer(colors.fuschia[400]), + 4: buildPlayer(colors.orange[400]), + 5: buildPlayer(colors.purple[400]), + 6: buildPlayer(colors.teal[400]), + 7: buildPlayer(colors.pink[400]), + 8: buildPlayer(colors.yellow[400]), +}; + +const editor = { + background: backgroundColor[500].base, + indent_guide: borderColor.muted, + indent_guide_active: borderColor.secondary, + line: { + active: withOpacity(colors.neutral[900], 0.06), + highlighted: withOpacity(colors.neutral[900], 0.12), + inserted: backgroundColor.ok.active, + deleted: backgroundColor.error.active, + modified: backgroundColor.info.active, + }, + highlight: { + selection: player[1].selectionColor, + occurrence: withOpacity(colors.neutral[900], 0.06), + activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side + matchingBracket: colors.neutral[0], + match: colors.yellow[100], + activeMatch: colors.yellow[200], // TODO: This is not hooked up to occurences on the rust side + related: colors.neutral[0], + }, + gutter: { + primary: colors.neutral[300], + active: textColor.active, + }, +}; + +const syntax: Syntax = { + primary: { + color: colors.neutral[800], + weight: fontWeights.normal, + }, + comment: { + color: colors.neutral[500], + weight: fontWeights.normal, + }, + punctuation: { + color: colors.neutral[600], + weight: fontWeights.normal, + }, + constant: { + color: colors.neutral[800], + weight: fontWeights.normal, + }, + keyword: { + color: colors.indigo[700], + weight: fontWeights.normal, + }, + function: { + color: colors.orange[600], + weight: fontWeights.normal, + }, + type: { + color: colors.yellow[600], + weight: fontWeights.normal, + }, + variant: { + color: colors.rose[700], + weight: fontWeights.normal, + }, + property: { + color: colors.emerald[700], + weight: fontWeights.normal, + }, + enum: { + color: colors.red[500], + weight: fontWeights.normal, + }, + operator: { + color: colors.red[500], + weight: fontWeights.normal, + }, + string: { + color: colors.red[500], + weight: fontWeights.normal, + }, + number: { + color: colors.indigo[500], + weight: fontWeights.normal, + }, + boolean: { + color: colors.red[500], + weight: fontWeights.normal, + }, + predictive: { + color: textColor.placeholder, + weight: fontWeights.normal, + }, + title: { + color: colors.sky[500], + weight: fontWeights.bold, + }, + emphasis: { + color: textColor.active, + weight: fontWeights.normal, + }, + emphasisStrong: { + color: textColor.active, + weight: fontWeights.bold, + }, + linkUrl: { + color: colors.lime[500], + weight: fontWeights.normal, + // TODO: add underline + }, + linkText: { + color: colors.red[500], + weight: fontWeights.normal, + // TODO: add italic + }, +}; + +const shadowAlpha: NumberToken = { + value: 0.12, + type: "number", +}; + +const theme: Theme = { + name: "light", + backgroundColor, + borderColor, + textColor, + iconColor, + editor, + syntax, + player, + shadowAlpha, +}; + +export default theme; diff --git a/styles/src/themes/theme.ts b/styles/src/themes/theme.ts new file mode 100644 index 0000000000..f4b3bcc680 --- /dev/null +++ b/styles/src/themes/theme.ts @@ -0,0 +1,147 @@ +import { ColorToken, FontWeightToken, NumberToken } from "../tokens"; +import { withOpacity } from "../utils/color"; + +export interface SyntaxHighlightStyle { + color: ColorToken; + weight: FontWeightToken; +} + +export interface Player { + baseColor: ColorToken; + cursorColor: ColorToken; + selectionColor: ColorToken; + borderColor: ColorToken; +} +export function buildPlayer( + color: ColorToken, + cursorOpacity?: number, + selectionOpacity?: number, + borderOpacity?: number +) { + return { + baseColor: color, + cursorColor: withOpacity(color, cursorOpacity || 1.0), + selectionColor: withOpacity(color, selectionOpacity || 0.24), + borderColor: withOpacity(color, borderOpacity || 0.8), + } +} + +export interface BackgroundColorSet { + base: ColorToken; + hovered: ColorToken; + active: ColorToken; + focused: ColorToken; +} + +export interface Syntax { + primary: SyntaxHighlightStyle; + comment: SyntaxHighlightStyle; + punctuation: SyntaxHighlightStyle; + constant: SyntaxHighlightStyle; + keyword: SyntaxHighlightStyle; + function: SyntaxHighlightStyle; + type: SyntaxHighlightStyle; + variant: SyntaxHighlightStyle; + property: SyntaxHighlightStyle; + enum: SyntaxHighlightStyle; + operator: SyntaxHighlightStyle; + string: SyntaxHighlightStyle; + number: SyntaxHighlightStyle; + boolean: SyntaxHighlightStyle; + predictive: SyntaxHighlightStyle; + // TODO: Either move the following or rename + title: SyntaxHighlightStyle; + emphasis: SyntaxHighlightStyle; + emphasisStrong: SyntaxHighlightStyle; + linkUrl: SyntaxHighlightStyle; + linkText: SyntaxHighlightStyle; +}; + +export default interface Theme { + name: string; + backgroundColor: { + 100: BackgroundColorSet; + 300: BackgroundColorSet; + 500: BackgroundColorSet; + on300: BackgroundColorSet; + on500: BackgroundColorSet; + ok: BackgroundColorSet; + error: BackgroundColorSet; + warning: BackgroundColorSet; + info: BackgroundColorSet; + }; + borderColor: { + primary: ColorToken; + secondary: ColorToken; + muted: ColorToken; + focused: ColorToken; + active: ColorToken; + ok: ColorToken; + error: ColorToken; + warning: ColorToken; + info: ColorToken; + }; + textColor: { + primary: ColorToken; + secondary: ColorToken; + muted: ColorToken; + placeholder: ColorToken; + active: ColorToken; + feature: ColorToken; + ok: ColorToken; + error: ColorToken; + warning: ColorToken; + info: ColorToken; + }; + iconColor: { + primary: ColorToken; + secondary: ColorToken; + muted: ColorToken; + placeholder: ColorToken; + active: ColorToken; + feature: ColorToken; + ok: ColorToken; + error: ColorToken; + warning: ColorToken; + info: ColorToken; + }; + editor: { + background: ColorToken; + indent_guide: ColorToken; + indent_guide_active: ColorToken; + line: { + active: ColorToken; + highlighted: ColorToken; + inserted: ColorToken; + deleted: ColorToken; + modified: ColorToken; + }; + highlight: { + selection: ColorToken; + occurrence: ColorToken; + activeOccurrence: ColorToken; + matchingBracket: ColorToken; + match: ColorToken; + activeMatch: ColorToken; + related: ColorToken; + }; + gutter: { + primary: ColorToken; + active: ColorToken; + }; + }; + + syntax: Syntax, + + player: { + 1: Player; + 2: Player; + 3: Player; + 4: Player; + 5: Player; + 6: Player; + 7: Player; + 8: Player; + }; + shadowAlpha: NumberToken; +} diff --git a/styles/src/tokens.ts b/styles/src/tokens.ts new file mode 100644 index 0000000000..5e412ae042 --- /dev/null +++ b/styles/src/tokens.ts @@ -0,0 +1,102 @@ +import { colorRamp } from "./utils/color"; + +interface Token { + value: V, + type: T +} + +export type FontFamily = string; +export type FontFamilyToken = Token; +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; +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; +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; + +export default { + fontFamilies, + fontSizes, + fontWeights, + colors, +}; diff --git a/styles/src/utils/color.ts b/styles/src/utils/color.ts new file mode 100644 index 0000000000..196c3d4e2e --- /dev/null +++ b/styles/src/utils/color.ts @@ -0,0 +1,52 @@ +import chroma, { Scale } from "chroma-js"; +import { ColorToken } from "../tokens"; + +export type Color = string; +export type ColorRampStep = { value: Color; type: "color"; step: number }; +export type ColorRamp = { + [index: number]: ColorRampStep; +}; + +export function colorRamp( + color: Color | [Color, Color], + options?: { steps?: number; increment?: number; } +): ColorRamp { + let scale: Scale; + if (Array.isArray(color)) { + const [startColor, endColor] = color; + scale = chroma.scale([startColor, endColor]); + } else { + let hue = Math.round(chroma(color).hsl()[0]); + let startColor = chroma.hsl(hue, 0.88, 0.96); + let endColor = chroma.hsl(hue, 0.68, 0.12); + scale = chroma + .scale([startColor, color, endColor]) + .domain([0, 0.5, 1]) + .mode("hsl") + .gamma(1) + // .correctLightness(true) + .padding([0, 0]); + } + + const ramp: ColorRamp = {}; + const steps = options?.steps || 10; + const increment = options?.increment || 100; + + scale.colors(steps, "hex").forEach((color, ix) => { + const step = ix * increment; + ramp[step] = { + value: color, + step, + type: "color", + }; + }); + + return ramp; +} + +export function withOpacity(color: ColorToken, opacity: number): ColorToken { + return { + ...color, + value: chroma(color.value).alpha(opacity).hex() + }; +} diff --git a/styles/src/utils/snakeCase.ts b/styles/src/utils/snakeCase.ts new file mode 100644 index 0000000000..890017f1c6 --- /dev/null +++ b/styles/src/utils/snakeCase.ts @@ -0,0 +1,35 @@ +import { snakeCase } from "case-anything"; + +// https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case + +// Typescript magic to convert any string from camelCase to snake_case at compile time +type SnakeCase = + S extends string ? + S extends `${infer T}${infer U}` ? + `${T extends Capitalize ? "_" : ""}${Lowercase}${SnakeCase}` : + S : + S; + +type SnakeCased = { + [Property in keyof Type as SnakeCase]: SnakeCased +} + +export default function snakeCaseTree(object: T): SnakeCased { + 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; + } +} diff --git a/styles/tsconfig.json b/styles/tsconfig.json new file mode 100644 index 0000000000..fa3e3062c9 --- /dev/null +++ b/styles/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "commonjs", + "esModuleInterop": true, + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true + }, + "exclude": [ + "node_modules" + ] +}