diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cd717d5c1f576668fead517290f8be36018e786..f1c16b2d4d2a540da2ff6bbd00f01323b820f12e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,6 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }} steps: - name: Install Rust run: | diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index d963ac46e062c5272908c4bcde9b2455ada91dbf..aaef0b536d005ea7015a6ca94cb956f049434add 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -6,8 +6,8 @@ on: push: branches: - randomized-tests-runner - schedule: - - cron: '0 * * * *' + # schedule: + # - cron: '0 * * * *' env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 5feb29e46930ab249a62050fcb9c7c1623ca7398..4ccab09cbe10c56f49d43e08d6483b28846021cb 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -21,19 +21,3 @@ jobs: ${{ github.event.release.body }} ``` - mixpanel_release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.10.5" - architecture: "x64" - cache: "pip" - - run: pip install -r script/mixpanel_release/requirements.txt - - run: > - python script/mixpanel_release/main.py - ${{ github.event.release.tag_name }} - ${{ secrets.MIXPANEL_PROJECT_ID }} - ${{ secrets.MIXPANEL_SERVICE_ACCOUNT_USERNAME }} - ${{ secrets.MIXPANEL_SERVICE_ACCOUNT_SECRET }} diff --git a/Cargo.lock b/Cargo.lock index 4bea44ffeef623156babd1ecb3c5b7db9b2be4fb..a7cbf1ec4f83877659a69eb4ac1b217a0607aa95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,15 +100,24 @@ name = "ai" version = "0.1.0" dependencies = [ "anyhow", - "assets", + "chrono", "collections", "editor", + "fs", "futures 0.3.28", "gpui", "isahc", + "language", + "menu", + "schemars", + "search", "serde", "serde_json", + "settings", + "theme", + "tiktoken-rs", "util", + "workspace", ] [[package]] @@ -180,15 +189,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "anyhow" version = "1.0.71" @@ -219,15 +219,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" -[[package]] -name = "assets" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "rust-embed", -] - [[package]] name = "async-broadcast" version = "0.4.1" @@ -404,7 +395,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -452,7 +443,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -495,7 +486,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -715,27 +706,42 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.59.2" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ "bitflags", "cexpr", "clang-sys", - "clap 2.34.0", - "env_logger 0.9.3", "lazy_static", "lazycell", "log", "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", + "syn 2.0.18", "which", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -861,6 +867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", + "once_cell", + "regex-automata", "serde", ] @@ -1085,21 +1093,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - [[package]] name = "clap" version = "3.2.25" @@ -1112,9 +1105,9 @@ dependencies = [ "clap_lex", "indexmap", "once_cell", - "strsim 0.10.0", + "strsim", "termcolor", - "textwrap 0.16.0", + "textwrap", ] [[package]] @@ -1144,7 +1137,7 @@ name = "cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap", "core-foundation", "core-services", "dirs 3.0.2", @@ -1246,7 +1239,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.12.4" +version = "0.13.1" dependencies = [ "anyhow", "async-tungstenite", @@ -1254,7 +1247,7 @@ dependencies = [ "axum-extra", "base64 0.13.1", "call", - "clap 3.2.25", + "clap", "client", "collections", "ctor", @@ -1426,7 +1419,6 @@ name = "copilot_button" version = "0.1.0" dependencies = [ "anyhow", - "assets", "context_menu", "copilot", "editor", @@ -1797,7 +1789,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -1814,7 +1806,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2211,6 +2203,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -2588,7 +2590,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -4349,7 +4351,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -4794,6 +4796,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +dependencies = [ + "proc-macro2", + "syn 2.0.18", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -4829,9 +4841,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -5108,9 +5120,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -6054,7 +6066,7 @@ checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6097,7 +6109,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6129,7 +6141,6 @@ name = "settings" version = "0.1.0" dependencies = [ "anyhow", - "assets", "collections", "fs", "futures 0.3.28", @@ -6138,6 +6149,7 @@ dependencies = [ "lazy_static", "postage", "pretty_assertions", + "rust-embed", "schemars", "serde", "serde_derive", @@ -6584,12 +6596,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -6661,9 +6667,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -6849,15 +6855,6 @@ dependencies = [ "util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "textwrap" version = "0.16.0" @@ -6931,7 +6928,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6961,6 +6958,21 @@ dependencies = [ "weezl", ] +[[package]] +name = "tiktoken-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba161c549e2c0686f35f5d920e63fad5cafba2c28ad2caceaf07e5d9fa6e8c4" +dependencies = [ + "anyhow", + "base64 0.21.0", + "bstr", + "fancy-regex", + "lazy_static", + "parking_lot 0.12.1", + "rustc-hash", +] + [[package]] name = "time" version = "0.1.45" @@ -7089,7 +7101,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7277,7 +7289,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7382,8 +7394,8 @@ dependencies = [ [[package]] name = "tree-sitter-elixir" -version = "0.19.0" -source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" +version = "0.1.0" +source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e" dependencies = [ "cc", "tree-sitter", @@ -7545,7 +7557,7 @@ dependencies = [ [[package]] name = "tree-sitter-yaml" version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492" +source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=5694b7f290cd9ef998829a0a6d8391a666370886#5694b7f290cd9ef998829a0a6d8391a666370886" dependencies = [ "cc", "tree-sitter", @@ -7779,6 +7791,7 @@ dependencies = [ "lazy_static", "log", "rand 0.8.5", + "rust-embed", "serde", "serde_json", "smol", @@ -7838,12 +7851,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" @@ -7855,7 +7862,6 @@ name = "vim" version = "0.1.0" dependencies = [ "anyhow", - "assets", "async-compat", "async-trait", "collections", @@ -8007,7 +8013,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-shared", ] @@ -8041,7 +8047,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8683,7 +8689,6 @@ name = "workspace" version = "0.1.0" dependencies = [ "anyhow", - "assets", "async-recursion 1.0.4", "bincode", "call", @@ -8778,12 +8783,11 @@ dependencies = [ [[package]] name = "zed" -version = "0.89.0" +version = "0.91.0" dependencies = [ "activity_indicator", "ai", "anyhow", - "assets", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -8908,7 +8912,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f1362e059df6945d6fa528edbbfa4741a6dc06d9..72a93177a9677e52f7cb0399dbc936fd584f6061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "crates/activity_indicator", "crates/ai", - "crates/assets", "crates/auto_update", "crates/breadcrumbs", "crates/call", @@ -88,6 +87,7 @@ parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } rand = { version = "0.8.5" } regex = { version = "1.5" } +rust-embed = { version = "6.3", features = ["include-exclude"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } @@ -116,3 +116,4 @@ split-debuginfo = "unpacked" [profile.release] debug = true +lto = "thin" diff --git a/Dockerfile b/Dockerfile index d3170696c5fc08c67cbca61a203eced42e4eba0d..2a78d37cbbcadd1bd7afaf612be5767a09abb581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.65-bullseye as builder +FROM rust:1.70-bullseye as builder WORKDIR app COPY . . diff --git a/assets/icons/speech_bubble_12.svg b/assets/icons/speech_bubble_12.svg index f5f330056a34f1261d31416b688bcc86dcdf8bf1..736f39a9840022eb882f8473710e73e8228e50ea 100644 --- a/assets/icons/speech_bubble_12.svg +++ b/assets/icons/speech_bubble_12.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 634aed322abde3333cef56f352a752d03e68005f..25143914cc4828d14e36af9e4b77b5a41cc87d4b 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -16,6 +16,12 @@ "replace_newest": true } ], + "ctrl-cmd-g": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], "ctrl-shift-down": "editor::AddSelectionBelow", "ctrl-shift-up": "editor::AddSelectionAbove", "cmd-shift-backspace": "editor::DeleteToBeginningOfLine", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7e1a8429bfd695a75b423b5e2a175d474ad3d49b..45e85fd04ff616054ac2a7d259c453d3ac92d76a 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -185,20 +185,22 @@ ], "alt-\\": "copilot::Suggest", "alt-]": "copilot::NextSuggestion", - "alt-[": "copilot::PreviousSuggestion" + "alt-[": "copilot::PreviousSuggestion", + "cmd->": "assistant::QuoteSelection" } }, { - "context": "Editor && extension == zmd", + "context": "Editor && mode == auto_height", "bindings": { - "cmd-enter": "ai::Assist" + "alt-enter": "editor::Newline", + "cmd-alt-enter": "editor::NewlineBelow" } }, { - "context": "Editor && mode == auto_height", + "context": "AssistantEditor > Editor", "bindings": { - "alt-enter": "editor::Newline", - "cmd-alt-enter": "editor::NewlineBelow" + "cmd-enter": "assistant::Assist", + "cmd->": "assistant::QuoteSelection" } }, { @@ -250,12 +252,24 @@ "replace_newest": false } ], + "ctrl-cmd-d": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], "cmd-k cmd-d": [ "editor::SelectNext", { "replace_newest": true } ], + "cmd-k ctrl-cmd-d": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], "cmd-k cmd-i": "editor::Hover", "cmd-/": [ "editor::ToggleComments", @@ -504,7 +518,7 @@ "terminal::SendText", "\u0001" ], - // Terminal.app compatability + // Terminal.app compatibility "alt-left": [ "terminal::SendText", "\u001bb" diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 4825d3e8b5fa2163e5a40b446c390f8d18ef96e8..b3e8f989a4a0337a3c1dd9dff63ef640d64dc6ef 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -26,6 +26,12 @@ "replace_newest": false } ], + "ctrl-cmd-g": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], "cmd-/": [ "editor::ToggleComments", { diff --git a/assets/settings/default.json b/assets/settings/default.json index 23599c8dfb1e327d9d842b76fb8c5e2914c23b3f..0430db4644665b1d530706b7a3fca1a5254bf3d4 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -81,6 +81,14 @@ // Default width of the project panel. "default_width": 240 }, + "assistant": { + // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. + "dock": "right", + // Default width when the assistant is docked to the left or right. + "default_width": 450, + // Default height when the assistant is docked to the bottom. + "default_height": 320 + }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. @@ -245,7 +253,7 @@ // copy to the system clipboard. "copy_on_select": false, // Any key-value pairs added to this list will be added to the terminal's - // enviroment. Use `:` to seperate multiple values. + // environment. Use `:` to separate multiple values. "env": { // "KEY": "value1:value2" }, diff --git a/assets/settings/initial_local_settings.json b/assets/settings/initial_local_settings.json new file mode 100644 index 0000000000000000000000000000000000000000..69be683aa81d758e114f02b9bf41d45dcfe32d81 --- /dev/null +++ b/assets/settings/initial_local_settings.json @@ -0,0 +1,11 @@ +// Folder-specific Zed settings +// +// A subset of Zed's settings can be configured on a per-folder basis. +// +// For information on how to configure Zed, see the Zed +// documentation: https://zed.dev/docs/configuring-zed +// +// To see all of Zed's default settings without changing your +// custom settings, run the `open default settings` command +// from the command palette or from `Zed` application menu. +{} diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index dc79fd7911caeacf7510a142c9769aeb68f878c4..2af677da9a3933db7ced27cf6aea9e4f21a1f630 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -1,7 +1,7 @@ -// Zed settings +// Folder-specific settings // -// For information on how to configure Zed, see the Zed -// documentation: https://zed.dev/docs/configuring-zed +// For a full list of overridable settings, and general information on folder-specific settings, see the documentation: +// https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings // // To see all of Zed's default settings without changing your // custom settings, run the `open default settings` command diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index b367a4d43cac845950dc123e66ed0c7be15da1f2..9d67cbd108e79145db2bae2c709ee4d7c0b61660 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -9,17 +9,26 @@ path = "src/ai.rs" doctest = false [dependencies] -assets = { path = "../assets"} collections = { path = "../collections"} editor = { path = "../editor" } +fs = { path = "../fs" } gpui = { path = "../gpui" } +language = { path = "../language" } +menu = { path = "../menu" } +search = { path = "../search" } +settings = { path = "../settings" } +theme = { path = "../theme" } util = { path = "../util" } +workspace = { path = "../workspace" } -serde.workspace = true -serde_json.workspace = true anyhow.workspace = true +chrono = "0.4" futures.workspace = true isahc.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +tiktoken-rs = "0.4" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 2a0110510f8481f5f9a84788ab6d51929b4f2363..40224b3229de1665e3fac89be0d035154e2cf67f 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,22 +1,10 @@ -use anyhow::{anyhow, Result}; -use assets::Assets; -use collections::HashMap; -use editor::Editor; -use futures::AsyncBufReadExt; -use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt}; -use gpui::executor::Background; -use gpui::{actions, AppContext, Task, ViewContext}; -use isahc::prelude::*; -use isahc::{http::StatusCode, Request}; -use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::fs; -use std::rc::Rc; -use std::{io, sync::Arc}; -use util::channel::{ReleaseChannel, RELEASE_CHANNEL}; -use util::{ResultExt, TryFutureExt}; +pub mod assistant; +mod assistant_settings; -actions!(ai, [Assist]); +pub use assistant::AssistantPanel; +use gpui::AppContext; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; // Data types for chat completion requests #[derive(Serialize)] @@ -38,7 +26,7 @@ struct ResponseMessage { content: Option, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] enum Role { User, @@ -46,6 +34,26 @@ enum Role { System, } +impl Role { + pub fn cycle(&mut self) { + *self = match self { + Role::User => Role::Assistant, + Role::Assistant => Role::System, + Role::System => Role::User, + } + } +} + +impl Display for Role { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Role::User => write!(f, "User"), + Role::Assistant => write!(f, "Assistant"), + Role::System => write!(f, "System"), + } + } +} + #[derive(Deserialize, Debug)] struct OpenAIResponseStreamEvent { pub id: Option, @@ -86,228 +94,5 @@ struct OpenAIChoice { } pub fn init(cx: &mut AppContext) { - if *RELEASE_CHANNEL == ReleaseChannel::Stable { - return; - } - - let assistant = Rc::new(Assistant::default()); - cx.add_action({ - let assistant = assistant.clone(); - move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext| { - assistant.assist(editor, cx).log_err(); - } - }); - cx.capture_action({ - let assistant = assistant.clone(); - move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext| { - if !assistant.cancel_last_assist(cx.view_id()) { - cx.propagate_action(); - } - } - }); -} - -type CompletionId = usize; - -#[derive(Default)] -struct Assistant(RefCell); - -#[derive(Default)] -struct AssistantState { - assist_stacks: HashMap>)>>, - next_completion_id: CompletionId, -} - -impl Assistant { - fn assist(self: &Rc, editor: &mut Editor, cx: &mut ViewContext) -> Result<()> { - let api_key = std::env::var("OPENAI_API_KEY")?; - - let selections = editor.selections.all(cx); - let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| { - // Insert markers around selected text as described in the system prompt above. - let snapshot = buffer.snapshot(cx); - let mut user_message = String::new(); - let mut user_message_suffix = String::new(); - let mut buffer_offset = 0; - for selection in selections { - if !selection.is_empty() { - if user_message_suffix.is_empty() { - user_message_suffix.push_str("\n\n"); - } - user_message_suffix.push_str("[Selected excerpt from above]\n"); - user_message_suffix - .extend(snapshot.text_for_range(selection.start..selection.end)); - user_message_suffix.push_str("\n\n"); - } - - user_message.extend(snapshot.text_for_range(buffer_offset..selection.start)); - user_message.push_str("[SELECTION_START]"); - user_message.extend(snapshot.text_for_range(selection.start..selection.end)); - buffer_offset = selection.end; - user_message.push_str("[SELECTION_END]"); - } - if buffer_offset < snapshot.len() { - user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len())); - } - user_message.push_str(&user_message_suffix); - - // Ensure the document ends with 4 trailing newlines. - let trailing_newline_count = snapshot - .reversed_chars_at(snapshot.len()) - .take_while(|c| *c == '\n') - .take(4); - let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count()); - buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx); - - let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing. - let insertion_site = snapshot.anchor_after(snapshot.len() - 2); - - (user_message, insertion_site) - }); - - let this = self.clone(); - let buffer = editor.buffer().clone(); - let executor = cx.background_executor().clone(); - let editor_id = cx.view_id(); - let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id); - let assist_task = cx.spawn(|_, mut cx| { - async move { - // TODO: We should have a get_string method on assets. This is repateated elsewhere. - let content = Assets::get("contexts/system.zmd").unwrap(); - let mut system_message = std::str::from_utf8(content.data.as_ref()) - .unwrap() - .to_string(); - - if let Ok(custom_system_message_path) = - std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH") - { - system_message.push_str( - "\n\nAlso consider the following user-defined system prompt:\n\n", - ); - // TODO: Replace this with our file system trait object. - system_message.push_str( - &cx.background() - .spawn(async move { fs::read_to_string(custom_system_message_path) }) - .await?, - ); - } - - let stream = stream_completion( - api_key, - executor, - OpenAIRequest { - model: "gpt-4".to_string(), - messages: vec![ - RequestMessage { - role: Role::System, - content: system_message.to_string(), - }, - RequestMessage { - role: Role::User, - content: user_message, - }, - ], - stream: false, - }, - ); - - let mut messages = stream.await?; - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - buffer.update(&mut cx, |buffer, cx| { - let text: Arc = choice.delta.content?.into(); - buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx); - Some(()) - }); - } - } - - this.0 - .borrow_mut() - .assist_stacks - .get_mut(&editor_id) - .unwrap() - .retain(|(id, _)| *id != assist_id); - - anyhow::Ok(()) - } - .log_err() - }); - - self.0 - .borrow_mut() - .assist_stacks - .entry(cx.view_id()) - .or_default() - .push((assist_id, assist_task)); - - Ok(()) - } - - fn cancel_last_assist(self: &Rc, editor_id: usize) -> bool { - self.0 - .borrow_mut() - .assist_stacks - .get_mut(&editor_id) - .and_then(|assists| assists.pop()) - .is_some() - } -} - -async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - let json_data = serde_json::to_string(&request)?; - let mut response = Request::post("https://api.openai.com/v1/chat/completions") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body(json_data)? - .send_async() - .await?; - - let status = response.status(); - if status == StatusCode::OK { - executor - .spawn(async move { - let mut lines = BufReader::new(response.body_mut()).lines(); - - fn parse_line( - line: Result, - ) -> Result> { - if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; - Ok(Some(event)) - } else { - Ok(None) - } - } - - while let Some(line) = lines.next().await { - if let Some(event) = parse_line(line).transpose() { - tx.unbounded_send(event).log_err(); - } - } - - anyhow::Ok(()) - }) - .detach(); - - Ok(rx) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )) - } + assistant::init(cx); } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs new file mode 100644 index 0000000000000000000000000000000000000000..77353e1ee497190277218ef747c48a2b2afe3eb4 --- /dev/null +++ b/crates/ai/src/assistant.rs @@ -0,0 +1,1383 @@ +use crate::{ + assistant_settings::{AssistantDockPosition, AssistantSettings}, + OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, +}; +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Local}; +use collections::{HashMap, HashSet}; +use editor::{ + display_map::ToDisplayPoint, + scroll::{ + autoscroll::{Autoscroll, AutoscrollStrategy}, + ScrollAnchor, + }, + Anchor, DisplayPoint, Editor, ExcerptId, ExcerptRange, MultiBuffer, +}; +use fs::Fs; +use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; +use gpui::{ + actions, + elements::*, + executor::Background, + geometry::vector::vec2f, + platform::{CursorStyle, MouseButton}, + Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, + Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, +}; +use isahc::{http::StatusCode, Request, RequestExt}; +use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; +use serde::Deserialize; +use settings::SettingsStore; +use std::{borrow::Cow, cell::RefCell, cmp, fmt::Write, io, rc::Rc, sync::Arc, time::Duration}; +use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + item::Item, + pane, Pane, Workspace, +}; + +const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; + +actions!( + assistant, + [NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey] +); + +pub fn init(cx: &mut AppContext) { + settings::register::(cx); + cx.add_action( + |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { + if let Some(this) = workspace.panel::(cx) { + this.update(cx, |this, cx| this.add_context(cx)) + } + + workspace.focus_panel::(cx); + }, + ); + cx.add_action(AssistantEditor::assist); + cx.capture_action(AssistantEditor::cancel_last_assist); + cx.add_action(AssistantEditor::quote_selection); + cx.capture_action(AssistantEditor::copy); + cx.add_action(AssistantPanel::save_api_key); + cx.add_action(AssistantPanel::reset_api_key); +} + +pub enum AssistantPanelEvent { + ZoomIn, + ZoomOut, + Focus, + Close, + DockPositionChanged, +} + +pub struct AssistantPanel { + width: Option, + height: Option, + pane: ViewHandle, + api_key: Rc>>, + api_key_editor: Option>, + has_read_credentials: bool, + languages: Arc, + fs: Arc, + subscriptions: Vec, +} + +impl AssistantPanel { + pub fn load( + workspace: WeakViewHandle, + cx: AsyncAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // TODO: deserialize state. + workspace.update(&mut cx, |workspace, cx| { + cx.add_view::(|cx| { + let weak_self = cx.weak_handle(); + let pane = cx.add_view(|cx| { + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.project().clone(), + workspace.app_state().background_actions, + Default::default(), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(false, cx); + pane.on_can_drop(move |_, _| false); + pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + let weak_self = weak_self.clone(); + Flex::row() + .with_child(Pane::render_tab_bar_button( + 0, + "icons/plus_12.svg", + false, + Some(("New Context".into(), Some(Box::new(NewContext)))), + cx, + move |_, cx| { + let weak_self = weak_self.clone(); + cx.window_context().defer(move |cx| { + if let Some(this) = weak_self.upgrade(cx) { + this.update(cx, |this, cx| this.add_context(cx)); + } + }) + }, + None, + )) + .with_child(Pane::render_tab_bar_button( + 1, + if pane.is_zoomed() { + "icons/minimize_8.svg" + } else { + "icons/maximize_8.svg" + }, + pane.is_zoomed(), + Some(( + "Toggle Zoom".into(), + Some(Box::new(workspace::ToggleZoom)), + )), + cx, + move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + None, + )) + .into_any() + }); + let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + pane + }); + + let mut this = Self { + pane, + api_key: Rc::new(RefCell::new(None)), + api_key_editor: None, + has_read_credentials: false, + languages: workspace.app_state().languages.clone(), + fs: workspace.app_state().fs.clone(), + width: None, + height: None, + subscriptions: Default::default(), + }; + + let mut old_dock_position = this.position(cx); + this.subscriptions = vec![ + cx.observe(&this.pane, |_, _, cx| cx.notify()), + cx.subscribe(&this.pane, Self::handle_pane_event), + cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(AssistantPanelEvent::DockPositionChanged); + } + }), + ]; + + this + }) + }) + }) + } + + fn handle_pane_event( + &mut self, + _pane: ViewHandle, + event: &pane::Event, + cx: &mut ViewContext, + ) { + match event { + pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn), + pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut), + pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus), + pane::Event::Remove => cx.emit(AssistantPanelEvent::Close), + _ => {} + } + } + + fn add_context(&mut self, cx: &mut ViewContext) { + let focus = self.has_focus(cx); + let editor = cx + .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx)); + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_assistant_editor_event)); + self.pane.update(cx, |pane, cx| { + pane.add_item(Box::new(editor), true, focus, None, cx) + }); + } + + fn handle_assistant_editor_event( + &mut self, + _: ViewHandle, + event: &AssistantEditorEvent, + cx: &mut ViewContext, + ) { + match event { + AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()), + } + } + + fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if let Some(api_key) = self + .api_key_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + { + if !api_key.is_empty() { + cx.platform() + .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) + .log_err(); + *self.api_key.borrow_mut() = Some(api_key); + self.api_key_editor.take(); + cx.focus_self(); + cx.notify(); + } + } else { + cx.propagate_action(); + } + } + + fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext) { + cx.platform().delete_credentials(OPENAI_API_URL).log_err(); + self.api_key.take(); + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.focus_self(); + cx.notify(); + } +} + +fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { + cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), + cx, + ); + editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); + editor + }) +} + +impl Entity for AssistantPanel { + type Event = AssistantPanelEvent; +} + +impl View for AssistantPanel { + fn ui_name() -> &'static str { + "AssistantPanel" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let style = &theme::current(cx).assistant; + if let Some(api_key_editor) = self.api_key_editor.as_ref() { + Flex::column() + .with_child( + Text::new( + "Paste your OpenAI API key and press Enter to use the assistant", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) + .with_child( + ChildView::new(api_key_editor, cx) + .contained() + .with_style(style.api_key_editor.container) + .aligned(), + ) + .contained() + .with_style(style.api_key_prompt.container) + .aligned() + .into_any() + } else { + ChildView::new(&self.pane, cx).into_any() + } + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + if let Some(api_key_editor) = self.api_key_editor.as_ref() { + cx.focus(api_key_editor); + } else { + cx.focus(&self.pane); + } + } + } +} + +impl Panel for AssistantPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match settings::get::(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, _: DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::(self.fs.clone(), cx, move |settings| { + let dock = match position { + DockPosition::Left => AssistantDockPosition::Left, + DockPosition::Bottom => AssistantDockPosition::Bottom, + DockPosition::Right => AssistantDockPosition::Right, + }; + settings.dock = Some(dock); + }); + } + + fn size(&self, cx: &WindowContext) -> f32 { + let settings = settings::get::(cx); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or_else(|| settings.default_width) + } + DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), + } + } + + fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = Some(size), + DockPosition::Bottom => self.height = Some(size), + } + cx.notify(); + } + + fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool { + matches!(event, AssistantPanelEvent::ZoomIn) + } + + fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool { + matches!(event, AssistantPanelEvent::ZoomOut) + } + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).is_zoomed() + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + if active { + if self.api_key.borrow().is_none() && !self.has_read_credentials { + self.has_read_credentials = true; + let api_key = if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + }; + if let Some(api_key) = api_key { + *self.api_key.borrow_mut() = Some(api_key); + } else if self.api_key_editor.is_none() { + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.notify(); + } + } + + if self.pane.read(cx).items_len() == 0 { + self.add_context(cx); + } + } + } + + fn icon_path(&self) -> &'static str { + "icons/speech_bubble_12.svg" + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Assistant Panel".into(), Some(Box::new(ToggleFocus))) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, AssistantPanelEvent::DockPositionChanged) + } + + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + + fn should_close_on_event(event: &AssistantPanelEvent) -> bool { + matches!(event, AssistantPanelEvent::Close) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).has_focus() + || self + .api_key_editor + .as_ref() + .map_or(false, |editor| editor.is_focused(cx)) + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, AssistantPanelEvent::Focus) + } +} + +enum AssistantEvent { + MessagesEdited { ids: Vec }, + SummaryChanged, + StreamedCompletion, +} + +struct Assistant { + buffer: ModelHandle, + messages: Vec, + messages_metadata: HashMap, + summary: Option, + pending_summary: Task>, + completion_count: usize, + pending_completions: Vec, + languages: Arc, + model: String, + token_count: Option, + max_token_count: usize, + pending_token_count: Task>, + api_key: Rc>>, + _subscriptions: Vec, +} + +impl Entity for Assistant { + type Event = AssistantEvent; +} + +impl Assistant { + fn new( + api_key: Rc>>, + language_registry: Arc, + cx: &mut ModelContext, + ) -> Self { + let model = "gpt-3.5-turbo"; + let buffer = cx.add_model(|_| MultiBuffer::new(0)); + let mut this = Self { + messages: Default::default(), + messages_metadata: Default::default(), + summary: None, + pending_summary: Task::ready(None), + completion_count: Default::default(), + pending_completions: Default::default(), + languages: language_registry, + token_count: None, + max_token_count: tiktoken_rs::model::get_context_size(model), + pending_token_count: Task::ready(None), + model: model.into(), + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + api_key, + buffer, + }; + this.insert_message_after(ExcerptId::max(), Role::User, cx); + this.count_remaining_tokens(cx); + this + } + + fn handle_buffer_event( + &mut self, + _: ModelHandle, + event: &editor::multi_buffer::Event, + cx: &mut ModelContext, + ) { + match event { + editor::multi_buffer::Event::ExcerptsAdded { .. } + | editor::multi_buffer::Event::ExcerptsRemoved { .. } + | editor::multi_buffer::Event::Edited => self.count_remaining_tokens(cx), + editor::multi_buffer::Event::ExcerptsEdited { ids } => { + cx.emit(AssistantEvent::MessagesEdited { ids: ids.clone() }); + } + _ => {} + } + } + + fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { + let messages = self + .messages + .iter() + .filter_map(|message| { + Some(tiktoken_rs::ChatCompletionRequestMessage { + role: match self.messages_metadata.get(&message.excerpt_id)?.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: message.content.read(cx).text(), + name: None, + }) + }) + .collect::>(); + let model = self.model.clone(); + self.pending_token_count = cx.spawn_weak(|this, mut cx| { + async move { + cx.background().timer(Duration::from_millis(200)).await; + let token_count = cx + .background() + .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) }) + .await?; + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))? + .update(&mut cx, |this, cx| { + this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); + this.token_count = Some(token_count); + cx.notify() + }); + anyhow::Ok(()) + } + .log_err() + }); + } + + fn remaining_tokens(&self) -> Option { + Some(self.max_token_count as isize - self.token_count? as isize) + } + + fn set_model(&mut self, model: String, cx: &mut ModelContext) { + self.model = model; + self.count_remaining_tokens(cx); + cx.notify(); + } + + fn assist(&mut self, cx: &mut ModelContext) -> Option<(Message, Message)> { + let messages = self + .messages + .iter() + .filter_map(|message| { + Some(RequestMessage { + role: self.messages_metadata.get(&message.excerpt_id)?.role, + content: message.content.read(cx).text(), + }) + }) + .collect(); + let request = OpenAIRequest { + model: self.model.clone(), + messages, + stream: true, + }; + + let api_key = self.api_key.borrow().clone()?; + let stream = stream_completion(api_key, cx.background().clone(), request); + let assistant_message = self.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + let user_message = self.insert_message_after(ExcerptId::max(), Role::User, cx); + let task = cx.spawn_weak({ + let assistant_message = assistant_message.clone(); + |this, mut cx| async move { + let assistant_message = assistant_message; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + assistant_message.content.update(&mut cx, |content, cx| { + let text: Arc = choice.delta.content?.into(); + content.edit([(content.len()..content.len(), text)], None, cx); + Some(()) + }); + this.upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))? + .update(&mut cx, |_, cx| { + cx.emit(AssistantEvent::StreamedCompletion); + }); + } + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Err(error) = result { + if let Some(metadata) = this + .messages_metadata + .get_mut(&assistant_message.excerpt_id) + { + metadata.error = Some(error.to_string().trim().into()); + cx.notify(); + } + } + }); + } + } + }); + + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + _task: task, + }); + Some((assistant_message, user_message)) + } + + fn cancel_last_assist(&mut self) -> bool { + self.pending_completions.pop().is_some() + } + + fn remove_empty_messages<'a>( + &mut self, + excerpts: HashSet, + protected_offsets: HashSet, + cx: &mut ModelContext, + ) { + let mut offset = 0; + let mut excerpts_to_remove = Vec::new(); + self.messages.retain(|message| { + let range = offset..offset + message.content.read(cx).len(); + offset = range.end + 1; + if range.is_empty() + && !protected_offsets.contains(&range.start) + && excerpts.contains(&message.excerpt_id) + { + excerpts_to_remove.push(message.excerpt_id); + self.messages_metadata.remove(&message.excerpt_id); + false + } else { + true + } + }); + + if !excerpts_to_remove.is_empty() { + self.buffer.update(cx, |buffer, cx| { + buffer.remove_excerpts(excerpts_to_remove, cx) + }); + cx.notify(); + } + } + + fn cycle_message_role(&mut self, excerpt_id: ExcerptId, cx: &mut ModelContext) { + if let Some(metadata) = self.messages_metadata.get_mut(&excerpt_id) { + metadata.role.cycle(); + cx.notify(); + } + } + + fn insert_message_after( + &mut self, + excerpt_id: ExcerptId, + role: Role, + cx: &mut ModelContext, + ) -> Message { + let content = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + let markdown = self.languages.language_for_name("Markdown"); + cx.spawn_weak(|buffer, mut cx| async move { + let markdown = markdown.await?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + buffer.set_language_registry(self.languages.clone()); + buffer + }); + let new_excerpt_id = self.buffer.update(cx, |buffer, cx| { + buffer + .insert_excerpts_after( + excerpt_id, + content.clone(), + vec![ExcerptRange { + context: 0..0, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + let ix = self + .messages + .iter() + .position(|message| message.excerpt_id == excerpt_id) + .map_or(self.messages.len(), |ix| ix + 1); + let message = Message { + excerpt_id: new_excerpt_id, + content: content.clone(), + }; + self.messages.insert(ix, message.clone()); + self.messages_metadata.insert( + new_excerpt_id, + MessageMetadata { + role, + sent_at: Local::now(), + error: None, + }, + ); + message + } + + fn summarize(&mut self, cx: &mut ModelContext) { + if self.messages.len() >= 2 && self.summary.is_none() { + let api_key = self.api_key.borrow().clone(); + if let Some(api_key) = api_key { + let messages = self + .messages + .iter() + .take(2) + .filter_map(|message| { + Some(RequestMessage { + role: self.messages_metadata.get(&message.excerpt_id)?.role, + content: message.content.read(cx).text(), + }) + }) + .chain(Some(RequestMessage { + role: Role::User, + content: + "Summarize the conversation into a short title without punctuation" + .into(), + })) + .collect(); + let request = OpenAIRequest { + model: self.model.clone(), + messages, + stream: true, + }; + + let stream = stream_completion(api_key, cx.background().clone(), request); + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + let text = choice.delta.content.unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.summary.get_or_insert(String::new()).push_str(&text); + cx.emit(AssistantEvent::SummaryChanged); + }); + } + } + + anyhow::Ok(()) + } + .log_err() + }); + } + } + } +} + +struct PendingCompletion { + id: usize, + _task: Task<()>, +} + +enum AssistantEditorEvent { + TabContentChanged, +} + +struct AssistantEditor { + assistant: ModelHandle, + editor: ViewHandle, + scroll_bottom: ScrollAnchor, + _subscriptions: Vec, +} + +impl AssistantEditor { + fn new( + api_key: Rc>>, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx)); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx); + editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_show_gutter(false, cx); + editor.set_render_excerpt_header( + { + let assistant = assistant.clone(); + move |_editor, params: editor::RenderExcerptHeaderParams, cx| { + enum Sender {} + enum ErrorTooltip {} + + let theme = theme::current(cx); + let style = &theme.assistant; + let excerpt_id = params.id; + if let Some(metadata) = assistant + .read(cx) + .messages_metadata + .get(&excerpt_id) + .cloned() + { + let sender = MouseEventHandler::::new( + params.id.into(), + cx, + |state, _| match metadata.role { + Role::User => { + let style = style.user_sender.style_for(state, false); + Label::new("You", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::Assistant => { + let style = style.assistant_sender.style_for(state, false); + Label::new("Assistant", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::System => { + let style = style.system_sender.style_for(state, false); + Label::new("System", style.text.clone()) + .contained() + .with_style(style.container) + } + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, { + let assistant = assistant.clone(); + move |_, _, cx| { + assistant.update(cx, |assistant, cx| { + assistant.cycle_message_role(excerpt_id, cx) + }) + } + }); + + Flex::row() + .with_child(sender.aligned()) + .with_child( + Label::new( + metadata.sent_at.format("%I:%M%P").to_string(), + style.sent_at.text.clone(), + ) + .contained() + .with_style(style.sent_at.container) + .aligned(), + ) + .with_children(metadata.error.map(|error| { + Svg::new("icons/circle_x_mark_12.svg") + .with_color(style.error_icon.color) + .constrained() + .with_width(style.error_icon.width) + .contained() + .with_style(style.error_icon.container) + .with_tooltip::( + params.id.into(), + error, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + })) + .aligned() + .left() + .contained() + .with_style(style.header) + .into_any() + } else { + Empty::new().into_any() + } + } + }, + cx, + ); + editor + }); + + let _subscriptions = vec![ + cx.observe(&assistant, |_, _, cx| cx.notify()), + cx.subscribe(&assistant, Self::handle_assistant_event), + cx.subscribe(&editor, Self::handle_editor_event), + ]; + + Self { + assistant, + editor, + scroll_bottom: ScrollAnchor { + offset: Default::default(), + anchor: Anchor::max(), + }, + _subscriptions, + } + } + + fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + let user_message = self.assistant.update(cx, |assistant, cx| { + let editor = self.editor.read(cx); + let newest_selection = editor.selections.newest_anchor(); + let excerpt_id = if newest_selection.head() == Anchor::min() { + assistant + .messages + .first() + .map(|message| message.excerpt_id)? + } else if newest_selection.head() == Anchor::max() { + assistant + .messages + .last() + .map(|message| message.excerpt_id)? + } else { + newest_selection.head().excerpt_id() + }; + + let metadata = assistant.messages_metadata.get(&excerpt_id)?; + let user_message = if metadata.role == Role::User { + let (_, user_message) = assistant.assist(cx)?; + user_message + } else { + let user_message = assistant.insert_message_after(excerpt_id, Role::User, cx); + user_message + }; + Some(user_message) + }); + + if let Some(user_message) = user_message { + self.editor.update(cx, |editor, cx| { + let cursor = editor + .buffer() + .read(cx) + .snapshot(cx) + .anchor_in_excerpt(user_message.excerpt_id, language::Anchor::MIN); + editor.change_selections( + Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), + cx, + |selections| selections.select_anchor_ranges([cursor..cursor]), + ); + }); + self.update_scroll_bottom(cx); + } + } + + fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if !self + .assistant + .update(cx, |assistant, _| assistant.cancel_last_assist()) + { + cx.propagate_action(); + } + } + + fn handle_assistant_event( + &mut self, + _: ModelHandle, + event: &AssistantEvent, + cx: &mut ViewContext, + ) { + match event { + AssistantEvent::MessagesEdited { ids } => { + let selections = self.editor.read(cx).selections.all::(cx); + let selection_heads = selections + .iter() + .map(|selection| selection.head()) + .collect::>(); + let ids = ids.iter().copied().collect::>(); + self.assistant.update(cx, |assistant, cx| { + assistant.remove_empty_messages(ids, selection_heads, cx) + }); + } + AssistantEvent::SummaryChanged => { + cx.emit(AssistantEditorEvent::TabContentChanged); + } + AssistantEvent::StreamedCompletion => { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let scroll_bottom_row = self + .scroll_bottom + .anchor + .to_display_point(&snapshot.display_snapshot) + .row(); + + let scroll_bottom = scroll_bottom_row as f32 + self.scroll_bottom.offset.y(); + let visible_line_count = editor.visible_line_count().unwrap_or(0.); + let scroll_top = scroll_bottom - visible_line_count; + editor + .set_scroll_position(vec2f(self.scroll_bottom.offset.x(), scroll_top), cx); + }); + } + } + } + + fn handle_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::ScrollPositionChanged { .. } => self.update_scroll_bottom(cx), + _ => {} + } + } + + fn update_scroll_bottom(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let scroll_position = editor + .scroll_manager + .anchor() + .scroll_position(&snapshot.display_snapshot); + let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); + let scroll_bottom_point = cmp::min( + DisplayPoint::new(scroll_bottom.floor() as u32, 0), + snapshot.display_snapshot.max_point(), + ); + let scroll_bottom_anchor = snapshot + .buffer_snapshot + .anchor_after(scroll_bottom_point.to_point(&snapshot.display_snapshot)); + let scroll_bottom_offset = vec2f( + scroll_position.x(), + scroll_bottom - scroll_bottom_point.row() as f32, + ); + self.scroll_bottom = ScrollAnchor { + anchor: scroll_bottom_anchor, + offset: scroll_bottom_offset, + }; + }); + } + + fn quote_selection( + workspace: &mut Workspace, + _: &QuoteSelection, + cx: &mut ViewContext, + ) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::()) else { + return; + }; + + let text = editor.read_with(cx, |editor, cx| { + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + + let selected_text = buffer.text_for_range(range).collect::(); + if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + format!("```{language_name}\n{selected_text}\n```") + }) + } + }); + + // Activate the panel + if !panel.read(cx).has_focus(cx) { + workspace.toggle_panel_focus::(cx); + } + + if let Some(text) = text { + panel.update(cx, |panel, cx| { + if let Some(assistant) = panel + .pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .ok_or_else(|| anyhow!("no active context")) + .log_err() + { + assistant.update(cx, |assistant, cx| { + assistant + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); + } + }); + } + } + + fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { + let editor = self.editor.read(cx); + let assistant = self.assistant.read(cx); + if editor.selections.count() == 1 { + let selection = editor.selections.newest::(cx); + let mut offset = 0; + let mut copied_text = String::new(); + let mut spanned_messages = 0; + for message in &assistant.messages { + let message_range = offset..offset + message.content.read(cx).len() + 1; + + if message_range.start >= selection.range().end { + break; + } else if message_range.end >= selection.range().start { + let range = cmp::max(message_range.start, selection.range().start) + ..cmp::min(message_range.end, selection.range().end); + if !range.is_empty() { + if let Some(metadata) = assistant.messages_metadata.get(&message.excerpt_id) + { + spanned_messages += 1; + write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap(); + for chunk in + assistant.buffer.read(cx).snapshot(cx).text_for_range(range) + { + copied_text.push_str(&chunk); + } + copied_text.push('\n'); + } + } + } + + offset = message_range.end; + } + + if spanned_messages > 1 { + cx.platform() + .write_to_clipboard(ClipboardItem::new(copied_text)); + return; + } + } + + cx.propagate_action(); + } + + fn cycle_model(&mut self, cx: &mut ViewContext) { + self.assistant.update(cx, |assistant, cx| { + let new_model = match assistant.model.as_str() { + "gpt-4" => "gpt-3.5-turbo", + _ => "gpt-4", + }; + assistant.set_model(new_model.into(), cx); + }); + } + + fn title(&self, cx: &AppContext) -> String { + self.assistant + .read(cx) + .summary + .clone() + .unwrap_or_else(|| "New Context".into()) + } +} + +impl Entity for AssistantEditor { + type Event = AssistantEditorEvent; +} + +impl View for AssistantEditor { + fn ui_name() -> &'static str { + "AssistantEditor" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum Model {} + let theme = &theme::current(cx).assistant; + let assistant = &self.assistant.read(cx); + let model = assistant.model.clone(); + let remaining_tokens = assistant.remaining_tokens().map(|remaining_tokens| { + let remaining_tokens_style = if remaining_tokens <= 0 { + &theme.no_remaining_tokens + } else { + &theme.remaining_tokens + }; + Label::new( + remaining_tokens.to_string(), + remaining_tokens_style.text.clone(), + ) + .contained() + .with_style(remaining_tokens_style.container) + }); + + Stack::new() + .with_child( + ChildView::new(&self.editor, cx) + .contained() + .with_style(theme.container), + ) + .with_child( + Flex::row() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.model.style_for(state, false); + Label::new(model, style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)), + ) + .with_children(remaining_tokens) + .contained() + .with_style(theme.model_info_container) + .aligned() + .top() + .right(), + ) + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.editor); + } + } +} + +impl Item for AssistantEditor { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN); + Label::new(title, style.label.clone()).into_any() + } + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + Some(self.title(cx).into()) + } + + fn as_searchable( + &self, + _: &ViewHandle, + ) -> Option> { + Some(Box::new(self.editor.clone())) + } +} + +#[derive(Clone, Debug)] +struct Message { + excerpt_id: ExcerptId, + content: ModelHandle, +} + +#[derive(Clone, Debug)] +struct MessageMetadata { + role: Role, + sent_at: DateTime, + error: Option, +} + +async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + let json_data = serde_json::to_string(&request)?; + let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_data)? + .send_async() + .await?; + + let status = response.status(); + if status == StatusCode::OK { + executor + .spawn(async move { + let mut lines = BufReader::new(response.body_mut()).lines(); + + fn parse_line( + line: Result, + ) -> Result> { + if let Some(data) = line?.strip_prefix("data: ") { + let event = serde_json::from_str(&data)?; + Ok(Some(event)) + } else { + Ok(None) + } + } + + while let Some(line) = lines.next().await { + if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); + if tx.unbounded_send(event).is_err() { + break; + } + + if done { + break; + } + } + } + + anyhow::Ok(()) + }) + .detach(); + + Ok(rx) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + #[derive(Deserialize)] + struct OpenAIResponse { + error: OpenAIError, + } + + #[derive(Deserialize)] + struct OpenAIError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::AppContext; + + #[gpui::test] + fn test_inserting_and_removing_messages(cx: &mut AppContext) { + let registry = Arc::new(LanguageRegistry::test()); + + cx.add_model(|cx| { + let mut assistant = Assistant::new(Default::default(), registry, cx); + let message_1 = assistant.messages[0].clone(); + let message_2 = assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + let message_3 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx); + let message_4 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx); + assistant.remove_empty_messages( + HashSet::from_iter([message_3.excerpt_id, message_4.excerpt_id]), + Default::default(), + cx, + ); + assert_eq!(assistant.messages.len(), 2); + assert_eq!(assistant.messages[0].excerpt_id, message_1.excerpt_id); + assert_eq!(assistant.messages[1].excerpt_id, message_2.excerpt_id); + assistant + }); + } +} diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..eb92e0f6e8c0bdd6e554844f2565057ed92e9ebd --- /dev/null +++ b/crates/ai/src/assistant_settings.rs @@ -0,0 +1,40 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AssistantDockPosition { + Left, + Right, + Bottom, +} + +#[derive(Deserialize, Debug)] +pub struct AssistantSettings { + pub dock: AssistantDockPosition, + pub default_width: f32, + pub default_height: f32, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct AssistantSettingsContent { + pub dock: Option, + pub default_width: Option, + pub default_height: Option, +} + +impl Setting for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant"); + + type FileContent = AssistantSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml deleted file mode 100644 index eec60ff84c54264856b0e374066258cc0293e682..0000000000000000000000000000000000000000 --- a/crates/assets/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "assets" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/assets.rs" -doctest = false - -[dependencies] -gpui = { path = "../gpui" } -anyhow.workspace = true -rust-embed = { version = "6.3", features = ["include-exclude"] } diff --git a/crates/assets/build.rs b/crates/assets/build.rs deleted file mode 100644 index 8500b2462240f944f7d64f129a2f560f7c7f4268..0000000000000000000000000000000000000000 --- a/crates/assets/build.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::process::Command; - -fn main() { - let output = Command::new("npm") - .current_dir("../../styles") - .args(["install", "--no-save"]) - .output() - .expect("failed to run npm"); - if !output.status.success() { - panic!( - "failed to install theme dependencies {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let output = Command::new("npm") - .current_dir("../../styles") - .args(["run", "build"]) - .output() - .expect("failed to run npm"); - if !output.status.success() { - panic!( - "build script failed {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - println!("cargo:rerun-if-changed=../../styles/src"); -} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index feebbff61b93043b88ed2eaa0603018eb191fd69..bdf677512c4b0326cf2bc0e513a00bbb563f0a02 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -159,10 +159,7 @@ impl Bundle { fn path(&self) -> &Path { match self { Self::App { app_bundle, .. } => app_bundle, - Self::LocalPath { - executable: excutable, - .. - } => excutable, + Self::LocalPath { executable, .. } => executable, } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c9b83d805a4a87a65eda95f5b765c35841627d30..78bcc55e93ffe57dcec4ce760978ab2d22a321e8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -776,15 +776,6 @@ impl Client { if credentials.is_none() && try_keychain { credentials = read_credentials_from_keychain(cx); read_from_keychain = credentials.is_some(); - if read_from_keychain { - cx.read(|cx| { - self.telemetry().report_mixpanel_event( - "read credentials from keychain", - Default::default(), - *settings::get::(cx), - ); - }); - } } if credentials.is_none() { let mut status_rx = self.status(); @@ -1072,11 +1063,8 @@ impl Client { ) -> Task> { let platform = cx.platform(); let executor = cx.background(); - let telemetry = self.telemetry.clone(); let http = self.http.clone(); - let telemetry_settings = cx.read(|cx| *settings::get::(cx)); - executor.clone().spawn(async move { // Generate a pair of asymmetric encryption keys. The public key will be used by the // zed server to encrypt the user's access token, so that it can'be intercepted by @@ -1159,12 +1147,6 @@ impl Client { .context("failed to decrypt access token")?; platform.activate(true); - telemetry.report_mixpanel_event( - "authenticate with browser", - Default::default(), - telemetry_settings, - ); - Ok(Credentials { user_id: user_id.parse()?, access_token, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index cf47700dfe26936d44676c208fee941e9e4d9565..c1d58222461e58b8ce48d7853818bc68acb35b46 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,14 +1,9 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use db::kvp::KEY_VALUE_STORE; -use gpui::{ - executor::Background, - serde_json::{self, value::Map, Value}, - AppContext, Task, -}; +use gpui::{executor::Background, serde_json, AppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; -use serde_json::json; use std::{ env, io::Write, @@ -19,7 +14,7 @@ use std::{ }; use tempfile::NamedTempFile; use util::http::HttpClient; -use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt}; +use util::{channel::ReleaseChannel, TryFutureExt}; use uuid::Uuid; pub struct Telemetry { @@ -37,23 +32,15 @@ struct TelemetryState { os_name: &'static str, os_version: Option>, architecture: &'static str, - mixpanel_events_queue: Vec, clickhouse_events_queue: Vec, - next_mixpanel_event_id: usize, - flush_mixpanel_events_task: Option>, flush_clickhouse_events_task: Option>, log_file: Option, is_staff: Option, } -const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track"; -const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set"; const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; lazy_static! { - static ref MIXPANEL_TOKEN: Option = std::env::var("ZED_MIXPANEL_TOKEN") - .ok() - .or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string())); static ref CLICKHOUSE_EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH); } @@ -95,47 +82,6 @@ pub enum ClickhouseEvent { }, } -#[derive(Serialize, Debug)] -struct MixpanelEvent { - event: String, - properties: MixpanelEventProperties, -} - -#[derive(Serialize, Debug)] -struct MixpanelEventProperties { - // Mixpanel required fields - #[serde(skip_serializing_if = "str::is_empty")] - token: &'static str, - time: u128, - #[serde(rename = "distinct_id")] - installation_id: Option>, - #[serde(rename = "$insert_id")] - insert_id: usize, - // Custom fields - #[serde(skip_serializing_if = "Option::is_none", flatten)] - event_properties: Option>, - #[serde(rename = "OS Name")] - os_name: &'static str, - #[serde(rename = "OS Version")] - os_version: Option>, - #[serde(rename = "Release Channel")] - release_channel: Option<&'static str>, - #[serde(rename = "App Version")] - app_version: Option>, - #[serde(rename = "Signed In")] - signed_in: bool, -} - -#[derive(Serialize)] -struct MixpanelEngageRequest { - #[serde(rename = "$token")] - token: &'static str, - #[serde(rename = "$distinct_id")] - installation_id: Arc, - #[serde(rename = "$set")] - set: Value, -} - #[cfg(debug_assertions)] const MAX_QUEUE_LEN: usize = 1; @@ -168,29 +114,13 @@ impl Telemetry { release_channel, installation_id: None, metrics_id: None, - mixpanel_events_queue: Default::default(), clickhouse_events_queue: Default::default(), - flush_mixpanel_events_task: Default::default(), flush_clickhouse_events_task: Default::default(), - next_mixpanel_event_id: 0, log_file: None, is_staff: None, }), }); - if MIXPANEL_TOKEN.is_some() { - this.executor - .spawn({ - let this = this.clone(); - async move { - if let Some(tempfile) = NamedTempFile::new().log_err() { - this.state.lock().log_file = Some(tempfile); - } - } - }) - .detach(); - } - this } @@ -218,20 +148,9 @@ impl Telemetry { let mut state = this.state.lock(); state.installation_id = Some(installation_id.clone()); - for event in &mut state.mixpanel_events_queue { - event - .properties - .installation_id - .get_or_insert_with(|| installation_id.clone()); - } - - let has_mixpanel_events = !state.mixpanel_events_queue.is_empty(); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); - drop(state); - if has_mixpanel_events { - this.flush_mixpanel_events(); - } + drop(state); if has_clickhouse_events { this.flush_clickhouse_events(); @@ -256,37 +175,11 @@ impl Telemetry { return; } - let this = self.clone(); let mut state = self.state.lock(); - let installation_id = state.installation_id.clone(); let metrics_id: Option> = metrics_id.map(|id| id.into()); state.metrics_id = metrics_id.clone(); state.is_staff = Some(is_staff); drop(state); - - if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) { - self.executor - .spawn( - async move { - let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest { - token, - installation_id, - set: json!({ - "Staff": is_staff, - "ID": metrics_id, - "App": true - }), - }])?; - - this.http_client - .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into()) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } } pub fn report_clickhouse_event( @@ -310,7 +203,7 @@ impl Telemetry { }); if state.installation_id.is_some() { - if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { + if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { @@ -324,55 +217,6 @@ impl Telemetry { } } - pub fn report_mixpanel_event( - self: &Arc, - kind: &str, - properties: Value, - telemetry_settings: TelemetrySettings, - ) { - if !telemetry_settings.metrics { - return; - } - - let mut state = self.state.lock(); - let event = MixpanelEvent { - event: kind.into(), - properties: MixpanelEventProperties { - token: "", - time: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - installation_id: state.installation_id.clone(), - insert_id: post_inc(&mut state.next_mixpanel_event_id), - event_properties: if let Value::Object(properties) = properties { - Some(properties) - } else { - None - }, - os_name: state.os_name, - os_version: state.os_version.clone(), - release_channel: state.release_channel, - app_version: state.app_version.clone(), - signed_in: state.metrics_id.is_some(), - }, - }; - state.mixpanel_events_queue.push(event); - if state.installation_id.is_some() { - if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { - drop(state); - self.flush_mixpanel_events(); - } else { - let this = self.clone(); - let executor = self.executor.clone(); - state.flush_mixpanel_events_task = Some(self.executor.spawn(async move { - executor.timer(DEBOUNCE_INTERVAL).await; - this.flush_mixpanel_events(); - })); - } - } - } - pub fn metrics_id(self: &Arc) -> Option> { self.state.lock().metrics_id.clone() } @@ -385,44 +229,6 @@ impl Telemetry { self.state.lock().is_staff } - fn flush_mixpanel_events(self: &Arc) { - let mut state = self.state.lock(); - let mut events = mem::take(&mut state.mixpanel_events_queue); - state.flush_mixpanel_events_task.take(); - drop(state); - - if let Some(token) = MIXPANEL_TOKEN.as_ref() { - let this = self.clone(); - self.executor - .spawn( - async move { - let mut json_bytes = Vec::new(); - - if let Some(file) = &mut this.state.lock().log_file { - let file = file.as_file_mut(); - for event in &mut events { - json_bytes.clear(); - serde_json::to_writer(&mut json_bytes, event)?; - file.write_all(&json_bytes)?; - file.write(b"\n")?; - - event.properties.token = token; - } - } - - json_bytes.clear(); - serde_json::to_writer(&mut json_bytes, &events)?; - this.http_client - .post_json(MIXPANEL_EVENTS_URL, json_bytes.into()) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } - } - fn flush_clickhouse_events(self: &Arc) { let mut state = self.state.lock(); let mut events = mem::take(&mut state.clickhouse_events_queue); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index cd06b9a70a253eb9668d2704880638c1eeabaaba..7058d04178a843b0a6b4275cc00a21dc38b3fc92 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.12.4" +version = "0.13.1" publish = false [[bin]] diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index c12afafaf31db451f9cd2b4c58f1a692e43446a8..595d841d075773699d88e4556354f4ef43b2b410 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -97,6 +97,17 @@ CREATE TABLE "worktree_repositories" ( CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); +CREATE TABLE "worktree_settings_files" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INTEGER NOT NULL, + "path" VARCHAR NOT NULL, + "content" TEXT, + PRIMARY KEY(project_id, worktree_id, path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); +CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id"); + CREATE TABLE "worktree_diagnostic_summaries" ( "project_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL, diff --git a/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql b/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql new file mode 100644 index 0000000000000000000000000000000000000000..973a40af0f21908e5dbe0d5a30373629f24b7f1e --- /dev/null +++ b/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql @@ -0,0 +1,10 @@ +CREATE TABLE "worktree_settings_files" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INT8 NOT NULL, + "path" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + PRIMARY KEY(project_id, worktree_id, path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); +CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 3456bea44eef7c53c2db5b11cb44bdab5ac92163..7e2c376bc2c9eef6b030fdc7b2b4017d157f9216 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -16,6 +16,7 @@ mod worktree_diagnostic_summary; mod worktree_entry; mod worktree_repository; mod worktree_repository_statuses; +mod worktree_settings_file; use crate::executor::Executor; use crate::{Error, Result}; @@ -1494,6 +1495,7 @@ impl Database { updated_repositories: Default::default(), removed_repositories: Default::default(), diagnostic_summaries: Default::default(), + settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, }; @@ -1591,6 +1593,25 @@ impl Database { }) .collect::>(); + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees + .iter_mut() + .find(|w| w.id == db_settings_file.worktree_id as u64) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + let mut collaborators = project .find_related(project_collaborator::Entity) .all(&*tx) @@ -2530,6 +2551,58 @@ impl Database { .await } + pub async fn update_worktree_settings( + &self, + update: &proto::UpdateWorktreeSettings, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + if let Some(content) = &update.content { + worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + content: ActiveValue::Set(content.clone()), + }) + .on_conflict( + OnConflict::columns([ + worktree_settings_file::Column::ProjectId, + worktree_settings_file::Column::WorktreeId, + worktree_settings_file::Column::Path, + ]) + .update_column(worktree_settings_file::Column::Content) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + pub async fn join_project( &self, project_id: ProjectId, @@ -2600,6 +2673,7 @@ impl Database { entries: Default::default(), repository_entries: Default::default(), diagnostic_summaries: Default::default(), + settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, }, @@ -2684,6 +2758,25 @@ impl Database { } } + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = + worktrees.get_mut(&(db_settings_file.worktree_id as u64)) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + // Populate language servers. let language_servers = project .find_related(language_server::Entity) @@ -3347,6 +3440,7 @@ pub struct RejoinedWorktree { pub updated_repositories: Vec, pub removed_repositories: Vec, pub diagnostic_summaries: Vec, + pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, } @@ -3402,10 +3496,17 @@ pub struct Worktree { pub entries: Vec, pub repository_entries: BTreeMap, pub diagnostic_summaries: Vec, + pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, } +#[derive(Debug)] +pub struct WorktreeSettingsFile { + pub path: String, + pub content: String, +} + #[cfg(test)] pub use test::*; diff --git a/crates/collab/src/db/worktree_settings_file.rs b/crates/collab/src/db/worktree_settings_file.rs new file mode 100644 index 0000000000000000000000000000000000000000..f8e87f6e599cbaaeb698bdb28cb1316c7097dc77 --- /dev/null +++ b/crates/collab/src/db/worktree_settings_file.rs @@ -0,0 +1,19 @@ +use super::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "worktree_settings_files")] +pub struct Model { + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub worktree_id: i64, + #[sea_orm(primary_key)] + pub path: String, + pub content: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4c117b613da90ce6ac54c792833aa94364715758..8d210513c2d3dc32fdac676b4953147cc4f0208e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -200,6 +200,7 @@ impl Server { .add_message_handler(start_language_server) .add_message_handler(update_language_server) .add_message_handler(update_diagnostic_summary) + .add_message_handler(update_worktree_settings) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) @@ -1088,6 +1089,18 @@ async fn rejoin_room( }, )?; } + + for settings_file in worktree.settings_files { + session.peer.send( + session.connection_id, + proto::UpdateWorktreeSettings { + project_id: project.id.to_proto(), + worktree_id: worktree.id, + path: settings_file.path, + content: Some(settings_file.content), + }, + )?; + } } for language_server in &project.language_servers { @@ -1410,6 +1423,18 @@ async fn join_project( }, )?; } + + for settings_file in worktree.settings_files { + session.peer.send( + session.connection_id, + proto::UpdateWorktreeSettings { + project_id: project_id.to_proto(), + worktree_id: worktree.id, + path: settings_file.path, + content: Some(settings_file.content), + }, + )?; + } } for language_server in &project.language_servers { @@ -1525,6 +1550,29 @@ async fn update_diagnostic_summary( Ok(()) } +async fn update_worktree_settings( + message: proto::UpdateWorktreeSettings, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .update_worktree_settings(&message, session.connection_id) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, message.clone()) + }, + ); + + Ok(()) +} + async fn start_language_server( request: proto::StartLanguageServer, session: Session, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 889f5bf77dbbde8452ee566f44abf255ea3907a8..ce4ede8684831fd78d0d6a16cc49905581fadc1a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3090,6 +3090,135 @@ async fn test_fs_operations( }); } +#[gpui::test(iterations = 10)] +async fn test_local_settings( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // As client A, open a project that contains some local settings files + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "a": { + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a.txt": "a-contents", + }, + "b": { + "b.txt": "b-contents", + } + }), + ) + .await; + let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // As client B, join that project and observe the local settings. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ] + ) + }); + + // As client A, update a settings file. As Client B, see the changed settings. + client_a + .fs + .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) + .await; + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("").into(), r#"{}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ] + ) + }); + + // As client A, create and remove some settings files. As client B, see the changed settings. + client_a + .fs + .remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) + .await + .unwrap(); + client_a + .fs + .create_dir("/dir/b/.zed".as_ref()) + .await + .unwrap(); + client_a + .fs + .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) + .await; + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), + ] + ) + }); + + // As client B, disconnect. + server.forbid_connections(); + server.disconnect_client(client_b.peer_id().unwrap()); + + // As client A, change and remove settings files while client B is disconnected. + client_a + .fs + .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) + .await; + client_a + .fs + .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) + .await + .unwrap(); + deterministic.run_until_parked(); + + // As client B, reconnect and see the changed settings. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] + ) + }); +} + #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save( deterministic: Arc, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index eb1755a9ff3c25494c93c5e320362d08da45c56b..720a73f477e1c5ceb4e8888de95afefe937f1589 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -472,7 +472,7 @@ impl CollabTitlebarItem { Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { - //TODO: Ensure this button has consistant width for both text variations + //TODO: Ensure this button has consistent width for both text variations let style = titlebar.share_button.style_for(state, false); Label::new(label, style.text.clone()) .contained() diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index de9104a6848d504eb78d28ab45da896d771dca29..e73424f0cd36945f1a24d3155521075c7967fa80 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,7 +4,7 @@ mod sign_in; use anyhow::{anyhow, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use collections::HashMap; +use collections::{HashMap, HashSet}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, @@ -127,7 +127,7 @@ impl CopilotServer { struct RunningCopilotServer { lsp: Arc, sign_in_status: SignInStatus, - registered_buffers: HashMap, + registered_buffers: HashMap, } #[derive(Clone, Debug)] @@ -163,7 +163,6 @@ impl Status { } struct RegisteredBuffer { - id: u64, uri: lsp::Url, language_id: String, snapshot: BufferSnapshot, @@ -178,13 +177,13 @@ impl RegisteredBuffer { buffer: &ModelHandle, cx: &mut ModelContext, ) -> oneshot::Receiver<(i32, BufferSnapshot)> { - let id = self.id; let (done_tx, done_rx) = oneshot::channel(); if buffer.read(cx).version() == self.snapshot.version { let _ = done_tx.send((self.snapshot_version, self.snapshot.clone())); } else { let buffer = buffer.downgrade(); + let id = buffer.id(); let prev_pending_change = mem::replace(&mut self.pending_buffer_change, Task::ready(None)); self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move { @@ -268,7 +267,7 @@ pub struct Copilot { http: Arc, node_runtime: Arc, server: CopilotServer, - buffers: HashMap>, + buffers: HashSet>, } impl Entity for Copilot { @@ -318,7 +317,7 @@ impl Copilot { fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { let http = self.http.clone(); let node_runtime = self.node_runtime.clone(); - if all_language_settings(cx).copilot_enabled(None, None) { + if all_language_settings(None, cx).copilot_enabled(None, None) { if matches!(self.server, CopilotServer::Disabled) { let start_task = cx .spawn({ @@ -375,7 +374,7 @@ impl Copilot { server .on_notification::(|params, _cx| { match params.level { - // Copilot is pretty agressive about logging + // Copilot is pretty aggressive about logging 0 => debug!("copilot: {}", params.message), 1 => debug!("copilot: {}", params.message), _ => error!("copilot: {}", params.message), @@ -559,8 +558,8 @@ impl Copilot { } pub fn register_buffer(&mut self, buffer: &ModelHandle, cx: &mut ModelContext) { - let buffer_id = buffer.read(cx).remote_id(); - self.buffers.insert(buffer_id, buffer.downgrade()); + let weak_buffer = buffer.downgrade(); + self.buffers.insert(weak_buffer.clone()); if let CopilotServer::Running(RunningCopilotServer { lsp: server, @@ -573,8 +572,7 @@ impl Copilot { return; } - let buffer_id = buffer.read(cx).remote_id(); - registered_buffers.entry(buffer_id).or_insert_with(|| { + registered_buffers.entry(buffer.id()).or_insert_with(|| { let uri: lsp::Url = uri_for_buffer(buffer, cx); let language_id = id_for_language(buffer.read(cx).language()); let snapshot = buffer.read(cx).snapshot(); @@ -592,7 +590,6 @@ impl Copilot { .log_err(); RegisteredBuffer { - id: buffer_id, uri, language_id, snapshot, @@ -603,8 +600,8 @@ impl Copilot { this.handle_buffer_event(buffer, event, cx).log_err(); }), cx.observe_release(buffer, move |this, _buffer, _cx| { - this.buffers.remove(&buffer_id); - this.unregister_buffer(buffer_id); + this.buffers.remove(&weak_buffer); + this.unregister_buffer(&weak_buffer); }), ], } @@ -619,8 +616,7 @@ impl Copilot { cx: &mut ModelContext, ) -> Result<()> { if let Ok(server) = self.server.as_running() { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) { + if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) { match event { language::Event::Edited => { let _ = registered_buffer.report_changes(&buffer, cx); @@ -674,9 +670,9 @@ impl Copilot { Ok(()) } - fn unregister_buffer(&mut self, buffer_id: u64) { + fn unregister_buffer(&mut self, buffer: &WeakModelHandle) { if let Ok(server) = self.server.as_running() { - if let Some(buffer) = server.registered_buffers.remove(&buffer_id) { + if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) { server .lsp .notify::( @@ -779,16 +775,12 @@ impl Copilot { Err(error) => return Task::ready(Err(error)), }; let lsp = server.lsp.clone(); - let buffer_id = buffer.read(cx).remote_id(); - let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap(); + let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap(); let snapshot = registered_buffer.report_changes(buffer, cx); let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); let position = position.to_point_utf16(buffer); - let settings = language_settings( - buffer.language_at(position).map(|l| l.name()).as_deref(), - cx, - ); + let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx); let tab_size = settings.tab_size; let hard_tabs = settings.hard_tabs; let relative_path = buffer @@ -853,7 +845,7 @@ impl Copilot { lsp_status: request::SignInStatus, cx: &mut ModelContext, ) { - self.buffers.retain(|_, buffer| buffer.is_upgradable(cx)); + self.buffers.retain(|buffer| buffer.is_upgradable(cx)); if let Ok(server) = self.server.as_running() { match lsp_status { @@ -861,7 +853,7 @@ impl Copilot { | request::SignInStatus::MaybeOk { .. } | request::SignInStatus::AlreadySignedIn { .. } => { server.sign_in_status = SignInStatus::Authorized; - for buffer in self.buffers.values().cloned().collect::>() { + for buffer in self.buffers.iter().cloned().collect::>() { if let Some(buffer) = buffer.upgrade(cx) { self.register_buffer(&buffer, cx); } @@ -869,14 +861,14 @@ impl Copilot { } request::SignInStatus::NotAuthorized { .. } => { server.sign_in_status = SignInStatus::Unauthorized; - for buffer_id in self.buffers.keys().copied().collect::>() { - self.unregister_buffer(buffer_id); + for buffer in self.buffers.iter().copied().collect::>() { + self.unregister_buffer(&buffer); } } request::SignInStatus::NotSignedIn => { server.sign_in_status = SignInStatus::SignedOut; - for buffer_id in self.buffers.keys().copied().collect::>() { - self.unregister_buffer(buffer_id); + for buffer in self.buffers.iter().copied().collect::>() { + self.unregister_buffer(&buffer); } } } @@ -899,9 +891,7 @@ fn uri_for_buffer(buffer: &ModelHandle, cx: &AppContext) -> lsp::Url { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { lsp::Url::from_file_path(file.abs_path(cx)).unwrap() } else { - format!("buffer://{}", buffer.read(cx).remote_id()) - .parse() - .unwrap() + format!("buffer://{}", buffer.id()).parse().unwrap() } } @@ -1175,6 +1165,10 @@ mod tests { fn to_proto(&self) -> rpc::proto::File { unimplemented!() } + + fn worktree_id(&self) -> usize { + 0 + } } impl language::LocalFile for File { diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 50fbaa64ee49de6ab1e682856eaec3ed16cfb4bc..c93e1920dc9081b94a7babc43ca42ab3305b5c76 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -9,7 +9,6 @@ path = "src/copilot_button.rs" doctest = false [dependencies] -assets = { path = "../assets" } copilot = { path = "../copilot" } editor = { path = "../editor" } fs = { path = "../fs" } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 17d27ca41f66caf08117518d29b7f6f627febe4c..2454074d459b4938cbeebadb5cf7cf73589b5d99 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -9,7 +9,10 @@ use gpui::{ AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::language_settings::{self, all_language_settings, AllLanguageSettings}; +use language::{ + language_settings::{self, all_language_settings, AllLanguageSettings}, + File, Language, +}; use settings::{update_settings_file, SettingsStore}; use std::{path::Path, sync::Arc}; use util::{paths, ResultExt}; @@ -26,8 +29,8 @@ pub struct CopilotButton { popup_menu: ViewHandle, editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, - language: Option>, - path: Option>, + language: Option>, + file: Option>, fs: Arc, } @@ -41,7 +44,7 @@ impl View for CopilotButton { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let all_language_settings = &all_language_settings(cx); + let all_language_settings = all_language_settings(None, cx); if !all_language_settings.copilot.feature_enabled { return Empty::new().into_any(); } @@ -165,7 +168,7 @@ impl CopilotButton { editor_subscription: None, editor_enabled: None, language: None, - path: None, + file: None, fs, } } @@ -197,14 +200,13 @@ impl CopilotButton { if let Some(language) = self.language.clone() { let fs = fs.clone(); - let language_enabled = - language_settings::language_settings(Some(language.as_ref()), cx) - .show_copilot_suggestions; + let language_enabled = language_settings::language_settings(Some(&language), None, cx) + .show_copilot_suggestions; menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for {}", if language_enabled { "Hide" } else { "Show" }, - language + language.name() ), move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), )); @@ -212,9 +214,9 @@ impl CopilotButton { let settings = settings::get::(cx); - if let Some(path) = self.path.as_ref() { - let path_enabled = settings.copilot_enabled_for_path(path); - let path = path.clone(); + if let Some(file) = &self.file { + let path = file.path().clone(); + let path_enabled = settings.copilot_enabled_for_path(&path); menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for This Path", @@ -276,17 +278,15 @@ impl CopilotButton { let editor = editor.read(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); let suggestion_anchor = editor.selections.newest_anchor().start; - let language_name = snapshot - .language_at(suggestion_anchor) - .map(|language| language.name()); - let path = snapshot.file_at(suggestion_anchor).map(|file| file.path()); + let language = snapshot.language_at(suggestion_anchor); + let file = snapshot.file_at(suggestion_anchor).cloned(); self.editor_enabled = Some( - all_language_settings(cx) - .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), + all_language_settings(self.file.as_ref(), cx) + .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())), ); - self.language = language_name; - self.path = path.cloned(); + self.language = language.cloned(); + self.file = file; cx.notify() } @@ -315,9 +315,7 @@ async fn configure_disabled_globs( let settings_editor = workspace .update(&mut cx, |_, cx| { create_and_open_local_file(&paths::SETTINGS, cx, || { - settings::initial_user_settings_content(&assets::Assets) - .as_ref() - .into() + settings::initial_user_settings_content().as_ref().into() }) })? .await? @@ -363,17 +361,18 @@ async fn configure_disabled_globs( } fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None); + let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None); update_settings_file::(fs, cx, move |file| { file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }); } -fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None); +fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { + let show_copilot_suggestions = + all_language_settings(None, cx).copilot_enabled(Some(&language), None); update_settings_file::(fs, cx, move |file| { file.languages - .entry(language) + .entry(language.name()) .or_default() .show_copilot_suggestions = Some(!show_copilot_suggestions); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 366e47ddc640fa6c9ee205debdc7b837b205ebe8..a594af51a6307bbfda7b421c8a723e0c0e3563ef 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -272,12 +272,11 @@ impl DisplayMap { } fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { - let language_name = buffer + let language = buffer .read(cx) .as_singleton() - .and_then(|buffer| buffer.read(cx).language()) - .map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + .and_then(|buffer| buffer.read(cx).language()); + language_settings(language.as_deref(), None, cx).tab_size } #[cfg(test)] @@ -476,7 +475,7 @@ impl DisplaySnapshot { }) } - /// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from` + /// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from` /// Stops if `condition` returns false for any of the character position pairs observed. pub fn find_while<'a>( &'a self, @@ -487,7 +486,7 @@ impl DisplaySnapshot { Self::find_internal(self.chars_at(from), target.chars().collect(), condition) } - /// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from` + /// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from` /// Stops if `condition` returns false for any of the character position pairs observed. pub fn reverse_find_while<'a>( &'a self, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 291d5cd329927834752f30091cfc5efe594a46fe..708a22c5c649a51b57b7ea8a5cd17d016fad4ded 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10,7 +10,7 @@ pub mod items; mod link_go_to_definition; mod mouse_context_menu; pub mod movement; -mod multi_buffer; +pub mod multi_buffer; mod persistence; pub mod scroll; pub mod selections_collection; @@ -31,9 +31,13 @@ use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; -pub use element::*; +pub use element::RenderExcerptHeaderParams; +pub use element::{ + Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, +}; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::LayoutContext; use gpui::{ actions, color::Color, @@ -44,9 +48,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json::{self, json}, - AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, - ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, + Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -109,6 +113,12 @@ pub struct SelectNext { pub replace_newest: bool, } +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct SelectPrevious { + #[serde(default)] + pub replace_newest: bool, +} + #[derive(Clone, Deserialize, PartialEq)] pub struct SelectToBeginningOfLine { #[serde(default)] @@ -270,6 +280,7 @@ impl_actions!( editor, [ SelectNext, + SelectPrevious, SelectToBeginningOfLine, SelectToEndOfLine, ToggleCodeActions, @@ -365,6 +376,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::add_selection_above); cx.add_action(Editor::add_selection_below); cx.add_action(Editor::select_next); + cx.add_action(Editor::select_previous); cx.add_action(Editor::toggle_comments); cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); @@ -482,6 +494,7 @@ pub struct Editor { columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, + select_prev_state: Option, selection_history: SelectionHistory, autoclose_regions: Vec, snippet_stack: InvalidationStack, @@ -496,7 +509,9 @@ pub struct Editor { blink_manager: ModelHandle, show_local_selections: bool, mode: EditorMode, + show_gutter: bool, placeholder_text: Option>, + render_excerpt_header: Option, highlighted_rows: Option>, #[allow(clippy::type_complexity)] background_highlights: BTreeMap Color, Vec>)>, @@ -526,6 +541,7 @@ pub struct Editor { pub struct EditorSnapshot { pub mode: EditorMode, + pub show_gutter: bool, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, is_focused: bool, @@ -537,6 +553,7 @@ pub struct EditorSnapshot { struct SelectionHistoryEntry { selections: Arc<[Selection]>, select_next_state: Option, + select_prev_state: Option, add_selections_state: Option, } @@ -1284,6 +1301,7 @@ impl Editor { columnar_selection_tail: None, add_selections_state: None, select_next_state: None, + select_prev_state: None, selection_history: Default::default(), autoclose_regions: Default::default(), snippet_stack: Default::default(), @@ -1297,7 +1315,9 @@ impl Editor { blink_manager: blink_manager.clone(), show_local_selections: true, mode, + show_gutter: mode == EditorMode::Full, placeholder_text: None, + render_excerpt_header: None, highlighted_rows: None, background_highlights: Default::default(), nav_history: None, @@ -1393,6 +1413,7 @@ impl Editor { pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { EditorSnapshot { mode: self.mode, + show_gutter: self.show_gutter, display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), scroll_anchor: self.scroll_manager.anchor(), ongoing_scroll: self.scroll_manager.ongoing_scroll(), @@ -1505,6 +1526,7 @@ impl Editor { let buffer = &display_map.buffer_snapshot; self.add_selections_state = None; self.select_next_state = None; + self.select_prev_state = None; self.select_larger_syntax_node_stack.clear(); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.snippet_stack @@ -2157,40 +2179,72 @@ impl Editor { indent.len = cmp::min(indent.len, start_point.column); let start = selection.start; let end = selection.end; + let is_cursor = start == end; + let language_scope = buffer.language_scope_at(start); + let (comment_delimiter, insert_extra_newline) = + if let Some(language) = &language_scope { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let insert_extra_newline = + language.brackets().any(|(pair, enabled)| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + enabled + && pair.newline + && buffer.contains_str_at( + end + trailing_whitespace_len, + pair_end, + ) + && buffer.contains_str_at( + (start - leading_whitespace_len) + .saturating_sub(pair_start.len()), + pair_start, + ) + }); + // Comment extension on newline is allowed only for cursor selections + let comment_delimiter = + language.line_comment_prefix().filter(|_| is_cursor); + let comment_delimiter = if let Some(delimiter) = comment_delimiter { + buffer + .buffer_line_for_row(start_point.row) + .is_some_and(|(snapshot, range)| { + snapshot + .chars_for_range(range) + .skip_while(|c| c.is_whitespace()) + .take(delimiter.len()) + .eq(delimiter.chars()) + }) + .then(|| delimiter.clone()) + } else { + None + }; + (comment_delimiter, insert_extra_newline) + } else { + (None, false) + }; - let mut insert_extra_newline = false; - if let Some(language) = buffer.language_scope_at(start) { - let leading_whitespace_len = buffer - .reversed_chars_at(start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - let trailing_whitespace_len = buffer - .chars_at(end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - insert_extra_newline = language.brackets().any(|(pair, enabled)| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); - - enabled - && pair.newline - && buffer - .contains_str_at(end + trailing_whitespace_len, pair_end) - && buffer.contains_str_at( - (start - leading_whitespace_len) - .saturating_sub(pair_start.len()), - pair_start, - ) - }); - } - - let mut new_text = String::with_capacity(1 + indent.len as usize); - new_text.push('\n'); + let capacity_for_delimiter = comment_delimiter + .as_deref() + .map(str::len) + .unwrap_or_default(); + let mut new_text = + String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); + new_text.push_str("\n"); new_text.extend(indent.chars()); + if let Some(delimiter) = &comment_delimiter { + new_text.push_str(&delimiter); + } if insert_extra_newline { new_text = new_text.repeat(2); } @@ -2372,7 +2426,7 @@ impl Editor { old_selections .iter() .map(|s| { - let anchor = snapshot.anchor_after(s.end); + let anchor = snapshot.anchor_after(s.head()); s.map(|_| anchor) }) .collect::>() @@ -2525,7 +2579,7 @@ impl Editor { .read(cx) .text_anchor_for_position(position.clone(), cx)?; - // OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances, + // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, // hence we do LSP request & edit on host side only — add formats to host's history. let push_to_lsp_host_history = true; // If this is not the host, append its history with new edits. @@ -3207,12 +3261,10 @@ impl Editor { snapshot: &MultiBufferSnapshot, cx: &mut ViewContext, ) -> bool { - let path = snapshot.file_at(location).map(|file| file.path().as_ref()); - let language_name = snapshot - .language_at(location) - .map(|language| language.name()); - let settings = all_language_settings(cx); - settings.copilot_enabled(language_name.as_deref(), path) + let file = snapshot.file_at(location); + let language = snapshot.language_at(location); + let settings = all_language_settings(file, cx); + settings.copilot_enabled(language, file.map(|f| f.path().as_ref())) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { @@ -3549,7 +3601,9 @@ impl Editor { s.move_with(|map, selection| { if selection.is_empty() && !line_mode { let cursor = movement::right(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); + selection.end = cursor; + selection.reversed = true; + selection.goal = SelectionGoal::None; } }) }); @@ -5213,6 +5267,101 @@ impl Editor { } } + pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext) { + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_prev_state) = self.select_prev_state.take() { + let query = &select_prev_state.query; + if !select_prev_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. + let bytes_before_last_selection = + buffer.reversed_bytes_in_range(0..last_selection.start); + let bytes_after_first_selection = + buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); + let query_matches = query + .stream_find_iter(bytes_before_last_selection) + .map(|result| (last_selection.start, result)) + .chain( + query + .stream_find_iter(bytes_after_first_selection) + .map(|result| (buffer.len(), result)), + ); + for (end_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + end_offset - query_match.end()..end_offset - query_match.start(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_prev_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + self.unfold_ranges([next_selected_range.clone()], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + if action.replace_newest { + s.delete(s.newest_anchor().id); + } + s.insert_range(next_selected_range); + }); + } else { + select_prev_state.done = true; + } + } + + self.select_prev_state = Some(select_prev_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + let select_state = SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: true, + done: false, + }; + self.unfold_ranges([selection.start..selection.end], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select(selections); + }); + self.select_prev_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + self.select_prev_state = Some(SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: false, + done: false, + }); + self.select_previous(action, cx); + } + } + } + pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); @@ -5586,6 +5735,7 @@ impl Editor { if let Some(entry) = self.selection_history.undo_stack.pop_back() { self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::newest(), cx); } @@ -5598,6 +5748,7 @@ impl Editor { if let Some(entry) = self.selection_history.redo_stack.pop_back() { self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::newest(), cx); } @@ -6375,6 +6526,7 @@ impl Editor { self.selection_history.push(SelectionHistoryEntry { selections: self.selections.disjoint_anchors(), select_next_state: self.select_next_state.clone(), + select_prev_state: self.select_prev_state.clone(), add_selections_state: self.add_selections_state.clone(), }); } @@ -6654,6 +6806,25 @@ impl Editor { cx.notify(); } + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext) { + self.show_gutter = show_gutter; + cx.notify(); + } + + pub fn set_render_excerpt_header( + &mut self, + render_excerpt_header: impl 'static + + Fn( + &mut Editor, + RenderExcerptHeaderParams, + &mut LayoutContext, + ) -> AnyElement, + cx: &mut ViewContext, + ) { + self.render_excerpt_header = Some(Arc::new(render_excerpt_header)); + cx.notify(); + } + pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { @@ -6878,7 +7049,7 @@ impl Editor { multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } - multi_buffer::Event::LanguageChanged => {} + _ => {} } } @@ -7076,11 +7247,13 @@ impl Editor { }; // If None, we are in a file without an extension - let file_extension = file_extension.or(self + let file = self .buffer .read(cx) .as_singleton() - .and_then(|b| b.read(cx).file()) + .and_then(|b| b.read(cx).file()); + let file_extension = file_extension.or(file + .as_ref() .and_then(|file| Path::new(file.file_name(cx)).extension()) .and_then(|e| e.to_str()) .map(|a| a.to_string())); @@ -7091,7 +7264,7 @@ impl Editor { .get("vim_mode") == Some(&serde_json::Value::Bool(true)); let telemetry_settings = *settings::get::(cx); - let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None); + let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None); let copilot_enabled_for_language = self .buffer .read(cx) @@ -7099,15 +7272,6 @@ impl Editor { .show_copilot_suggestions; let telemetry = project.read(cx).client().telemetry().clone(); - telemetry.report_mixpanel_event( - match name { - "open" => "open editor", - "save" => "save editor", - _ => name, - }, - json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }), - telemetry_settings, - ); let event = ClickhouseEvent::Editor { file_extension, vim_mode, @@ -7299,8 +7463,12 @@ impl View for Editor { }); } + let mut editor = EditorElement::new(style.clone()); + if let Some(render_excerpt_header) = self.render_excerpt_header.clone() { + editor = editor.with_render_excerpt_header(render_excerpt_header); + } Stack::new() - .with_child(EditorElement::new(style.clone())) + .with_child(editor) .with_child(ChildView::new(&self.mouse_context_menu, cx)) .into_any() } @@ -7801,13 +7969,13 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } pub fn highlight_diagnostic_message( - inital_highlights: Vec, + initial_highlights: Vec, message: &str, ) -> (String, Vec) { let mut message_without_backticks = String::new(); let mut prev_offset = 0; let mut inside_block = false; - let mut highlights = inital_highlights; + let mut highlights = initial_highlights; for (match_ix, (offset, _)) in message .match_indices('`') .chain([(message.len(), "")]) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index bc671b9ffc3652856d16faf567784eab438440b7..a94f01a386e2738b8a18b5c70b18490bb11e78d0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9,7 +9,8 @@ use gpui::{ executor::Deterministic, geometry::{rect::RectF, vector::vec2f}, platform::{WindowBounds, WindowOptions}, - serde_json, TestAppContext, + serde_json::{self, json}, + TestAppContext, }; use indoc::indoc; use language::{ @@ -578,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); // Ensure we don't panic when navigation data contains invalid anchors *and* points. - let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor; + let mut invalid_anchor = editor.scroll_manager.anchor().anchor; invalid_anchor.text_anchor.buffer_id = Some(999); let invalid_point = Point::new(9999, 0); editor.navigate( @@ -586,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { cursor_anchor: invalid_anchor, cursor_position: invalid_point, scroll_anchor: ScrollAnchor { - top_anchor: invalid_anchor, + anchor: invalid_anchor, offset: Default::default(), }, scroll_top_row: invalid_point.row, @@ -1718,6 +1719,33 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { "}); } +#[gpui::test] +async fn test_newline_comments(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("//".into()), + ..LanguageConfig::default() + }, + None, + )); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + // Fooˇ + "}); + + cx.update_editor(|e, cx| e.newline(&Newline, cx)); + cx.assert_editor_state(indoc! {" + // Foo + //ˇ + "}); +} + #[gpui::test] fn test_insert_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -3107,6 +3135,57 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } +#[gpui::test] +async fn test_select_previous(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + { + // `Select previous` without a selection (selects wordwise) + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + } + { + // `Select previous` with a selection + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); + } +} + #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -4270,7 +4349,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - // Set rust language override and assert overriden tabsize is sent to language server + // Set rust language override and assert overridden tabsize is sent to language server update_test_settings(cx, |settings| { settings.languages.insert( "Rust".into(), @@ -4384,7 +4463,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - // Set rust language override and assert overriden tabsize is sent to language server + // Set rust language override and assert overridden tabsize is sent to language server update_test_settings(cx, |settings| { settings.languages.insert( "Rust".into(), @@ -4725,7 +4804,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { two threeˇ "}, - "overlapping aditional edit", + "overlapping additional edit", ), ( indoc! {" @@ -5225,7 +5304,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), ] - ) + ); + + // Ensure the cursor's head is respected when deleting across an excerpt boundary. + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "Xa\nbbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(1, 0)..Point::new(1, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "X\nbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(0, 1)..Point::new(0, 1)] + ); }); } @@ -5742,7 +5842,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); follower.set_scroll_anchor( ScrollAnchor { - top_anchor, + anchor: top_anchor, offset: vec2f(0.0, 0.5), }, cx, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6be065084814e570a0411ffb91df4a4eb380e1ac..9aec670659df0b9ecba06ddbd4bffb1e26119ab3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -91,18 +91,41 @@ impl SelectionLayout { } } -#[derive(Clone)] +pub struct RenderExcerptHeaderParams<'a> { + pub id: crate::ExcerptId, + pub buffer: &'a language::BufferSnapshot, + pub range: &'a crate::ExcerptRange, + pub starts_new_buffer: bool, + pub gutter_padding: f32, + pub editor_style: &'a EditorStyle, +} + +pub type RenderExcerptHeader = Arc< + dyn Fn( + &mut Editor, + RenderExcerptHeaderParams, + &mut LayoutContext, + ) -> AnyElement, +>; + pub struct EditorElement { style: Arc, + render_excerpt_header: RenderExcerptHeader, } impl EditorElement { pub fn new(style: EditorStyle) -> Self { Self { style: Arc::new(style), + render_excerpt_header: Arc::new(render_excerpt_header), } } + pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self { + self.render_excerpt_header = render; + self + } + fn attach_mouse_handlers( scene: &mut SceneBuilder, position_map: &Arc, @@ -1465,11 +1488,9 @@ impl EditorElement { line_height: f32, style: &EditorStyle, line_layouts: &[LineWithInvisibles], - include_root: bool, editor: &mut Editor, cx: &mut LayoutContext, ) -> (f32, Vec) { - let tooltip_style = theme::current(cx).tooltip.clone(); let scroll_x = snapshot.scroll_anchor.offset.x(); let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) @@ -1510,112 +1531,18 @@ impl EditorElement { range, starts_new_buffer, .. - } => { - let id = *id; - let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { - let jump_path = ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }; - let jump_anchor = range - .primary - .as_ref() - .map_or(range.context.start, |primary| primary.start); - let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - - enum JumpIcon {} - MouseEventHandler::::new(id.into(), cx, |state, _| { - let style = style.jump_icon.style_for(state, false); - Svg::new("icons/arrow_up_right_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, editor, cx| { - if let Some(workspace) = editor - .workspace - .as_ref() - .and_then(|(workspace, _)| workspace.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Editor::jump( - workspace, - jump_path.clone(), - jump_position, - jump_anchor, - cx, - ); - }); - } - }) - .with_tooltip::( - id.into(), - "Jump to Buffer".to_string(), - Some(Box::new(crate::OpenExcerpts)), - tooltip_style.clone(), - cx, - ) - .aligned() - .flex_float() - }); - - if *starts_new_buffer { - let style = &self.style.diagnostic_path_header; - let font_size = - (style.text_scale_factor * self.style.text.font_size).round(); - - let path = buffer.resolve_file_path(cx, include_root); - let mut filename = None; - let mut parent_path = None; - // Can't use .and_then() because `.file_name()` and `.parent()` return references :( - if let Some(path) = path { - filename = path.file_name().map(|f| f.to_string_lossy().to_string()); - parent_path = - path.parent().map(|p| p.to_string_lossy().to_string() + "/"); - } - - Flex::row() - .with_child( - Label::new( - filename.unwrap_or_else(|| "untitled".to_string()), - style.filename.text.clone().with_font_size(font_size), - ) - .contained() - .with_style(style.filename.container) - .aligned(), - ) - .with_children(parent_path.map(|path| { - Label::new(path, style.path.text.clone().with_font_size(font_size)) - .contained() - .with_style(style.path.container) - .aligned() - })) - .with_children(jump_icon) - .contained() - .with_style(style.container) - .with_padding_left(gutter_padding) - .with_padding_right(gutter_padding) - .expanded() - .into_any_named("path header block") - } else { - let text_style = self.style.text.clone(); - Flex::row() - .with_child(Label::new("⋯", text_style)) - .with_children(jump_icon) - .contained() - .with_padding_left(gutter_padding) - .with_padding_right(gutter_padding) - .expanded() - .into_any_named("collapsed context") - } - } + } => (self.render_excerpt_header)( + editor, + RenderExcerptHeaderParams { + id: *id, + buffer, + range, + starts_new_buffer: *starts_new_buffer, + gutter_padding, + editor_style: style, + }, + cx, + ), }; element.layout( @@ -1899,7 +1826,7 @@ impl Element for EditorElement { let gutter_padding; let gutter_width; let gutter_margin; - if snapshot.mode == EditorMode::Full { + if snapshot.show_gutter { let em_width = style.text.em_width(cx.font_cache()); gutter_padding = (em_width * style.gutter_padding_factor).round(); gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; @@ -2080,12 +2007,6 @@ impl Element for EditorElement { ShowScrollbar::Never => false, }; - let include_root = editor - .project - .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(); - let fold_ranges: Vec<(BufferRow, Range, Color)> = fold_ranges .into_iter() .map(|(id, fold)| { @@ -2144,7 +2065,6 @@ impl Element for EditorElement { line_height, &style, &line_layouts, - include_root, editor, cx, ); @@ -2759,6 +2679,121 @@ impl HighlightedRange { } } +fn render_excerpt_header( + editor: &mut Editor, + RenderExcerptHeaderParams { + id, + buffer, + range, + starts_new_buffer, + gutter_padding, + editor_style, + }: RenderExcerptHeaderParams, + cx: &mut LayoutContext, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + let include_root = editor + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary + .as_ref() + .map_or(range.context.start, |primary| primary.start); + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + + enum JumpIcon {} + MouseEventHandler::::new(id.into(), cx, |state, _| { + let style = editor_style.jump_icon.style_for(state, false); + Svg::new("icons/arrow_up_right_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, editor, cx| { + if let Some(workspace) = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx); + }); + } + }) + .with_tooltip::( + id.into(), + "Jump to Buffer".to_string(), + Some(Box::new(crate::OpenExcerpts)), + tooltip_style.clone(), + cx, + ) + .aligned() + .flex_float() + }); + + if starts_new_buffer { + let style = &editor_style.diagnostic_path_header; + let font_size = (style.text_scale_factor * editor_style.text.font_size).round(); + + let path = buffer.resolve_file_path(cx, include_root); + let mut filename = None; + let mut parent_path = None; + // Can't use .and_then() because `.file_name()` and `.parent()` return references :( + if let Some(path) = path { + filename = path.file_name().map(|f| f.to_string_lossy().to_string()); + parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + } + + Flex::row() + .with_child( + Label::new( + filename.unwrap_or_else(|| "untitled".to_string()), + style.filename.text.clone().with_font_size(font_size), + ) + .contained() + .with_style(style.filename.container) + .aligned(), + ) + .with_children(parent_path.map(|path| { + Label::new(path, style.path.text.clone().with_font_size(font_size)) + .contained() + .with_style(style.path.container) + .aligned() + })) + .with_children(jump_icon) + .contained() + .with_style(style.container) + .with_padding_left(gutter_padding) + .with_padding_right(gutter_padding) + .expanded() + .into_any_named("path header block") + } else { + let text_style = editor_style.text.clone(); + Flex::row() + .with_child(Label::new("⋯", text_style)) + .with_children(jump_icon) + .contained() + .with_padding_left(gutter_padding) + .with_padding_right(gutter_padding) + .expanded() + .into_any_named("collapsed context") + } +} + fn position_to_display_point( position: Vector2F, text_bounds: RectF, @@ -3080,7 +3115,7 @@ mod tests { editor_width: f32, ) -> Vec { info!( - "Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'" + "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" ); let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&input_text, cx); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9192dc75e13997dcd77f10da172ce7084b479a77..7a203d54a96764c5ccca9e354f85e648a8529c00 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -221,6 +221,7 @@ fn show_hover( project: project.clone(), symbol_range: range, blocks: hover_result.contents, + language: hover_result.language, rendered_content: None, }) }); @@ -253,6 +254,7 @@ fn render_blocks( theme_id: usize, blocks: &[HoverBlock], language_registry: &Arc, + language: Option<&Arc>, style: &EditorStyle, ) -> RenderedInfo { let mut text = String::new(); @@ -351,11 +353,13 @@ fn render_blocks( } Tag::CodeBlock(kind) => { new_paragraph(&mut text, &mut list_stack); - if let CodeBlockKind::Fenced(language) = kind { - current_language = language_registry + current_language = if let CodeBlockKind::Fenced(language) = kind { + language_registry .language_for_name(language.as_ref()) .now_or_never() - .and_then(Result::ok); + .and_then(Result::ok) + } else { + language.cloned() } } Tag::Emphasis => italic_depth += 1, @@ -414,10 +418,6 @@ fn render_blocks( } } - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - RenderedInfo { theme_id, text, @@ -524,6 +524,7 @@ pub struct InfoPopover { pub project: ModelHandle, pub symbol_range: Range, pub blocks: Vec, + language: Option>, rendered_content: Option, } @@ -559,6 +560,7 @@ impl InfoPopover { style.theme_id, &self.blocks, self.project.read(cx).languages(), + self.language.as_ref(), style, ) }); @@ -588,10 +590,7 @@ impl InfoPopover { MouseRegion::new::(view_id, region_id, bounds) .on_click::( MouseButton::Left, - move |_, _, cx| { - println!("clicked link {url}"); - cx.platform().open_url(&url); - }, + move |_, _, cx| cx.platform().open_url(&url), ), ); } @@ -906,7 +905,7 @@ mod tests { text: "one **two** three".to_string(), kind: HoverBlockKind::Markdown, }], - expected_marked_text: "one «two» three\n".to_string(), + expected_marked_text: "one «two» three".to_string(), expected_styles: vec![HighlightStyle { weight: Some(Weight::BOLD), ..Default::default() @@ -918,7 +917,7 @@ mod tests { text: "one [two](the-url) three".to_string(), kind: HoverBlockKind::Markdown, }], - expected_marked_text: "one «two» three\n".to_string(), + expected_marked_text: "one «two» three".to_string(), expected_styles: vec![HighlightStyle { underline: Some(Underline { thickness: 1.0.into(), @@ -937,8 +936,7 @@ mod tests { - b * two - [c](the-url) - - d - " + - d" .unindent(), kind: HoverBlockKind::Markdown, }], @@ -949,8 +947,7 @@ mod tests { - b - two - «c» - - d - " + - d" .unindent(), expected_styles: vec![HighlightStyle { underline: Some(Underline { @@ -973,9 +970,8 @@ mod tests { nine * ten - * six - " - .unindent(), + * six" + .unindent(), kind: HoverBlockKind::Markdown, }], expected_marked_text: " @@ -985,9 +981,8 @@ mod tests { nine - ten - - six - " - .unindent(), + - six" + .unindent(), expected_styles: vec![HighlightStyle { underline: Some(Underline { thickness: 1.0.into(), @@ -1004,7 +999,7 @@ mod tests { expected_styles, } in &rows[0..] { - let rendered = render_blocks(0, &blocks, &Default::default(), &style); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let expected_highlights = ranges diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 40e7c89cb298e34510002550349f7faff6f0fa19..9d639f9b7bd6aeebe619b9382862c75f0347c7ad 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -196,7 +196,7 @@ impl FollowableItem for Editor { singleton: buffer.is_singleton(), title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), excerpts, - scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)), + scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)), scroll_x: scroll_anchor.offset.x(), scroll_y: scroll_anchor.offset.y(), selections: self @@ -253,7 +253,7 @@ impl FollowableItem for Editor { } Event::ScrollPositionChanged { .. } => { let scroll_anchor = self.scroll_manager.anchor(); - update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor)); + update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor)); update.scroll_x = scroll_anchor.offset.x(); update.scroll_y = scroll_anchor.offset.y(); true @@ -412,7 +412,7 @@ async fn update_editor_from_message( } else if let Some(scroll_top_anchor) = scroll_top_anchor { editor.set_scroll_anchor_remote( ScrollAnchor { - top_anchor: scroll_top_anchor, + anchor: scroll_top_anchor, offset: vec2f(message.scroll_x, message.scroll_y), }, cx, @@ -510,8 +510,8 @@ impl Item for Editor { }; let mut scroll_anchor = data.scroll_anchor; - if !buffer.can_resolve(&scroll_anchor.top_anchor) { - scroll_anchor.top_anchor = buffer.anchor_before( + if !buffer.can_resolve(&scroll_anchor.anchor) { + scroll_anchor.anchor = buffer.anchor_before( buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), ); } @@ -1231,6 +1231,10 @@ mod tests { unimplemented!() } + fn worktree_id(&self) -> usize { + 0 + } + fn is_deleted(&self) -> bool { unimplemented!() } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4650dff38f58088eab5fc1638080680dc86dd84e..0d7fb6a450d9ee784c1c601450d2170939a7e073 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -64,6 +64,9 @@ pub enum Event { ExcerptsRemoved { ids: Vec, }, + ExcerptsEdited { + ids: Vec, + }, Edited, Reloaded, DiffBaseChanged, @@ -196,6 +199,13 @@ pub struct MultiBufferBytes<'a> { chunk: &'a [u8], } +pub struct ReversedMultiBufferBytes<'a> { + range: Range, + excerpts: Cursor<'a, Excerpt, usize>, + excerpt_bytes: Option>, + chunk: &'a [u8], +} + struct ExcerptChunks<'a> { content_chunks: BufferChunks<'a>, footer_height: usize, @@ -387,6 +397,7 @@ impl MultiBuffer { original_indent_column: u32, } let mut buffer_edits: HashMap> = Default::default(); + let mut edited_excerpt_ids = Vec::new(); let mut cursor = snapshot.excerpts.cursor::(); for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); @@ -403,6 +414,7 @@ impl MultiBuffer { .start .to_offset(&start_excerpt.buffer) + start_overshoot; + edited_excerpt_ids.push(start_excerpt.id); cursor.seek(&range.end, Bias::Right, &()); if cursor.item().is_none() && range.end == *cursor.start() { @@ -428,6 +440,7 @@ impl MultiBuffer { original_indent_column, }); } else { + edited_excerpt_ids.push(end_excerpt.id); let start_excerpt_range = buffer_start ..start_excerpt .range @@ -474,6 +487,7 @@ impl MultiBuffer { is_insertion: false, original_indent_column, }); + edited_excerpt_ids.push(excerpt.id); cursor.next(&()); } } @@ -546,6 +560,10 @@ impl MultiBuffer { buffer.edit(insertions, insertion_autoindent_mode, cx); }) } + + cx.emit(Event::ExcerptsEdited { + ids: edited_excerpt_ids, + }); } pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { @@ -1377,8 +1395,14 @@ impl MultiBuffer { point: T, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language = self.language_at(point, cx); - language_settings(language.map(|l| l.name()).as_deref(), cx) + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { + let buffer = buffer.read(cx); + language = buffer.language_at(offset); + file = buffer.file(); + } + language_settings(language.as_ref(), file, cx) } pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { @@ -1961,7 +1985,6 @@ impl MultiBufferSnapshot { } else { None }; - MultiBufferBytes { range, excerpts, @@ -1970,6 +1993,33 @@ impl MultiBufferSnapshot { } } + pub fn reversed_bytes_in_range( + &self, + range: Range, + ) -> ReversedMultiBufferBytes { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut excerpts = self.excerpts.cursor::(); + excerpts.seek(&range.end, Bias::Left, &()); + + let mut chunk = &[][..]; + let excerpt_bytes = if let Some(excerpt) = excerpts.item() { + let mut excerpt_bytes = excerpt.reversed_bytes_in_range( + range.start - excerpts.start()..range.end - excerpts.start(), + ); + chunk = excerpt_bytes.next().unwrap_or(&[][..]); + Some(excerpt_bytes) + } else { + None + }; + + ReversedMultiBufferBytes { + range, + excerpts, + excerpt_bytes, + chunk, + } + } + pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { let mut result = MultiBufferRows { buffer_row_range: 0..0, @@ -2785,9 +2835,13 @@ impl MultiBufferSnapshot { point: T, cx: &'a AppContext, ) -> &'a LanguageSettings { - self.point_to_buffer_offset(point) - .map(|(buffer, offset)| buffer.settings_at(offset, cx)) - .unwrap_or_else(|| language_settings(None, cx)) + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { + language = buffer.language_at(offset); + file = buffer.file(); + } + language_settings(language, file, cx) } pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { @@ -3399,6 +3453,26 @@ impl Excerpt { } } + fn reversed_bytes_in_range(&self, range: Range) -> ExcerptBytes { + let content_start = self.range.context.start.to_offset(&self.buffer); + let bytes_start = content_start + range.start; + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); + let footer_height = if self.has_trailing_newline + && range.start <= self.text_summary.len + && range.end > self.text_summary.len + { + 1 + } else { + 0 + }; + let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end); + + ExcerptBytes { + content_bytes, + footer_height, + } + } + fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { if text_anchor .cmp(&self.range.context.start, &self.buffer) @@ -3717,6 +3791,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> { } } +impl<'a> ReversedMultiBufferBytes<'a> { + fn consume(&mut self, len: usize) { + self.range.end -= len; + self.chunk = &self.chunk[..self.chunk.len() - len]; + + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { + self.chunk = chunk; + } else { + self.excerpts.next(&()); + if let Some(excerpt) = self.excerpts.item() { + let mut excerpt_bytes = + excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); + self.chunk = excerpt_bytes.next().unwrap(); + self.excerpt_bytes = Some(excerpt_bytes); + } + } + } + } +} + +impl<'a> io::Read for ReversedMultiBufferBytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let len = cmp::min(buf.len(), self.chunk.len()); + buf[..len].copy_from_slice(&self.chunk[..len]); + buf[..len].reverse(); + if len > 0 { + self.consume(len); + } + Ok(len) + } +} impl<'a> Iterator for ExcerptBytes<'a> { type Item = &'a [u8]; @@ -5237,7 +5343,7 @@ mod tests { assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); // An undo in the multibuffer undoes the multibuffer transaction - // and also any individual buffer edits that have occured since + // and also any individual buffer edits that have occurred since // that transaction. multibuffer.undo(cx); assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 21894dea88fbf68f9e93fd120082daa60714e2b7..17e8d18a625434b2fd81f8ea6938c72729aed423 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -36,21 +36,21 @@ pub struct ScrollbarAutoHide(pub bool); #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { pub offset: Vector2F, - pub top_anchor: Anchor, + pub anchor: Anchor, } impl ScrollAnchor { fn new() -> Self { Self { offset: Vector2F::zero(), - top_anchor: Anchor::min(), + anchor: Anchor::min(), } } pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { let mut scroll_position = self.offset; - if self.top_anchor != Anchor::min() { - let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32; + if self.anchor != Anchor::min() { + let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; scroll_position.set_y(scroll_top + scroll_position.y()); } else { scroll_position.set_y(0.); @@ -59,7 +59,7 @@ impl ScrollAnchor { } pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { - self.top_anchor.to_point(buffer).row + self.anchor.to_point(buffer).row } } @@ -179,7 +179,7 @@ impl ScrollManager { let (new_anchor, top_row) = if scroll_position.y() <= 0. { ( ScrollAnchor { - top_anchor: Anchor::min(), + anchor: Anchor::min(), offset: scroll_position.max(vec2f(0., 0.)), }, 0, @@ -193,7 +193,7 @@ impl ScrollManager { ( ScrollAnchor { - top_anchor, + anchor: top_anchor, offset: vec2f( scroll_position.x(), scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, @@ -322,7 +322,7 @@ impl Editor { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); let top_row = scroll_anchor - .top_anchor + .anchor .to_point(&self.buffer().read(cx).snapshot(cx)) .row; self.scroll_manager @@ -337,7 +337,7 @@ impl Editor { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); let top_row = scroll_anchor - .top_anchor + .anchor .to_point(&self.buffer().read(cx).snapshot(cx)) .row; self.scroll_manager @@ -377,7 +377,7 @@ impl Editor { let screen_top = self .scroll_manager .anchor - .top_anchor + .anchor .to_display_point(&snapshot); if screen_top > newest_head { @@ -408,7 +408,7 @@ impl Editor { .anchor_at(Point::new(top_row as u32, 0), Bias::Left); let scroll_anchor = ScrollAnchor { offset: Vector2F::new(x, y), - top_anchor, + anchor: top_anchor, }; self.set_scroll_anchor(scroll_anchor, cx); } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index a79b0f24498c27e43969e9fcddd4cf775733a983..da5e3603e7e2326c690ba3e3a80213874cf325b6 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -86,7 +86,7 @@ impl Editor { editor.set_scroll_anchor( ScrollAnchor { - top_anchor: new_anchor, + anchor: new_anchor, offset: Default::default(), }, cx, @@ -113,7 +113,7 @@ impl Editor { editor.set_scroll_anchor( ScrollAnchor { - top_anchor: new_anchor, + anchor: new_anchor, offset: Default::default(), }, cx, @@ -143,7 +143,7 @@ impl Editor { editor.set_scroll_anchor( ScrollAnchor { - top_anchor: new_anchor, + anchor: new_anchor, offset: Default::default(), }, cx, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 4c32b543b210b509b75319cb3094621116b557c8..d82ce5e21637fde58d1e19c866befe53270f2e9f 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -76,6 +76,9 @@ impl SelectionsCollection { count } + /// The non-pending, non-overlapping selections. There could still be a pending + /// selection that overlaps these if the mouse is being dragged, etc. Returned as + /// selections over Anchors. pub fn disjoint_anchors(&self) -> Arc<[Selection]> { self.disjoint.clone() } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 8a3e08bea570ec745b527c24951540026caa4879..9b568e4a4fe27ed36c47c9fb02561d4a63cfb237 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -48,8 +48,8 @@ pub fn marked_display_snapshot( } pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { - let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true); - assert_eq!(editor.text(cx), umarked_text); + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); + assert_eq!(editor.text(cx), unmarked_text); editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); } diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index b6eb6e8b6d0f630778398a18b2e2645b807f756b..48955a86291282a3ef4bf55ca27e9a838a2f51e9 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -6,6 +6,8 @@ use std::{env, fmt::Display}; use sysinfo::{System, SystemExt}; use util::channel::ReleaseChannel; +// TODO: Move this file out of feedback and into a more general place + #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { #[serde(serialize_with = "serialize_app_version")] diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index aff7439f0afcd2f8208ccf63c15e1cadc5fc7415..60e0a14461f5f83538a5b8d6b78f6b0cee48f571 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -33,7 +33,7 @@ use repository::{FakeGitRepositoryState, GitFileStatus}; use std::sync::Weak; lazy_static! { - static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); + static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); } #[derive(Clone, Copy, Debug, PartialEq)] @@ -78,13 +78,13 @@ impl LineEnding { } pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { *text = replaced; } } pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { replaced.into() } else { text diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index c1dc13084e804d10d0f2134d0160c165098c4f5f..31b7db008d28d56849c7ce36eb3f23336fe82fad 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -53,7 +53,7 @@ uuid = { version = "1.1.2", features = ["v4"] } waker-fn = "1.1.0" [build-dependencies] -bindgen = "0.59.2" +bindgen = "0.65.1" cc = "1.0.67" [dev-dependencies] diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d0388a9aedebbec4618fa02b22b335e2a659c5aa..882800f128abe8d41dd48e83a694b48d0020778b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -6335,9 +6335,9 @@ mod tests { #[crate::test(self)] async fn test_labeled_tasks(cx: &mut TestAppContext) { assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next())); - let (mut sender, mut reciever) = postage::oneshot::channel::<()>(); + let (mut sender, mut receiver) = postage::oneshot::channel::<()>(); let task = cx - .update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await })); + .update(|cx| cx.spawn_labeled("Test Label", |_| async move { receiver.recv().await })); assert_eq!( Some("Test Label"), diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 30d58cdd726e532599a743caa5aae3c01917340f..cfcef626dfa2a5baf36a230eebcd1e5ae2d23495 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -965,10 +965,10 @@ impl<'a> WindowContext<'a> { } pub fn rect_for_text_range(&self, range_utf16: Range) -> Option { - let root_view_id = self.window.root_view().id(); + let focused_view_id = self.window.focused_view_id?; self.window .rendered_views - .get(&root_view_id)? + .get(&focused_view_id)? .rect_for_text_range(range_utf16, self) .log_err() .flatten() diff --git a/crates/gpui/src/app/window_input_handler.rs b/crates/gpui/src/app/window_input_handler.rs index 938dbe1a0390c499575975bfdcda1bf8f2139609..8ee9f7eeff5c2a3e4f4d63ca55c9338f02e3ed69 100644 --- a/crates/gpui/src/app/window_input_handler.rs +++ b/crates/gpui/src/app/window_input_handler.rs @@ -84,8 +84,8 @@ impl InputHandler for WindowInputHandler { fn rect_for_range(&self, range_utf16: Range) -> Option { self.app - .borrow_mut() - .update_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) + .borrow() + .read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) .flatten() } } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9ccd57b2919727ec1c60e8af525d8b6de73e8f77..8344914da0d8f0124fd6ddc5cc1cbc29ab2e4d1d 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -36,7 +36,7 @@ struct StateInner { scroll_to: Option, } -pub struct LayoutState { +pub struct UniformListLayoutState { scroll_max: f32, item_height: f32, items: Vec>, @@ -152,7 +152,7 @@ impl UniformList { } impl Element for UniformList { - type LayoutState = LayoutState; + type LayoutState = UniformListLayoutState; type PaintState = (); fn layout( @@ -169,7 +169,7 @@ impl Element for UniformList { let no_items = ( constraint.min, - LayoutState { + UniformListLayoutState { item_height: 0., scroll_max: 0., items: Default::default(), @@ -263,7 +263,7 @@ impl Element for UniformList { ( size, - LayoutState { + UniformListLayoutState { item_height, scroll_max, items, diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index 0c088f9728977379ee0f8fcd80d5d9d1209f5d20..bc70638b2c077bf5aee53f9fedd22e96938d6b4b 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -67,7 +67,7 @@ impl KeymapMatcher { /// MatchResult::Pending => /// There exist bindings which are still waiting for more keys. /// MatchResult::Complete(matches) => - /// 1 or more bindings have recieved the necessary key presses. + /// 1 or more bindings have received the necessary key presses. /// The order of the matched actions is by position of the matching first, // and order in the keymap second. pub fn push_keystroke( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 93b50cf597cfd3e0ed18ac8e317fa5847740f1b1..e09ee48da630989774abdbf0bdffd386afa44a1b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -216,6 +216,11 @@ pub trait File: Send + Sync { /// of its worktree, then this method will return the name of the worktree itself. fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr; + /// Returns the id of the worktree to which this file belongs. + /// + /// This is needed for looking up project-specific settings. + fn worktree_id(&self) -> usize; + fn is_deleted(&self) -> bool; fn as_any(&self) -> &dyn Any; @@ -1802,8 +1807,7 @@ impl BufferSnapshot { } pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { - let language_name = self.language_at(position).map(|language| language.name()); - let settings = language_settings(language_name.as_deref(), cx); + let settings = language_settings(self.language_at(position), self.file(), cx); if settings.hard_tabs { IndentSize::tab() } else { @@ -2127,8 +2131,7 @@ impl BufferSnapshot { position: D, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language = self.language_at(position); - language_settings(language.map(|l| l.name()).as_deref(), cx) + language_settings(self.language_at(position), self.file.as_ref(), cx) } pub fn language_scope_at(&self, position: D) -> Option { @@ -2250,7 +2253,7 @@ impl BufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - self.outline_items_containing(0..self.len(), theme) + self.outline_items_containing(0..self.len(), true, theme) .map(Outline::new) } @@ -2262,6 +2265,7 @@ impl BufferSnapshot { let position = position.to_offset(self); let mut items = self.outline_items_containing( position.saturating_sub(1)..self.len().min(position + 1), + false, theme, )?; let mut prev_depth = None; @@ -2276,6 +2280,7 @@ impl BufferSnapshot { fn outline_items_containing( &self, range: Range, + include_extra_context: bool, theme: Option<&SyntaxTheme>, ) -> Option>> { let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { @@ -2310,7 +2315,10 @@ impl BufferSnapshot { let node_is_name; if capture.index == config.name_capture_ix { node_is_name = true; - } else if Some(capture.index) == config.context_capture_ix { + } else if Some(capture.index) == config.context_capture_ix + || (Some(capture.index) == config.extra_context_capture_ix + && include_extra_context) + { node_is_name = false; } else { continue; @@ -2337,10 +2345,12 @@ impl BufferSnapshot { buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end, true, ); + let mut last_buffer_range_end = 0; for (buffer_range, is_name) in buffer_ranges { - if !text.is_empty() { + if !text.is_empty() && buffer_range.start > last_buffer_range_end { text.push(' '); } + last_buffer_range_end = buffer_range.end; if is_name { let mut start = text.len(); let end = start + buffer_range.len(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index be573aa8956e3dc28e7074d17f85160fe5f2d1e9..9f44de40ac1f4010f7e335277c38e594650f0140 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { + let language = javascript_lang() + .with_outline_query( + r#" + (function_declaration + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context.extra + ")" @context.extra)) @item + "#, + ) + .unwrap(); + + let text = r#" + function a() {} + function b(c) {} + "# + .unindent(); + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx)); + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + + // extra context nodes are included in the outline. + let outline = snapshot.outline(None).unwrap(); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[("function a()", 0), ("function b( )", 0),] + ); + + // extra context nodes do not appear in breadcrumbs. + let symbols = snapshot.symbols_containing(3, None).unwrap(); + assert_eq!( + symbols + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[("function a", 0)] + ); +} + #[gpui::test] async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { let text = r#" diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8b4041b852bc6a2545e35209b156e94ac88050bb..0ff1d973d3ff47d7e67a82a7639ddcac810702eb 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -34,7 +34,7 @@ use std::{ fmt::Debug, hash::Hash, mem, - ops::Range, + ops::{Not, Range}, path::{Path, PathBuf}, str, sync::{ @@ -455,6 +455,7 @@ struct OutlineConfig { item_capture_ix: u32, name_capture_ix: u32, context_capture_ix: Option, + extra_context_capture_ix: Option, } struct InjectionConfig { @@ -500,6 +501,7 @@ struct AvailableLanguage { grammar: tree_sitter::Language, lsp_adapters: Vec>, get_queries: fn(&str) -> LanguageQueries, + loaded: bool, } pub struct LanguageRegistry { @@ -527,6 +529,7 @@ struct LanguageRegistryState { subscription: (watch::Sender<()>, watch::Receiver<()>), theme: Option>, version: usize, + reload_count: usize, } pub struct PendingLanguageServer { @@ -547,6 +550,7 @@ impl LanguageRegistry { subscription: watch::channel(), theme: Default::default(), version: 0, + reload_count: 0, }), language_server_download_dir: None, lsp_binary_statuses_tx, @@ -566,6 +570,14 @@ impl LanguageRegistry { self.executor = Some(executor); } + /// Clear out all of the loaded languages and reload them from scratch. + /// + /// This is useful in development, when queries have changed. + #[cfg(debug_assertions)] + pub fn reload(&self) { + self.state.write().reload(); + } + pub fn register( &self, path: &'static str, @@ -582,6 +594,7 @@ impl LanguageRegistry { grammar, lsp_adapters, get_queries, + loaded: false, }); } @@ -590,7 +603,7 @@ impl LanguageRegistry { let mut result = state .available_languages .iter() - .map(|l| l.config.name.to_string()) + .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string())) .chain(state.languages.iter().map(|l| l.config.name.to_string())) .collect::>(); result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); @@ -603,6 +616,7 @@ impl LanguageRegistry { state .available_languages .iter() + .filter(|l| !l.loaded) .flat_map(|l| l.lsp_adapters.clone()) .chain( state @@ -639,10 +653,17 @@ impl LanguageRegistry { self.state.read().subscription.1.clone() } + /// The number of times that the registry has been changed, + /// by adding languages or reloading. pub fn version(&self) -> usize { self.state.read().version } + /// The number of times that the registry has been reloaded. + pub fn reload_count(&self) -> usize { + self.state.read().reload_count + } + pub fn set_theme(&self, theme: Arc) { let mut state = self.state.write(); state.theme = Some(theme.clone()); @@ -721,7 +742,7 @@ impl LanguageRegistry { if let Some(language) = state .available_languages .iter() - .find(|l| callback(&l.config)) + .find(|l| !l.loaded && callback(&l.config)) .cloned() { let txs = state @@ -743,9 +764,7 @@ impl LanguageRegistry { let language = Arc::new(language); let mut state = this.state.write(); state.add(language.clone()); - state - .available_languages - .retain(|language| language.id != id); + state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { for tx in txs.drain(..) { let _ = tx.send(Ok(language.clone())); @@ -753,10 +772,9 @@ impl LanguageRegistry { } } Err(err) => { + log::error!("failed to load language {name} - {err}"); let mut state = this.state.write(); - state - .available_languages - .retain(|language| language.id != id); + state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { for tx in txs.drain(..) { let _ = tx.send(Err(anyhow!( @@ -905,6 +923,28 @@ impl LanguageRegistryState { self.version += 1; *self.subscription.0.borrow_mut() = (); } + + #[cfg(debug_assertions)] + fn reload(&mut self) { + self.languages.clear(); + self.version += 1; + self.reload_count += 1; + for language in &mut self.available_languages { + language.loaded = false; + } + *self.subscription.0.borrow_mut() = (); + } + + /// Mark the given language a having been loaded, so that the + /// language registry won't try to load it again. + fn mark_language_loaded(&mut self, id: AvailableLanguageId) { + for language in &mut self.available_languages { + if language.id == id { + language.loaded = true; + break; + } + } + } } #[cfg(any(test, feature = "test-support"))] @@ -1021,34 +1061,22 @@ impl Language { pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { - self = self - .with_highlights_query(query.as_ref()) - .expect("failed to evaluate highlights query"); + self = self.with_highlights_query(query.as_ref())?; } if let Some(query) = queries.brackets { - self = self - .with_brackets_query(query.as_ref()) - .expect("failed to load brackets query"); + self = self.with_brackets_query(query.as_ref())?; } if let Some(query) = queries.indents { - self = self - .with_indents_query(query.as_ref()) - .expect("failed to load indents query"); + self = self.with_indents_query(query.as_ref())?; } if let Some(query) = queries.outline { - self = self - .with_outline_query(query.as_ref()) - .expect("failed to load outline query"); + self = self.with_outline_query(query.as_ref())?; } if let Some(query) = queries.injections { - self = self - .with_injection_query(query.as_ref()) - .expect("failed to load injection query"); + self = self.with_injection_query(query.as_ref())?; } if let Some(query) = queries.overrides { - self = self - .with_override_query(query.as_ref()) - .expect("failed to load override query"); + self = self.with_override_query(query.as_ref())?; } Ok(self) } @@ -1064,12 +1092,14 @@ impl Language { let mut item_capture_ix = None; let mut name_capture_ix = None; let mut context_capture_ix = None; + let mut extra_context_capture_ix = None; get_capture_indices( &query, &mut [ ("item", &mut item_capture_ix), ("name", &mut name_capture_ix), ("context", &mut context_capture_ix), + ("context.extra", &mut extra_context_capture_ix), ], ); if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { @@ -1078,6 +1108,7 @@ impl Language { item_capture_ix, name_capture_ix, context_capture_ix, + extra_context_capture_ix, }); } Ok(self) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c98297c03648f7db9c307a592b4f7bf2dcfe279d..1a953b0bf21c0f3ed7d162a516657ce69cbb6a0b 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,3 +1,4 @@ +use crate::{File, Language}; use anyhow::Result; use collections::HashMap; use globset::GlobMatcher; @@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { - settings::get::(cx).language(language) +pub fn language_settings<'a>( + language: Option<&Arc>, + file: Option<&Arc>, + cx: &'a AppContext, +) -> &'a LanguageSettings { + let language_name = language.map(|l| l.name()); + all_language_settings(file, cx).language(language_name.as_deref()) } -pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { - settings::get::(cx) +pub fn all_language_settings<'a>( + file: Option<&Arc>, + cx: &'a AppContext, +) -> &'a AllLanguageSettings { + let location = file.map(|f| (f.worktree_id(), f.path().as_ref())); + settings::get_local(location, cx) } #[derive(Debug, Clone)] @@ -155,7 +165,7 @@ impl AllLanguageSettings { .any(|glob| glob.is_match(path)) } - pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool { + pub fn copilot_enabled(&self, language: Option<&Arc>, path: Option<&Path>) -> bool { if !self.copilot.feature_enabled { return false; } @@ -166,7 +176,8 @@ impl AllLanguageSettings { } } - self.language(language_name).show_copilot_suggestions + self.language(language.map(|l| l.name()).as_deref()) + .show_copilot_suggestions } } @@ -253,7 +264,7 @@ impl settings::Setting for AllLanguageSettings { let mut root_schema = generator.root_schema_for::(); // Create a schema for a 'languages overrides' object, associating editor - // settings with specific langauges. + // settings with specific languages. assert!(root_schema .definitions .contains_key("LanguageSettingsContent")); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index ce1196e946cacdaf6bef12db21c4dfcd3b4a157b..16313db49881070f6c0baac9b71b317761079830 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -773,7 +773,7 @@ impl<'a> SyntaxMapCaptures<'a> { } in layers { let grammar = match &language.grammar { - Some(grammer) => grammer, + Some(grammar) => grammar, None => continue, }; let query = match query(&grammar) { @@ -896,7 +896,7 @@ impl<'a> SyntaxMapMatches<'a> { } in layers { let grammar = match &language.grammar { - Some(grammer) => grammer, + Some(grammar) => grammar, None => continue, }; let query = match query(&grammar) { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 1b01660308cb88762c143cc226463b6f4e91f409..691203d5e8f51cb29249c4321814febb651ad0f9 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -260,9 +260,10 @@ impl LanguageServer { buffer.clear(); stdout.read_until(b'\n', &mut buffer).await?; stdout.read_until(b'\n', &mut buffer).await?; - let message_len: usize = std::str::from_utf8(&buffer)? + let header = std::str::from_utf8(&buffer)?; + let message_len: usize = header .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid header"))? + .ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))? .trim_end() .parse()?; @@ -301,7 +302,7 @@ impl LanguageServer { } } else { warn!( - "Failed to deserialize message:\n{}", + "failed to deserialize LSP message:\n{}", std::str::from_utf8(&buffer)? ); } diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 22a494d754ca09c4feef16fa3dc48c505dab48ff..3825897e7b5be2d871bac7cd4a778517191ed1eb 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -18,4 +18,4 @@ metal = "0.21.0" objc = "0.2" [build-dependencies] -bindgen = "0.59.2" +bindgen = "0.65.1" diff --git a/crates/plugin_macros/src/lib.rs b/crates/plugin_macros/src/lib.rs index 3f708658fd4f368088f58a837087ef89d722a9ba..2fe8b31b613b6fdf9206c39bac3d9083c3111a6e 100644 --- a/crates/plugin_macros/src/lib.rs +++ b/crates/plugin_macros/src/lib.rs @@ -11,7 +11,7 @@ use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Ty /// "Hello from Wasm".into() /// } /// ``` -/// This macro makes a function defined guest-side avaliable host-side. +/// This macro makes a function defined guest-side available host-side. /// Note that all arguments and return types must be `serde`. #[proc_macro_attribute] pub fn export(args: TokenStream, function: TokenStream) -> TokenStream { @@ -92,7 +92,7 @@ pub fn export(args: TokenStream, function: TokenStream) -> TokenStream { /// #[import] /// pub fn operating_system_name() -> String; /// ``` -/// This macro makes a function defined host-side avaliable guest-side. +/// This macro makes a function defined host-side available guest-side. /// Note that all arguments and return types must be `serde`. /// All that's provided is a signature, as the function is implemented host-side. #[proc_macro_attribute] diff --git a/crates/plugin_runtime/OPAQUE.md b/crates/plugin_runtime/OPAQUE.md index c99224edeb45d54e29deb75d77ac970c75839345..52ee75dbf9bab78811d0ad858d2036889d9a0517 100644 --- a/crates/plugin_runtime/OPAQUE.md +++ b/crates/plugin_runtime/OPAQUE.md @@ -127,7 +127,7 @@ use plugin_handles::RopeHandle; pub fn append(rope: RopeHandle, string: &str); ``` -This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only aquire resources to handles we're given, so we'd need to expose a fuction that takes a handle. +This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle. To illustrate that point, here's an example. First, we'd define a plugin-side function as follows: @@ -177,7 +177,7 @@ So here's what calling `append_newline` would do, from the top: 6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource. -Throughout this entire chain of calls, the resource remain host-side. By temporarilty checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource. +Throughout this entire chain of calls, the resource remain host-side. By temporarily checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource. ## Final Notes diff --git a/crates/plugin_runtime/src/plugin.rs b/crates/plugin_runtime/src/plugin.rs index bfcb024faa4514da0cc19186a38e0bc971c8d221..8070539b35da3ab2ec438c3886defe3e8cff8578 100644 --- a/crates/plugin_runtime/src/plugin.rs +++ b/crates/plugin_runtime/src/plugin.rs @@ -132,7 +132,7 @@ impl PluginBuilder { "env", &format!("__{}", name), move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { - // TODO: use try block once avaliable + // TODO: use try block once available let result: Result<(WasiBuffer, Memory, _), Trap> = (|| { // grab a handle to the memory let plugin_memory = match caller.get_export("memory") { @@ -211,7 +211,7 @@ impl PluginBuilder { "env", &format!("__{}", name), move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { - // TODO: use try block once avaliable + // TODO: use try block once available let result: Result<(WasiBuffer, Memory, Vec), Trap> = (|| { // grab a handle to the memory let plugin_memory = match caller.get_export("memory") { @@ -297,7 +297,7 @@ pub enum PluginBinary<'a> { Precompiled(&'a [u8]), } -/// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface. +/// Represents a WebAssembly plugin, with access to the WebAssembly System Interface. /// Build a new plugin using [`PluginBuilder`]. pub struct Plugin { store: Store, @@ -559,7 +559,7 @@ impl Plugin { .ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?; // write the argument to linear memory - // this returns a (ptr, lentgh) pair + // this returns a (ptr, length) pair let arg_buffer = Self::bytes_to_buffer( self.store.data().alloc_buffer(), &mut plugin_memory, @@ -569,7 +569,7 @@ impl Plugin { .await?; // call the function, passing in the buffer and its length - // this returns a ptr to a (ptr, lentgh) pair + // this returns a ptr to a (ptr, length) pair let result_buffer = handle .function .call_async(&mut self.store, arg_buffer.into_u64()) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ce7004dd317361a6cf751f9d860edce50a0d4311..2d4bed760d12029a26e0391b483ad690a065acd7 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1111,14 +1111,18 @@ impl LspCommand for GetHover { cx: AsyncAppContext, ) -> Result { Ok(message.and_then(|hover| { - let range = hover.range.map(|range| { - cx.read(|cx| { - let buffer = buffer.read(cx); - let token_start = - buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); - let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); - buffer.anchor_after(token_start)..buffer.anchor_before(token_end) - }) + let (language, range) = cx.read(|cx| { + let buffer = buffer.read(cx); + ( + buffer.language().cloned(), + hover.range.map(|range| { + let token_start = + buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); + let token_end = + buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); + buffer.anchor_after(token_start)..buffer.anchor_before(token_end) + }), + ) }); fn hover_blocks_from_marked_string( @@ -1163,7 +1167,11 @@ impl LspCommand for GetHover { }], }); - Some(Hover { contents, range }) + Some(Hover { + contents, + range, + language, + }) })) } @@ -1247,16 +1255,9 @@ impl LspCommand for GetHover { self, message: proto::GetHoverResponse, _: ModelHandle, - _: ModelHandle, - _: AsyncAppContext, + buffer: ModelHandle, + cx: AsyncAppContext, ) -> Result { - let range = if let (Some(start), Some(end)) = (message.start, message.end) { - language::proto::deserialize_anchor(start) - .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) - } else { - None - }; - let contents: Vec<_> = message .contents .into_iter() @@ -1271,12 +1272,23 @@ impl LspCommand for GetHover { }, }) .collect(); + if contents.is_empty() { + return Ok(None); + } - Ok(if contents.is_empty() { - None + let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned()); + let range = if let (Some(start), Some(end)) = (message.start, message.end) { + language::proto::deserialize_anchor(start) + .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) } else { - Some(Hover { contents, range }) - }) + None + }; + + Ok(Some(Hover { + contents, + range, + language, + })) } fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 { @@ -1499,7 +1511,11 @@ impl LspCommand for GetCodeActions { type ProtoRequest = proto::GetCodeActions; fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { - capabilities.code_action_provider.is_some() + match &capabilities.code_action_provider { + None => false, + Some(lsp::CodeActionProviderCapability::Simple(false)) => false, + _ => true, + } } fn to_lsp( @@ -1717,8 +1733,7 @@ impl LspCommand for OnTypeFormatting { .await?; let tab_size = buffer.read_with(&cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + language_settings(buffer.language(), buffer.file(), cx).tab_size }); Ok(Self { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ed30c0ecd496be76a3cfc7e26d8bf07da7a72f71..6c3f90beddb3fe8c09c2e30f8b74694be32bed3d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,7 +28,7 @@ use gpui::{ ModelHandle, Task, WeakModelHandle, }; use language::{ - language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, + language_settings::{language_settings, FormatOnSave, Formatter}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -72,7 +72,10 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use terminals::Terminals; -use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; +use util::{ + debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, + ResultExt, TryFutureExt as _, +}; pub use fs::*; pub use worktree::*; @@ -356,6 +359,7 @@ pub enum HoverBlockKind { pub struct Hover { pub contents: Vec, pub range: Option>, + pub language: Option>, } #[derive(Default)] @@ -460,6 +464,7 @@ impl Project { client.add_model_request_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_worktree); + client.add_model_message_handler(Self::handle_update_worktree_settings); client.add_model_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry); @@ -519,7 +524,7 @@ impl Project { _subscriptions: vec![ cx.observe_global::(Self::on_settings_changed) ], - _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), active_entry: None, languages, @@ -588,7 +593,7 @@ impl Project { active_entry: None, collaborators: Default::default(), join_project_response_message_id: response.message_id, - _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), languages, user_store: user_store.clone(), @@ -686,42 +691,37 @@ impl Project { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let settings = all_language_settings(cx); - let mut language_servers_to_start = Vec::new(); for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); - if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) - { - if settings - .language(Some(&language.name())) - .enable_language_server - { - let worktree = file.worktree.read(cx); - language_servers_to_start.push(( - worktree.id(), - worktree.as_local().unwrap().abs_path().clone(), - language.clone(), - )); + if let Some((file, language)) = buffer.file().zip(buffer.language()) { + let settings = language_settings(Some(language), Some(file), cx); + if settings.enable_language_server { + if let Some(file) = File::from_dyn(Some(file)) { + language_servers_to_start + .push((file.worktree.clone(), language.clone())); + } } } } } let mut language_servers_to_stop = Vec::new(); - for language in self.languages.to_vec() { - for lsp_adapter in language.lsp_adapters() { - if !settings - .language(Some(&language.name())) - .enable_language_server - { - let lsp_name = &lsp_adapter.name; - for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { - if lsp_name == started_lsp_name { - language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); - } - } + let languages = self.languages.to_vec(); + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { + let language = languages.iter().find(|l| { + l.lsp_adapters() + .iter() + .any(|adapter| &adapter.name == started_lsp_name) + }); + if let Some(language) = language { + let worktree = self.worktree_for_id(*worktree_id, cx); + let file = worktree.and_then(|tree| { + tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) + }); + if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { + language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } } } @@ -733,8 +733,9 @@ impl Project { } // Start all the newly-enabled language servers. - for (worktree_id, worktree_path, language) in language_servers_to_start { - self.start_language_servers(worktree_id, worktree_path, language, cx); + for (worktree, language) in language_servers_to_start { + let worktree_path = worktree.read(cx).abs_path(); + self.start_language_servers(&worktree, worktree_path, language, cx); } if !self.copilot_enabled && Copilot::global(cx).is_some() { @@ -1107,6 +1108,21 @@ impl Project { .log_err(); } + let store = cx.global::(); + for worktree in self.worktrees(cx) { + let worktree_id = worktree.read(cx).id().to_proto(); + for (path, content) in store.local_settings(worktree.id()) { + self.client + .send(proto::UpdateWorktreeSettings { + project_id, + worktree_id, + path: path.to_string_lossy().into(), + content: Some(content), + }) + .log_err(); + } + } + let (updates_tx, mut updates_rx) = mpsc::unbounded(); let client = self.client.clone(); self.client_state = Some(ProjectClientState::Local { @@ -1219,6 +1235,14 @@ impl Project { message_id: u32, cx: &mut ModelContext, ) -> Result<()> { + cx.update_global::(|store, cx| { + for worktree in &self.worktrees { + store + .clear_local_settings(worktree.handle_id(), cx) + .log_err(); + } + }); + self.join_project_response_message_id = message_id; self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_collaborators_from_proto(message.collaborators, cx)?; @@ -2215,13 +2239,34 @@ impl Project { } fn maintain_buffer_languages( - languages: &LanguageRegistry, + languages: Arc, cx: &mut ModelContext, ) -> Task<()> { let mut subscription = languages.subscribe(); + let mut prev_reload_count = languages.reload_count(); cx.spawn_weak(|project, mut cx| async move { while let Some(()) = subscription.next().await { if let Some(project) = project.upgrade(&cx) { + // If the language registry has been reloaded, then remove and + // re-assign the languages on all open buffers. + let reload_count = languages.reload_count(); + if reload_count > prev_reload_count { + prev_reload_count = reload_count; + project.update(&mut cx, |this, cx| { + let buffers = this + .opened_buffers + .values() + .filter_map(|b| b.upgrade(cx)) + .collect::>(); + for buffer in buffers { + if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() { + this.unregister_buffer_from_language_servers(&buffer, &f, cx); + buffer.update(cx, |buffer, cx| buffer.set_language(None, cx)); + } + } + }); + } + project.update(&mut cx, |project, cx| { let mut plain_text_buffers = Vec::new(); let mut buffers_with_unknown_injections = Vec::new(); @@ -2321,25 +2366,34 @@ impl Project { }); if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if let Some(worktree) = file.worktree.read(cx).as_local() { - let worktree_id = worktree.id(); - let worktree_abs_path = worktree.abs_path().clone(); - self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx); + let worktree = file.worktree.clone(); + if let Some(tree) = worktree.read(cx).as_local() { + self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); } } } fn start_language_servers( &mut self, - worktree_id: WorktreeId, + worktree: &ModelHandle, worktree_path: Arc, language: Arc, cx: &mut ModelContext, ) { - if !language_settings(Some(&language.name()), cx).enable_language_server { + if !language_settings( + Some(&language), + worktree + .update(cx, |tree, cx| tree.root_file(cx)) + .map(|f| f as _) + .as_ref(), + cx, + ) + .enable_language_server + { return; } + let worktree_id = worktree.read(cx).id(); for adapter in language.lsp_adapters() { let key = (worktree_id, adapter.name.clone()); if self.language_server_ids.contains_key(&key) { @@ -2748,23 +2802,22 @@ impl Project { buffers: impl IntoIterator>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = buffers + let language_server_lookup_info: HashSet<(ModelHandle, Arc)> = buffers .into_iter() .filter_map(|buffer| { let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; - let worktree = file.worktree.read(cx).as_local()?; let full_path = file.full_path(cx); let language = self .languages .language_for_file(&full_path, Some(buffer.as_rope())) .now_or_never()? .ok()?; - Some((worktree.id(), worktree.abs_path().clone(), language)) + Some((file.worktree.clone(), language)) }) .collect(); - for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { - self.restart_language_servers(worktree_id, worktree_abs_path, language, cx); + for (worktree, language) in language_server_lookup_info { + self.restart_language_servers(worktree, language, cx); } None @@ -2773,11 +2826,13 @@ impl Project { // TODO This will break in the case where the adapter's root paths and worktrees are not equal fn restart_language_servers( &mut self, - worktree_id: WorktreeId, - fallback_path: Arc, + worktree: ModelHandle, language: Arc, cx: &mut ModelContext, ) { + let worktree_id = worktree.read(cx).id(); + let fallback_path = worktree.read(cx).abs_path(); + let mut stops = Vec::new(); for adapter in language.lsp_adapters() { stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); @@ -2807,7 +2862,7 @@ impl Project { .map(|path_buf| Arc::from(path_buf.as_path())) .unwrap_or(fallback_path); - this.start_language_servers(worktree_id, root_path, language.clone(), cx); + this.start_language_servers(&worktree, root_path, language.clone(), cx); // Lookup new server ids and set them for each of the orphaned worktrees for adapter in language.lsp_adapters() { @@ -3432,8 +3487,7 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { let settings = buffer.read_with(&cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).clone() + language_settings(buffer.language(), buffer.file(), cx).clone() }); let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -4020,7 +4074,7 @@ impl Project { let end_within = range.start.cmp(&primary.end, buffer).is_le() && range.end.cmp(&primary.end, buffer).is_ge(); - //Skip addtional edits which overlap with the primary completion edit + //Skip additional edits which overlap with the primary completion edit //https://github.com/zed-industries/zed/pull/1871 if !start_within && !end_within { buffer.edit([(range, text)], None, cx); @@ -4463,11 +4517,14 @@ impl Project { push_to_history: bool, cx: &mut ModelContext, ) -> Task>> { - let tab_size = buffer.read_with(cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + let (position, tab_size) = buffer.read_with(cx, |buffer, cx| { + let position = position.to_point_utf16(buffer); + ( + position, + language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx) + .tab_size, + ) }); - let position = position.to_point_utf16(buffer.read(cx)); self.request_lsp( buffer.clone(), OnTypeFormatting { @@ -4873,6 +4930,7 @@ impl Project { worktree::Event::UpdatedEntries(changes) => { this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); + this.update_local_worktree_settings(&worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(updated_repos) => { this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) @@ -4893,8 +4951,12 @@ impl Project { .push(WorktreeHandle::Weak(worktree.downgrade())); } - cx.observe_release(worktree, |this, worktree, cx| { + let handle_id = worktree.id(); + cx.observe_release(worktree, move |this, worktree, cx| { let _ = this.remove_worktree(worktree.id(), cx); + cx.update_global::(|store, cx| { + store.clear_local_settings(handle_id, cx).log_err() + }); }) .detach(); @@ -5179,6 +5241,71 @@ impl Project { .detach(); } + fn update_local_worktree_settings( + &mut self, + worktree: &ModelHandle, + changes: &UpdatedEntriesSet, + cx: &mut ModelContext, + ) { + let project_id = self.remote_id(); + let worktree_id = worktree.id(); + let worktree = worktree.read(cx).as_local().unwrap(); + let remote_worktree_id = worktree.id(); + + let mut settings_contents = Vec::new(); + for (path, _, change) in changes.iter() { + if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { + let settings_dir = Arc::from( + path.ancestors() + .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count()) + .unwrap(), + ); + let fs = self.fs.clone(); + let removed = *change == PathChange::Removed; + let abs_path = worktree.absolutize(path); + settings_contents.push(async move { + (settings_dir, (!removed).then_some(fs.load(&abs_path).await)) + }); + } + } + + if settings_contents.is_empty() { + return; + } + + let client = self.client.clone(); + cx.spawn_weak(move |_, mut cx| async move { + let settings_contents: Vec<(Arc, _)> = + futures::future::join_all(settings_contents).await; + cx.update(|cx| { + cx.update_global::(|store, cx| { + for (directory, file_content) in settings_contents { + let file_content = file_content.and_then(|content| content.log_err()); + store + .set_local_settings( + worktree_id, + directory.clone(), + file_content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + if let Some(remote_id) = project_id { + client + .send(proto::UpdateWorktreeSettings { + project_id: remote_id, + worktree_id: remote_worktree_id.to_proto(), + path: directory.to_string_lossy().into_owned(), + content: file_content, + }) + .log_err(); + } + } + }); + }); + }) + .detach(); + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; @@ -5431,6 +5558,30 @@ impl Project { }) } + async fn handle_update_worktree_settings( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { + cx.update_global::(|store, cx| { + store + .set_local_settings( + worktree.id(), + PathBuf::from(&envelope.payload.path).into(), + envelope.payload.content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + }); + } + Ok(()) + }) + } + async fn handle_create_project_entry( this: ModelHandle, envelope: TypedEnvelope, @@ -6521,8 +6672,8 @@ impl Project { } self.metadata_changed(cx); - for (id, _) in old_worktrees_by_id { - cx.emit(Event::WorktreeRemoved(id)); + for id in old_worktrees_by_id.keys() { + cx.emit(Event::WorktreeRemoved(*id)); } Ok(()) @@ -6892,6 +7043,13 @@ impl WorktreeHandle { WorktreeHandle::Weak(handle) => handle.upgrade(cx), } } + + pub fn handle_id(&self) -> usize { + match self { + WorktreeHandle::Strong(handle) => handle.id(), + WorktreeHandle::Weak(handle) => handle.id(), + } + } } impl OpenBuffer { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 656fdaf25d17979de0338bc1bbe7508057455997..a67e38893b2457bcec814834c5f4d0142ecd6b12 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_managing_project_specific_settings( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/the-root", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + "b": { + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "b.rs": "fn b() {\n B\n}" + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); + + deterministic.run_until_parked(); + cx.read(|cx| { + let tree = worktree.read(cx); + + let settings_a = language_settings( + None, + Some( + &(File::for_entry( + tree.entry_for_path("a/a.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), + cx, + ); + let settings_b = language_settings( + None, + Some( + &(File::for_entry( + tree.entry_for_path("b/b.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), + cx, + ); + + assert_eq!(settings_a.tab_size.get(), 8); + assert_eq!(settings_b.tab_size.get(), 2); + }); +} + #[gpui::test] async fn test_managing_language_servers( deterministic: Arc, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index abaddd450e63a07c5af0cc50fb14ffe802752f5a..0fa22c7e6653b0e41a6da26358058645542fa96f 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -571,6 +571,11 @@ impl Worktree { Worktree::Remote(worktree) => worktree.abs_path.clone(), } } + + pub fn root_file(&self, cx: &mut ModelContext) -> Option> { + let entry = self.root_entry()?; + Some(File::for_entry(entry.clone(), cx.handle())) + } } impl LocalWorktree { @@ -578,14 +583,6 @@ impl LocalWorktree { path.starts_with(&self.abs_path) } - fn absolutize(&self, path: &Path) -> PathBuf { - if path.file_name().is_some() { - self.abs_path.join(path) - } else { - self.abs_path.to_path_buf() - } - } - pub(crate) fn load_buffer( &mut self, id: u64, @@ -1429,6 +1426,14 @@ impl Snapshot { &self.abs_path } + pub fn absolutize(&self, path: &Path) -> PathBuf { + if path.file_name().is_some() { + self.abs_path.join(path) + } else { + self.abs_path.to_path_buf() + } + } + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { self.entries_by_id.get(&entry_id, &()).is_some() } @@ -2360,6 +2365,10 @@ impl language::File for File { .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name)) } + fn worktree_id(&self) -> usize { + self.worktree.id() + } + fn is_deleted(&self) -> bool { self.is_deleted } @@ -2424,6 +2433,17 @@ impl language::LocalFile for File { } impl File { + pub fn for_entry(entry: Entry, worktree: ModelHandle) -> Arc { + Arc::new(Self { + worktree, + path: entry.path.clone(), + mtime: entry.mtime, + entry_id: entry.id, + is_local: true, + is_deleted: false, + }) + } + pub fn from_proto( proto: rpc::proto::File, worktree: ModelHandle, @@ -2485,7 +2505,7 @@ pub enum EntryKind { File(CharBag), } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum PathChange { /// A filesystem entry was was created. Added, @@ -3617,7 +3637,7 @@ pub trait WorktreeHandle { impl WorktreeHandle for ModelHandle { // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that - // occurred before the worktree was constructed. These events can cause the worktree to perfrom + // occurred before the worktree was constructed. These events can cause the worktree to perform // extra directory scans, and emit extra scan-state notifications. // // This function mutates the worktree's directory and waits for those mutations to be picked up, diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 71271d4e7c736e839fdc2fcc00ccc5678e46f741..0dc5dad3bf8d43edf3cb4c18507a7389ca65c2e1 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -276,7 +276,7 @@ mod tests { .await .unwrap(); - // Set up fake langauge server to return fuzzy matches against + // Set up fake language server to return fuzzy matches against // a fixed set of symbol names. let fake_symbols = [ symbol("one", "/external"), diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 797fb39317bd42596063406a85d4ee6f2d7bf37c..6b6f364fdb0b7395be9db6d516a46f1b40d042bd 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -179,7 +179,11 @@ impl Rope { } pub fn bytes_in_range(&self, range: Range) -> Bytes { - Bytes::new(self, range) + Bytes::new(self, range, false) + } + + pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, true) } pub fn chunks(&self) -> Chunks { @@ -579,22 +583,33 @@ impl<'a> Iterator for Chunks<'a> { pub struct Bytes<'a> { chunks: sum_tree::Cursor<'a, Chunk, usize>, range: Range, + reversed: bool, } impl<'a> Bytes<'a> { - pub fn new(rope: &'a Rope, range: Range) -> Self { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { let mut chunks = rope.chunks.cursor(); - chunks.seek(&range.start, Bias::Right, &()); - Self { chunks, range } + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } } pub fn peek(&self) -> Option<&'a [u8]> { let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } let chunk_start = *self.chunks.start(); if self.range.end <= chunk_start { return None; } - let start = self.range.start.saturating_sub(chunk_start); let end = self.range.end - chunk_start; Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)]) @@ -607,7 +622,11 @@ impl<'a> Iterator for Bytes<'a> { fn next(&mut self) -> Option { let result = self.peek(); if result.is_some() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } result } @@ -617,10 +636,21 @@ impl<'a> io::Read for Bytes<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if let Some(chunk) = self.peek() { let len = cmp::min(buf.len(), chunk.len()); - buf[..len].copy_from_slice(&chunk[..len]); - self.range.start += len; + if self.reversed { + buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); + buf[..len].reverse(); + self.range.end -= len; + } else { + buf[..len].copy_from_slice(&chunk[..len]); + self.range.start += len; + } + if len == chunk.len() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } Ok(len) } else { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 001d5038756ab49f0164d01b8a61ec5519d556d6..ce4dd7f7cf5514fa56aa4a62f1ed4553a9273b54 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -132,6 +132,8 @@ message Envelope { OnTypeFormatting on_type_formatting = 111; OnTypeFormattingResponse on_type_formatting_response = 112; + + UpdateWorktreeSettings update_worktree_settings = 113; } } @@ -339,6 +341,13 @@ message UpdateWorktree { string abs_path = 10; } +message UpdateWorktreeSettings { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; + optional string content = 4; +} + message CreateProjectEntry { uint64 project_id = 1; uint64 worktree_id = 2; @@ -467,7 +476,7 @@ message Symbol { string name = 4; int32 kind = 5; string path = 6; - // Cannot use generate anchors for unopend files, + // Cannot use generate anchors for unopened files, // so we are forced to use point coords instead PointUtf16 start = 7; PointUtf16 end = 8; diff --git a/crates/rpc/src/auth.rs b/crates/rpc/src/auth.rs index 39c8c39a4423de758807e67751a313e091e4f929..ac7bbcebecd98ebfb00a5dad4b92c16eddb3e628 100644 --- a/crates/rpc/src/auth.rs +++ b/crates/rpc/src/auth.rs @@ -42,7 +42,7 @@ impl PublicKey { } impl PrivateKey { - /// Decrypt a base64-encoded string that was encrypted by the correspoding public key. + /// Decrypt a base64-encoded string that was encrypted by the corresponding public key. pub fn decrypt_string(&self, encrypted_string: &str) -> Result { let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE) .context("failed to base64-decode encrypted string")?; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 07925a0486e6d210ceac44a7c5a5428b3f48a58e..13794ea64dad1446e579dd00806ccb4afda4758b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -236,6 +236,7 @@ messages!( (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), (UpdateWorktree, Foreground), + (UpdateWorktreeSettings, Foreground), (UpdateDiffBase, Foreground), (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), @@ -345,6 +346,7 @@ entity_messages!( UpdateProject, UpdateProjectCollaborator, UpdateWorktree, + UpdateWorktreeSettings, UpdateDiffBase ); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2c22517e2040c87f3573cfdc099dcb2cab2ee070..27aac1762bec9f37389e0cba1a47d4e5a11016e0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -25,7 +25,7 @@ use std::{ borrow::Cow, collections::HashSet, mem, - ops::Range, + ops::{Not, Range}, path::PathBuf, sync::Arc, }; @@ -242,7 +242,13 @@ impl View for ProjectSearchView { impl Item for ProjectSearchView { fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { - Some(self.query_editor.read(cx).text(cx).into()) + let query_text = self.query_editor.read(cx).text(cx); + + query_text + .is_empty() + .not() + .then(|| query_text.into()) + .or_else(|| Some("Project Search".into())) } fn act_as_type<'a>( diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 1ec0ff4a635551e5877f302375e41d0b8ed0fb62..f0396266fc78ff3ce515ce63350f98aa5bc9d2d6 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -12,7 +12,6 @@ doctest = false test-support = ["gpui/test-support", "fs/test-support"] [dependencies] -assets = { path = "../assets" } collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } @@ -25,6 +24,7 @@ futures.workspace = true json_comments = "0.2" lazy_static.workspace = true postage.workspace = true +rust-embed.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0b638da9242b8dbfdfe504cce5fa478a14c53fb0..e607a254bd70d066be2cae4efcb78d82d222a861 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,6 +1,5 @@ -use crate::settings_store::parse_json_with_comments; +use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{Context, Result}; -use assets::Assets; use collections::BTreeMap; use gpui::{keymap_matcher::Binding, AppContext}; use schemars::{ @@ -10,11 +9,11 @@ use schemars::{ }; use serde::Deserialize; use serde_json::{value::RawValue, Value}; -use util::ResultExt; +use util::{asset_str, ResultExt}; #[derive(Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] -pub struct KeymapFileContent(Vec); +pub struct KeymapFile(Vec); #[derive(Deserialize, Default, Clone, JsonSchema)] pub struct KeymapBlock { @@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction { #[derive(Deserialize)] struct ActionWithData(Box, Box); -impl KeymapFileContent { +impl KeymapFile { pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { - let content = Assets::get(asset_path).unwrap().data; - let content_str = std::str::from_utf8(content.as_ref()).unwrap(); - Self::parse(content_str)?.add_to_cx(cx) + let content = asset_str::(asset_path); + Self::parse(content.as_ref())?.add_to_cx(cx) } pub fn parse(content: &str) -> Result { @@ -83,40 +81,40 @@ impl KeymapFileContent { } Ok(()) } -} -pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value { - let mut root_schema = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) - .into_generator() - .into_root_schema_for::(); + pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value { + let mut root_schema = SchemaSettings::draft07() + .with(|settings| settings.option_add_null_type = false) + .into_generator() + .into_root_schema_for::(); - let action_schema = Schema::Object(SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(vec![ - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), - enum_values: Some( - action_names - .iter() - .map(|name| Value::String(name.to_string())) - .collect(), - ), - ..Default::default() - }), - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), - ..Default::default() - }), - ]), + let action_schema = Schema::Object(SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![ + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + enum_values: Some( + action_names + .iter() + .map(|name| Value::String(name.to_string())) + .collect(), + ), + ..Default::default() + }), + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), + ..Default::default() + }), + ]), + ..Default::default() + })), ..Default::default() - })), - ..Default::default() - }); + }); - root_schema - .definitions - .insert("KeymapAction".to_owned(), action_schema); + root_schema + .definitions + .insert("KeymapAction".to_owned(), action_schema); - serde_json::to_value(root_schema).unwrap() + serde_json::to_value(root_schema).unwrap() + } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 840797c6ad644cc851c298712e7db080a0650d78..8c3587d942d438895f21c707086f804d06b4e3f0 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -2,18 +2,37 @@ mod keymap_file; mod settings_file; mod settings_store; -use gpui::AssetSource; -pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; +use rust_embed::RustEmbed; +use std::{borrow::Cow, str}; +use util::asset_str; + +pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore}; -use std::{borrow::Cow, str}; -pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json"; -pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json"; +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "settings/*"] +#[include = "keymaps/*"] +#[exclude = "*.DS_Store"] +pub struct SettingsAssets; + +pub fn default_settings() -> Cow<'static, str> { + asset_str::("settings/default.json") +} + +pub fn default_keymap() -> Cow<'static, str> { + asset_str::("keymaps/default.json") +} + +pub fn vim_keymap() -> Cow<'static, str> { + asset_str::("keymaps/vim.json") +} + +pub fn initial_user_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_user_settings.json") +} -pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> { - match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } +pub fn initial_local_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_local_settings.json") } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index cca2909da22dba93842304b87c600e2b05a99ae4..c7ae296469c3e32b6e3234540c98bb511a4de663 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,10 +1,15 @@ -use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH}; +use crate::{settings_store::SettingsStore, Setting}; use anyhow::Result; -use assets::Assets; use fs::Fs; use futures::{channel::mpsc, StreamExt}; -use gpui::{executor::Background, AppContext, AssetSource}; -use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration}; +use gpui::{executor::Background, AppContext}; +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, + str, + sync::Arc, + time::Duration, +}; use util::{paths, ResultExt}; pub fn register(cx: &mut AppContext) { @@ -17,11 +22,8 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T { cx.global::().get(None) } -pub fn default_settings() -> Cow<'static, str> { - match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } +pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T { + cx.global::().get(location) } pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; @@ -29,7 +31,7 @@ pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; #[cfg(any(test, feature = "test-support"))] pub fn test_settings() -> String { let mut value = crate::settings_store::parse_json_with_comments::( - default_settings().as_ref(), + crate::default_settings().as_ref(), ) .unwrap(); util::merge_non_null_json_value_into( @@ -55,15 +57,22 @@ pub fn watch_config_file( .spawn(async move { let events = fs.watch(&path, Duration::from_millis(100)).await; futures::pin_mut!(events); + + let contents = fs.load(&path).await.unwrap_or_default(); + if tx.unbounded_send(contents).is_err() { + return; + } + loop { + if events.next().await.is_none() { + break; + } + if let Ok(contents) = fs.load(&path).await { if !tx.unbounded_send(contents).is_ok() { break; } } - if events.next().await.is_none() { - break; - } } }) .detach(); @@ -101,7 +110,7 @@ async fn load_settings(fs: &Arc) -> Result { Err(err) => { if let Some(e) = err.downcast_ref::() { if e.kind() == ErrorKind::NotFound { - return Ok(crate::initial_user_settings_content(&Assets).to_string()); + return Ok(crate::initial_user_settings_content().to_string()); } } return Err(err); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 71b3cc635f4e03465d94cb498567c21bd36bd76a..1133cb597067e70472ec6267ffb9575751efe36a 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; use gpui::AppContext; use lazy_static::lazy_static; @@ -84,19 +84,30 @@ pub struct SettingsJsonSchemaParams<'a> { } /// A set of strongly-typed setting values defined via multiple JSON files. -#[derive(Default)] pub struct SettingsStore { setting_values: HashMap>, - default_deserialized_settings: Option, - user_deserialized_settings: Option, - local_deserialized_settings: BTreeMap, serde_json::Value>, + default_deserialized_settings: serde_json::Value, + user_deserialized_settings: serde_json::Value, + local_deserialized_settings: BTreeMap<(usize, Arc), serde_json::Value>, tab_size_callback: Option<(TypeId, Box Option>)>, } +impl Default for SettingsStore { + fn default() -> Self { + SettingsStore { + setting_values: Default::default(), + default_deserialized_settings: serde_json::json!({}), + user_deserialized_settings: serde_json::json!({}), + local_deserialized_settings: Default::default(), + tab_size_callback: Default::default(), + } + } +} + #[derive(Debug)] struct SettingValue { global_value: Option, - local_values: Vec<(Arc, T)>, + local_values: Vec<(usize, Arc, T)>, } trait AnySettingValue { @@ -109,9 +120,9 @@ trait AnySettingValue { custom: &[DeserializedSetting], cx: &AppContext, ) -> Result>; - fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any; fn set_global_value(&mut self, value: Box); - fn set_local_value(&mut self, path: Arc, value: Box); + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box); fn json_schema( &self, generator: &mut SchemaGenerator, @@ -136,27 +147,24 @@ impl SettingsStore { local_values: Vec::new(), })); - if let Some(default_settings) = &self.default_deserialized_settings { - if let Some(default_settings) = setting_value - .deserialize_setting(default_settings) + if let Some(default_settings) = setting_value + .deserialize_setting(&self.default_deserialized_settings) + .log_err() + { + let mut user_values_stack = Vec::new(); + + if let Some(user_settings) = setting_value + .deserialize_setting(&self.user_deserialized_settings) .log_err() { - let mut user_values_stack = Vec::new(); - - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_settings) = - setting_value.deserialize_setting(user_settings).log_err() - { - user_values_stack = vec![user_settings]; - } - } + user_values_stack = vec![user_settings]; + } - if let Some(setting) = setting_value - .load_setting(&default_settings, &user_values_stack, cx) - .log_err() - { - setting_value.set_global_value(setting); - } + if let Some(setting) = setting_value + .load_setting(&default_settings, &user_values_stack, cx) + .log_err() + { + setting_value.set_global_value(setting); } } } @@ -165,7 +173,7 @@ impl SettingsStore { /// /// Panics if the given setting type has not been registered, or if there is no /// value for this setting. - pub fn get(&self, path: Option<&Path>) -> &T { + pub fn get(&self, path: Option<(usize, &Path)>) -> &T { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) @@ -189,9 +197,7 @@ impl SettingsStore { /// This is only for debugging and reporting. For user-facing functionality, /// use the typed setting interface. pub fn untyped_user_settings(&self) -> &serde_json::Value { - self.user_deserialized_settings - .as_ref() - .unwrap_or(&serde_json::Value::Null) + &self.user_deserialized_settings } #[cfg(any(test, feature = "test-support"))] @@ -213,11 +219,7 @@ impl SettingsStore { cx: &AppContext, update: impl FnOnce(&mut T::FileContent), ) { - if self.user_deserialized_settings.is_none() { - self.set_user_settings("{}", cx).unwrap(); - } - let old_text = - serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap(); + let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap(); let new_text = self.new_text_for_update::(old_text, update); self.set_user_settings(&new_text, cx).unwrap(); } @@ -250,11 +252,7 @@ impl SettingsStore { .setting_values .get(&setting_type_id) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .deserialize_setting( - self.user_deserialized_settings - .as_ref() - .expect("no user settings loaded"), - ) + .deserialize_setting(&self.user_deserialized_settings) .unwrap_or_else(|e| { panic!( "could not deserialize setting type {} from user settings: {}", @@ -323,10 +321,14 @@ impl SettingsStore { default_settings_content: &str, cx: &AppContext, ) -> Result<()> { - self.default_deserialized_settings = - Some(parse_json_with_comments(default_settings_content)?); - self.recompute_values(None, cx)?; - Ok(()) + let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?; + if settings.is_object() { + self.default_deserialized_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } } /// Set the user settings via a JSON string. @@ -335,28 +337,49 @@ impl SettingsStore { user_settings_content: &str, cx: &AppContext, ) -> Result<()> { - self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?); - self.recompute_values(None, cx)?; - Ok(()) + let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?; + if settings.is_object() { + self.user_deserialized_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } } /// Add or remove a set of local settings via a JSON string. pub fn set_local_settings( &mut self, + root_id: usize, path: Arc, settings_content: Option<&str>, cx: &AppContext, ) -> Result<()> { if let Some(content) = settings_content { self.local_deserialized_settings - .insert(path.clone(), parse_json_with_comments(content)?); + .insert((root_id, path.clone()), parse_json_with_comments(content)?); } else { - self.local_deserialized_settings.remove(&path); + self.local_deserialized_settings + .remove(&(root_id, path.clone())); } - self.recompute_values(Some(&path), cx)?; + self.recompute_values(Some((root_id, &path)), cx)?; Ok(()) } + /// Add or remove a set of local settings via a JSON string. + pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> { + self.local_deserialized_settings + .retain(|k, _| k.0 != root_id); + self.recompute_values(Some((root_id, "".as_ref())), cx)?; + Ok(()) + } + + pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator, String)> { + self.local_deserialized_settings + .range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into())) + .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap())) + } + pub fn json_schema( &self, schema_params: &SettingsJsonSchemaParams, @@ -436,72 +459,70 @@ impl SettingsStore { fn recompute_values( &mut self, - changed_local_path: Option<&Path>, + changed_local_path: Option<(usize, &Path)>, cx: &AppContext, ) -> Result<()> { // Reload the global and local values for every setting. let mut user_settings_stack = Vec::::new(); - let mut paths_stack = Vec::>::new(); + let mut paths_stack = Vec::>::new(); for setting_value in self.setting_values.values_mut() { - if let Some(default_settings) = &self.default_deserialized_settings { - let default_settings = setting_value.deserialize_setting(default_settings)?; + let default_settings = + setting_value.deserialize_setting(&self.default_deserialized_settings)?; - user_settings_stack.clear(); - paths_stack.clear(); + user_settings_stack.clear(); + paths_stack.clear(); - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_settings) = - setting_value.deserialize_setting(user_settings).log_err() - { - user_settings_stack.push(user_settings); - paths_stack.push(None); - } + if let Some(user_settings) = setting_value + .deserialize_setting(&self.user_deserialized_settings) + .log_err() + { + user_settings_stack.push(user_settings); + paths_stack.push(None); + } + + // If the global settings file changed, reload the global value for the field. + if changed_local_path.is_none() { + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() + { + setting_value.set_global_value(value); } + } - // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() { - if let Some(value) = setting_value - .load_setting(&default_settings, &user_settings_stack, cx) - .log_err() - { - setting_value.set_global_value(value); + // Reload the local values for the setting. + for ((root_id, path), local_settings) in &self.local_deserialized_settings { + // Build a stack of all of the local values for that setting. + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !path.starts_with(prev_path) { + paths_stack.pop(); + user_settings_stack.pop(); + continue; + } } + break; } - // Reload the local values for the setting. - for (path, local_settings) in &self.local_deserialized_settings { - // Build a stack of all of the local values for that setting. - while let Some(prev_path) = paths_stack.last() { - if let Some(prev_path) = prev_path { - if !path.starts_with(prev_path) { - paths_stack.pop(); - user_settings_stack.pop(); - continue; - } - } - break; + if let Some(local_settings) = + setting_value.deserialize_setting(&local_settings).log_err() + { + paths_stack.push(Some((*root_id, path.as_ref()))); + user_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| { + *root_id != changed_root_id || !path.starts_with(changed_local_path) + }) { + continue; } - if let Some(local_settings) = - setting_value.deserialize_setting(&local_settings).log_err() + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() { - paths_stack.push(Some(path.as_ref())); - user_settings_stack.push(local_settings); - - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.map_or(false, |changed_local_path| { - !path.starts_with(changed_local_path) - }) { - continue; - } - - if let Some(value) = setting_value - .load_setting(&default_settings, &user_settings_stack, cx) - .log_err() - { - setting_value.set_local_value(path.clone(), value); - } + setting_value.set_local_value(*root_id, path.clone(), value); } } } @@ -510,6 +531,24 @@ impl SettingsStore { } } +impl Debug for SettingsStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SettingsStore") + .field( + "types", + &self + .setting_values + .values() + .map(|value| value.setting_type_name()) + .collect::>(), + ) + .field("default_settings", &self.default_deserialized_settings) + .field("user_settings", &self.user_deserialized_settings) + .field("local_settings", &self.local_deserialized_settings) + .finish_non_exhaustive() + } +} + impl AnySettingValue for SettingValue { fn key(&self) -> Option<&'static str> { T::KEY @@ -546,10 +585,10 @@ impl AnySettingValue for SettingValue { Ok(DeserializedSetting(Box::new(value))) } - fn value_for_path(&self, path: Option<&Path>) -> &dyn Any { - if let Some(path) = path { - for (settings_path, value) in self.local_values.iter().rev() { - if path.starts_with(&settings_path) { + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any { + if let Some((root_id, path)) = path { + for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { + if root_id == *settings_root_id && path.starts_with(&settings_path) { return value; } } @@ -563,11 +602,14 @@ impl AnySettingValue for SettingValue { self.global_value = Some(*value.downcast().unwrap()); } - fn set_local_value(&mut self, path: Arc, value: Box) { + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box) { let value = *value.downcast().unwrap(); - match self.local_values.binary_search_by_key(&&path, |e| &e.0) { - Ok(ix) => self.local_values[ix].1 = value, - Err(ix) => self.local_values.insert(ix, (path, value)), + match self + .local_values + .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) + { + Ok(ix) => self.local_values[ix].2 = value, + Err(ix) => self.local_values.insert(ix, (root_id, path, value)), } } @@ -581,22 +623,6 @@ impl AnySettingValue for SettingValue { } } -// impl Debug for SettingsStore { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// return f -// .debug_struct("SettingsStore") -// .field( -// "setting_value_sets_by_type", -// &self -// .setting_values -// .values() -// .map(|set| (set.setting_type_name(), set)) -// .collect::>(), -// ) -// .finish_non_exhaustive(); -// } -// } - fn update_value_in_json_text<'a>( text: &mut String, key_path: &mut Vec<&'a str>, @@ -639,6 +665,10 @@ fn update_value_in_json_text<'a>( key_path.pop(); } } else if old_value != new_value { + let mut new_value = new_value.clone(); + if let Some(new_object) = new_value.as_object_mut() { + new_object.retain(|_, v| !v.is_null()); + } let (range, replacement) = replace_value_in_json_text(text, &key_path, tab_size, &new_value); text.replace_range(range.clone(), &replacement); @@ -650,7 +680,7 @@ fn replace_value_in_json_text( text: &str, key_path: &[&str], tab_size: usize, - new_value: impl Serialize, + new_value: &serde_json::Value, ) -> (Range, String) { const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; const LANGUAGES: &'static str = "languages"; @@ -884,6 +914,7 @@ mod tests { store .set_local_settings( + 1, Path::new("/root1").into(), Some(r#"{ "user": { "staff": true } }"#), cx, @@ -891,6 +922,7 @@ mod tests { .unwrap(); store .set_local_settings( + 1, Path::new("/root1/subdir").into(), Some(r#"{ "user": { "name": "Jane Doe" } }"#), cx, @@ -899,6 +931,7 @@ mod tests { store .set_local_settings( + 1, Path::new("/root2").into(), Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), cx, @@ -906,7 +939,7 @@ mod tests { .unwrap(); assert_eq!( - store.get::(Some(Path::new("/root1/something"))), + store.get::(Some((1, Path::new("/root1/something")))), &UserSettings { name: "John Doe".to_string(), age: 31, @@ -914,7 +947,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root1/subdir/something"))), + store.get::(Some((1, Path::new("/root1/subdir/something")))), &UserSettings { name: "Jane Doe".to_string(), age: 31, @@ -922,7 +955,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &UserSettings { name: "John Doe".to_string(), age: 42, @@ -930,7 +963,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &MultiKeySettings { key1: "a".to_string(), key2: "b".to_string(), @@ -994,24 +1027,32 @@ mod tests { r#"{ "languages": { "JSON": { - "is_enabled": true + "language_setting_1": true } } }"# .unindent(), |settings| { - settings.languages.get_mut("JSON").unwrap().is_enabled = false; settings .languages - .insert("Rust".into(), LanguageSettingEntry { is_enabled: true }); + .get_mut("JSON") + .unwrap() + .language_setting_1 = Some(false); + settings.languages.insert( + "Rust".into(), + LanguageSettingEntry { + language_setting_2: Some(true), + ..Default::default() + }, + ); }, r#"{ "languages": { "Rust": { - "is_enabled": true + "language_setting_2": true }, "JSON": { - "is_enabled": false + "language_setting_1": false } } }"# @@ -1074,6 +1115,23 @@ mod tests { .unindent(), cx, ); + + check_settings_update::( + &mut store, + r#"{ + } + "# + .unindent(), + |settings| settings.age = Some(37), + r#"{ + "user": { + "age": 37 + } + } + "# + .unindent(), + cx, + ); } fn check_settings_update( @@ -1202,9 +1260,10 @@ mod tests { languages: HashMap, } - #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] struct LanguageSettingEntry { - is_enabled: bool, + language_setting_1: Option, + language_setting_2: Option, } impl Setting for LanguageSettings { diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index c3fe4657ee9182dbf1c229e32b19ebf3c30c0ec7..ffadb3af41d165737af55538661ec7100e62daeb 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -160,7 +160,7 @@ impl ThreadSafeConnection { // Create a one shot channel for the result of the queued write // so we can await on the result - let (sender, reciever) = oneshot::channel(); + let (sender, receiver) = oneshot::channel(); let thread_safe_connection = (*self).clone(); write_channel(Box::new(move || { @@ -168,7 +168,7 @@ impl ThreadSafeConnection { let result = connection.with_write(|connection| callback(connection)); sender.send(result).ok(); })); - reciever.map(|response| response.expect("Write queue unexpectedly closed")) + receiver.map(|response| response.expect("Write queue unexpectedly closed")) } pub(crate) fn create_connection( @@ -245,10 +245,10 @@ pub fn background_thread_queue() -> WriteQueueConstructor { use std::sync::mpsc::channel; Box::new(|| { - let (sender, reciever) = channel::(); + let (sender, receiver) = channel::(); thread::spawn(move || { - while let Ok(write) = reciever.recv() { + while let Ok(write) = receiver.recv() { write() } }); diff --git a/crates/terminal/src/mappings/colors.rs b/crates/terminal/src/mappings/colors.rs index 5bed4b573c4b97ada6f6c0c3a5ef649c1d1a7e92..3f776251b52693893f58eb6868a53ad995f5ab84 100644 --- a/crates/terminal/src/mappings/colors.rs +++ b/crates/terminal/src/mappings/colors.rs @@ -45,7 +45,7 @@ pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { } ///Converts an 8 bit ANSI color to it's GPUI equivalent. -///Accepts usize for compatability with the alacritty::Colors interface, +///Accepts usize for compatibility with the alacritty::Colors interface, ///Other than that use case, should only be called with values in the [0,255] range pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { match index { @@ -78,7 +78,7 @@ pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale } - //For compatability with the alacritty::Colors interface + //For compatibility with the alacritty::Colors interface 256 => style.foreground, 257 => style.background, 258 => style.cursor, diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 6fa920d739382f4a3f8ccebe8f5b601bce3e4ee0..85de173604db82610a8cf1191771d920fa883583 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -37,8 +37,6 @@ lazy_static.workspace = true serde.workspace = true serde_derive.workspace = true - - [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/terminal_view/README.md b/crates/terminal_view/README.md index 272212a538a06766d254b6bbd675643af62e1175..019460067ebf20f991b18a9cd5ce262ae7675504 100644 --- a/crates/terminal_view/README.md +++ b/crates/terminal_view/README.md @@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal: 3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`. -4. Pasted text has a seperate pathway. +4. Pasted text has a separate pathway. Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal \ No newline at end of file diff --git a/crates/terminal_view/scripts/print256color.sh b/crates/terminal_view/scripts/print256color.sh index 99e3d8c9f9d3ae7d37e006fe9af328f0c7ea849c..8a53f3bc025842d900b5b4797eefc6d8a0946120 100755 --- a/crates/terminal_view/scripts/print256color.sh +++ b/crates/terminal_view/scripts/print256color.sh @@ -40,7 +40,7 @@ function contrast_colour { # Uncomment the below for more precise luminance calculations - # # Calculate percieved brightness + # # Calculate perceived brightness # # See https://www.w3.org/TR/AERT#color-contrast # # and http://www.itu.int/rec/R-REC-BT.601 # # Luminance is in range 0..5000 as each value is 0..5 diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 18c85db980ba14aab27ad91254ee022db57b1a0f..2f2ff2cdc3bde8bb5ee1c4a32978ed28518bd94f 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -34,7 +34,7 @@ use std::{mem, ops::Range}; use crate::TerminalView; -///The information generated during layout that is nescessary for painting +///The information generated during layout that is necessary for painting pub struct LayoutState { cells: Vec, rects: Vec, @@ -206,7 +206,7 @@ impl TerminalElement { //Expand background rect range { if matches!(bg, Named(NamedColor::Background)) { - //Continue to next cell, resetting variables if nescessary + //Continue to next cell, resetting variables if necessary cur_alac_color = None; if let Some(rect) = cur_rect { rects.push(rect); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index e45459e68363169b1bd0b3f2cc8b44621e078271..ac3875af9e10704be06a7a2a047f86ec1fe992b6 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -70,6 +70,7 @@ impl TerminalPanel { .with_child(Pane::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(( "New Terminal".into(), Some(Box::new(workspace::NewTerminal)), @@ -94,6 +95,7 @@ impl TerminalPanel { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7f43f99ebd43727ea3284e179c70da49accfbff1..c40a1a7ccd3d493e16e6a4b03a2d36e21a0b0c14 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -133,8 +133,8 @@ impl TerminalView { Event::Wakeup => { if !cx.is_self_focused() { this.has_new_content = true; - cx.notify(); } + cx.notify(); cx.emit(Event::Wakeup); } Event::Bell => { @@ -804,7 +804,7 @@ mod tests { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); - //Make sure enviroment is as expeted + //Make sure environment is as expected assert!(active_entry.is_none()); assert!(workspace.worktrees(cx).next().is_none()); @@ -825,7 +825,7 @@ mod tests { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); - //Make sure enviroment is as expeted + //Make sure environment is as expected assert!(active_entry.is_none()); assert!(workspace.worktrees(cx).next().is_some()); @@ -905,7 +905,10 @@ mod tests { cx: &mut TestAppContext, ) -> (ModelHandle, ViewHandle) { let params = cx.update(AppState::test); - cx.update(|cx| theme::init((), cx)); + cx.update(|cx| { + theme::init((), cx); + language::init(cx); + }); let project = Project::test(params.fs.clone(), [], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 4b8266120a3682e118c497c3d4ba3cea7cd9e2df..7e26e0a29649681346ce6148a71f7dc1058467da 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -193,7 +193,7 @@ fn test_line_len() { } #[test] -fn test_common_prefix_at_positionn() { +fn test_common_prefix_at_position() { let text = "a = str; b = δα"; let buffer = Buffer::new(0, 0, text.into()); @@ -216,7 +216,7 @@ fn test_common_prefix_at_positionn() { empty_range_after(text, "str"), ); - // prefix matching is case insenstive. + // prefix matching is case insensitive. assert_eq!( buffer.common_prefix_at(offset1, "Strαngε"), range_of(text, "str"), diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index dcfaf818d1f97f98002d60fecf90533d50c2969b..2693add8ed0ac0d9d032ad5bd3f5e540138cf2fe 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1749,6 +1749,12 @@ impl BufferSnapshot { self.visible_text.bytes_in_range(start..end) } + pub fn reversed_bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.reversed_bytes_in_range(start..end) + } + pub fn text_for_range(&self, range: Range) -> Chunks<'_> { let start = range.start.to_offset(self); let end = range.end.to_offset(self); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 21c01150a852b7251ac6968641ad915d4124d399..f7df63ca099d9a50cf39c565d9cb658aafe098a1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -60,6 +60,7 @@ pub struct Theme { pub incoming_call_notification: IncomingCallNotification, pub tooltip: TooltipStyle, pub terminal: TerminalStyle, + pub assistant: AssistantStyle, pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, pub color_scheme: ColorScheme, @@ -968,6 +969,23 @@ pub struct TerminalStyle { pub dim_foreground: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct AssistantStyle { + pub container: ContainerStyle, + pub header: ContainerStyle, + pub sent_at: ContainedText, + pub user_sender: Interactive, + pub assistant_sender: Interactive, + pub system_sender: Interactive, + pub model_info_container: ContainerStyle, + pub model: Interactive, + pub remaining_tokens: ContainedText, + pub no_remaining_tokens: ContainedText, + pub error_icon: Icon, + pub api_key_editor: FieldEditor, + pub api_key_prompt: ContainedText, +} + #[derive(Clone, Deserialize, Default)] pub struct FeedbackStyle { pub submit_button: Interactive, diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 6216d2e47201d7d980fe3d59de1c29fcd82c095d..8d9594fbeb8783104ac5e165173c7fac50c9e0dc 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -21,6 +21,7 @@ isahc.workspace = true smol.workspace = true url = "2.2" rand.workspace = true +rust-embed.workspace = true tempdir = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f998fc319fcb7bcdfc6be70c66b482ec247a93b0..e3397a1557cfeed520e4c56fa38c40e4539d71a9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -15,6 +15,7 @@ lazy_static::lazy_static! { pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); + pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); } pub mod legacy { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index a182770dce96afdc10835cef8cd01683bf7a6649..71ffacebf34e2474bcb1c394dada124827c53477 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -7,8 +7,10 @@ pub mod paths; pub mod test; use std::{ + borrow::Cow, cmp::{self, Ordering}, ops::{AddAssign, Range, RangeInclusive}, + panic::Location, pin::Pin, task::{Context, Poll}, }; @@ -129,11 +131,13 @@ where { type Ok = T; + #[track_caller] fn log_err(self) -> Option { match self { Ok(value) => Some(value), Err(error) => { - log::error!("{:?}", error); + let caller = Location::caller(); + log::error!("{}:{}: {:?}", caller.file(), caller.line(), error); None } } @@ -281,6 +285,14 @@ impl Iterator for RandomCharIter { } } +/// Get an embedded file as a string. +pub fn asset_str(path: &str) -> Cow<'static, str> { + match A::get(path).unwrap().data { + Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()), + Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()), + } +} + // copy unstable standard feature option unzip // https://github.com/rust-lang/rust/issues/87800 // Remove when this ship in Rust 1.66 or 1.67 diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index ee3144fd566ba4fae33a4333f159c64b6140595a..57d38213798352bf8fad6970306691182e6fb1fb 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -24,7 +24,6 @@ nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", f tokio = { version = "1.15", "optional" = true } serde_json.workspace = true -assets = { path = "../assets" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } editor = { path = "../editor" } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 83a18a1ac9f585fe80e657752983b120b021dfd5..1f90d259d3e73801af58621ee4e80d925647e489 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -400,7 +400,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext VimTestContext<'a> { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); - settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap(); + settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); }); // Setup search toolbars and keypress hook diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index b22607e20dec0ac9f285c9a66f5df638c5a66809..8606be4944830f9859863f1510ffe3413631b31a 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -19,7 +19,6 @@ test-support = [ ] [dependencies] -assets = { path = "../assets" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 886afe943d0c6ff57c8c2f9fb5c385b8c09f7451..48f486381dd223340becb30eb1c594e5a1f55bb4 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -175,6 +175,10 @@ impl Dock { } } + pub fn position(&self) -> DockPosition { + self.position + } + pub fn is_open(&self) -> bool { self.is_open } @@ -184,6 +188,12 @@ impl Dock { .map_or(false, |panel| panel.has_focus(cx)) } + pub fn panel(&self) -> Option> { + self.panel_entries + .iter() + .find_map(|entry| entry.panel.as_any().clone().downcast()) + } + pub fn panel_index_for_type(&self) -> Option { self.panel_entries .iter() @@ -472,11 +482,22 @@ impl View for PanelButtons { Flex::row() .with_children(panels.into_iter().enumerate().map( |(panel_ix, (view, context_menu))| { - let (tooltip, tooltip_action) = view.icon_tooltip(cx); + let is_active = is_open && panel_ix == active_ix; + let (tooltip, tooltip_action) = if is_active { + ( + format!("Close {} dock", dock_position.to_label()), + Some(match dock_position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + }), + ) + } else { + view.icon_tooltip(cx) + }; Stack::new() .with_child( MouseEventHandler::::new(panel_ix, cx, |state, cx| { - let is_active = is_open && panel_ix == active_ix; let style = button_style.style_for(state, is_active); Flex::row() .with_child( diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 21b3be09d06ea6781e70dd12fa91a1c2ace2e152..1e3c6044a1651101939453d2ee2e5c6b90df3564 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,5 +1,5 @@ use crate::{Toast, Workspace}; -use collections::HashSet; +use collections::HashMap; use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use std::{any::TypeId, ops::DerefMut}; @@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle { } } -struct NotificationTracker { - notifications_sent: HashSet, +pub(crate) struct NotificationTracker { + notifications_sent: HashMap>, } impl std::ops::Deref for NotificationTracker { - type Target = HashSet; + type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.notifications_sent @@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker { impl NotificationTracker { fn new() -> Self { Self { - notifications_sent: HashSet::default(), + notifications_sent: Default::default(), } } } impl Workspace { + pub fn has_shown_notification_once( + &self, + id: usize, + cx: &ViewContext, + ) -> bool { + cx.global::() + .get(&TypeId::of::()) + .map(|ids| ids.contains(&id)) + .unwrap_or(false) + } + pub fn show_notification_once( &mut self, id: usize, cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { - if !cx - .global::() - .contains(&TypeId::of::()) - { + if !self.has_shown_notification_once::(id, cx) { cx.update_global::(|tracker, _| { - tracker.insert(TypeId::of::()) + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); }); self.show_notification::(id, cx, build_notification) @@ -154,9 +163,10 @@ pub mod simple_message_notification { use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, + fonts::TextStyle, impl_actions, platform::{CursorStyle, MouseButton}, - AppContext, Element, Entity, View, ViewContext, + AnyElement, AppContext, Element, Entity, View, ViewContext, }; use menu::Cancel; use serde::Deserialize; @@ -184,8 +194,13 @@ pub mod simple_message_notification { ) } + enum NotificationMessage { + Text(Cow<'static, str>), + Element(fn(TextStyle, &AppContext) -> AnyElement), + } + pub struct MessageNotification { - message: Cow<'static, str>, + message: NotificationMessage, on_click: Option)>>, click_message: Option>, } @@ -204,7 +219,17 @@ pub mod simple_message_notification { S: Into>, { Self { - message: message.into(), + message: NotificationMessage::Text(message.into()), + on_click: None, + click_message: None, + } + } + + pub fn new_element( + message: fn(TextStyle, &AppContext) -> AnyElement, + ) -> MessageNotification { + Self { + message: NotificationMessage::Element(message), on_click: None, click_message: None, } @@ -243,84 +268,90 @@ pub mod simple_message_notification { enum MessageNotificationTag {} let click_message = self.click_message.clone(); - let message = self.message.clone(); + let message = match &self.message { + NotificationMessage::Text(text) => { + Text::new(text.to_owned(), theme.message.text.clone()).into_any() + } + NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), + }; let on_click = self.on_click.clone(); let has_click_action = on_click.is_some(); - MouseEventHandler::::new(0, cx, |state, cx| { - Flex::column() - .with_child( - Flex::row() - .with_child( - Text::new(message, theme.message.text.clone()) - .contained() - .with_style(theme.message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .constrained() - .with_height( - cx.font_cache().line_height(theme.message.text.font_size), - ) + Flex::column() + .with_child( + Flex::row() + .with_child( + message + .contained() + .with_style(theme.message.container) .aligned() .top() - .flex_float(), - ), - ) - .with_children({ - let style = theme.action_message.style_for(state, false); - if let Some(click_message) = click_message { - Some( - Flex::row().with_child( - Text::new(click_message, style.text.clone()) + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state, false); + Svg::new("icons/x_mark_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_padding(Padding::uniform(5.)) + .on_click(MouseButton::Left, move |_, this, cx| { + this.dismiss(&Default::default(), cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .constrained() + .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + .aligned() + .top() + .flex_float(), + ), + ) + .with_children({ + click_message + .map(|click_message| { + MouseEventHandler::::new( + 0, + cx, + |state, _| { + let style = theme.action_message.style_for(state, false); + + Flex::row() + .with_child( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container), + ) .contained() - .with_style(style.container), - ), + }, ) - } else { - None - } + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(on_click) = on_click.as_ref() { + on_click(cx); + this.dismiss(&Default::default(), cx); + } + }) + // Since we're not using a proper overlay, we have to capture these extra events + .on_down(MouseButton::Left, |_, _, _| {}) + .on_up(MouseButton::Left, |_, _, _| {}) + .with_cursor_style(if has_click_action { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + }) .into_iter() - }) - .contained() - }) - // Since we're not using a proper overlay, we have to capture these extra events - .on_down(MouseButton::Left, |_, _, _| {}) - .on_up(MouseButton::Left, |_, _, _| {}) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(on_click) = on_click.as_ref() { - on_click(cx); - this.dismiss(&Default::default(), cx); - } - }) - .with_cursor_style(if has_click_action { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .into_any() + }) + .into_any() } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 921ae5e0100d45ced85f1664dfe9a1acf534892a..551bc831d3cc074a95cef62479b26407c6fe65bf 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,8 +2,8 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; use crate::{ - item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewCenterTerminal, NewFile, - NewSearch, ToggleZoom, Workspace, WorkspaceSettings, + item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item, + NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; @@ -268,6 +268,7 @@ impl Pane { .with_child(Self::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(("New...".into(), None)), cx, |pane, cx| pane.deploy_new_menu(cx), @@ -277,6 +278,7 @@ impl Pane { .with_child(Self::render_tab_bar_button( 1, "icons/split_12.svg", + false, Some(("Split Pane".into(), None)), cx, |pane, cx| pane.deploy_split_menu(cx), @@ -290,6 +292,7 @@ impl Pane { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), @@ -536,6 +539,11 @@ impl Pane { } pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + // Potentially warn the user of the new keybinding + let workspace_handle = self.workspace().clone(); + cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) + .detach(); + if self.zoomed { cx.emit(Event::ZoomOut); } else if !self.items.is_empty() { @@ -1014,7 +1022,7 @@ impl Pane { let is_active_item = target_item_id == active_item_id; let target_pane = cx.weak_handle(); - // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab + // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab self.tab_context_menu.update(cx, |menu, cx| { menu.show( @@ -1143,7 +1151,8 @@ impl Pane { let theme = theme::current(cx).clone(); let mut tooltip_theme = theme.tooltip.clone(); tooltip_theme.max_text_width = None; - let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string()); + let tab_tooltip_text = + item.tab_tooltip_text(cx).map(|text| text.into_owned()); move |mouse_state, cx| { let tab_style = @@ -1401,6 +1410,7 @@ impl Pane { pub fn render_tab_bar_button)>( index: usize, icon: &'static str, + active: bool, tooltip: Option<(String, Option>)>, cx: &mut ViewContext, on_click: F, @@ -1410,7 +1420,7 @@ impl Pane { let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; - let style = theme.pane_button.style_for(mouse_state, false); + let style = theme.pane_button.style_for(mouse_state, active); Svg::new(icon) .with_color(style.color) .constrained() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d051f6d80a9f6a2ee8974797333ac3ae0d6ddfe8..862767c0ee8744bb0dca6893da38525e22b098cf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,6 @@ mod toolbar; mod workspace_settings; use anyhow::{anyhow, Context, Result}; -use assets::Assets; use call::ActiveCall; use client::{ proto::{self, PeerId}, @@ -60,7 +59,7 @@ use std::{ }; use crate::{ - notifications::simple_message_notification::MessageNotification, + notifications::{simple_message_notification::MessageNotification, NotificationTracker}, persistence::model::{ DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, }, @@ -81,9 +80,9 @@ use serde::Deserialize; use shared_screen::SharedScreen; use status_bar::StatusBar; pub use status_bar::StatusItemView; -use theme::Theme; +use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{async_iife, paths, ResultExt}; +use util::{async_iife, ResultExt}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { @@ -133,8 +132,6 @@ actions!( ] ); -actions!(zed, [OpenSettings]); - #[derive(Clone, PartialEq)] pub struct OpenPaths { pub paths: Vec, @@ -295,17 +292,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { .detach(); }); - cx.add_action( - move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - create_and_open_local_file(&paths::SETTINGS, cx, || { - settings::initial_user_settings_content(&Assets) - .as_ref() - .into() - }) - .detach_and_log_err(cx); - }, - ); - let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); @@ -765,25 +751,21 @@ impl Workspace { DB.next_id().await.unwrap_or(0) }; - let window_bounds_override = - ZED_WINDOW_POSITION - .zip(*ZED_WINDOW_SIZE) - .map(|(position, size)| { - WindowBounds::Fixed(RectF::new( - cx.platform().screens()[0].bounds().origin() + position, - size, - )) - }); - - let build_workspace = |cx: &mut ViewContext| { - Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) - }; - let workspace = requesting_window_id .and_then(|window_id| { - cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx))) + cx.update(|cx| { + cx.replace_root_view(window_id, |cx| { + Workspace::new( + workspace_id, + project_handle.clone(), + app_state.clone(), + cx, + ) + }) + }) }) .unwrap_or_else(|| { + let window_bounds_override = window_bounds_env_override(&cx); let (bounds, display) = if let Some(bounds) = window_bounds_override { (Some(bounds), None) } else { @@ -819,7 +801,14 @@ impl Workspace { // Use the serialized workspace to construct the new window cx.add_window( (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| build_workspace(cx), + |cx| { + Workspace::new( + workspace_id, + project_handle.clone(), + app_state.clone(), + cx, + ) + }, ) .1 }); @@ -908,18 +897,24 @@ impl Workspace { } }); } else if T::should_zoom_in_on_event(event) { - this.zoom_out(cx); dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); if panel.has_focus(cx) { this.zoomed = Some(panel.downgrade().into_any()); this.zoomed_position = Some(panel.read(cx).position(cx)); } } else if T::should_zoom_out_on_event(event) { - this.zoom_out(cx); + dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); + if this.zoomed_position == Some(prev_position) { + this.zoomed = None; + this.zoomed_position = None; + } + cx.notify(); } else if T::is_focus_event(event) { + let position = panel.read(cx).position(cx); + this.dismiss_zoomed_items_to_reveal(Some(position), cx); if panel.is_zoomed(cx) { this.zoomed = Some(panel.downgrade().into_any()); - this.zoomed_position = Some(panel.read(cx).position(cx)); + this.zoomed_position = Some(position); } else { this.zoomed = None; this.zoomed_position = None; @@ -968,9 +963,8 @@ impl Workspace { let timestamp = entry.timestamp; match history.entry(project_path) { hash_map::Entry::Occupied(mut entry) => { - let (old_fs_path, old_timestamp) = entry.get(); + let (_, old_timestamp) = entry.get(); if ×tamp > old_timestamp { - assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history"); entry.insert((fs_path, timestamp)); } } @@ -1592,7 +1586,7 @@ impl Workspace { DockPosition::Right => &self.right_dock, }; let mut focus_center = false; - let mut zoom_out = false; + let mut reveal_dock = false; dock.update(cx, |dock, cx| { let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); let was_visible = dock.is_open() && !other_is_zoomed; @@ -1607,14 +1601,15 @@ impl Workspace { if active_panel.is_zoomed(cx) { cx.focus(active_panel.as_any()); } - zoom_out = true; + reveal_dock = true; } } }); - if zoom_out { - self.zoom_out_everything_except(dock_side, cx); + if reveal_dock { + self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); } + if focus_center { cx.focus_self(); } @@ -1623,62 +1618,49 @@ impl Workspace { self.serialize_workspace(cx); } + /// Transfer focus to the panel of the given type. pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { - self.show_or_hide_panel::(cx, |_, _| true)? + self.focus_or_unfocus_panel::(cx, |_, _| true)? .as_any() .clone() .downcast() } + /// Focus the panel of the given type if it isn't already focused. If it is + /// already focused, then transfer focus back to the workspace center. pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { - self.show_or_hide_panel::(cx, |panel, cx| !panel.has_focus(cx)); + self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); } - fn show_or_hide_panel( + /// Focus or unfocus the given panel type, depending on the given callback. + fn focus_or_unfocus_panel( &mut self, cx: &mut ViewContext, - show: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, + should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, ) -> Option> { - for (dock, position) in [ - self.left_dock.clone(), - self.bottom_dock.clone(), - self.right_dock.clone(), - ] - .into_iter() - .zip( - [ - DockPosition::Left, - DockPosition::Bottom, - DockPosition::Right, - ] - .into_iter(), - ) { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { let mut focus_center = false; - let mut zoom_out = false; + let mut reveal_dock = false; let panel = dock.update(cx, |dock, cx| { dock.activate_panel(panel_index, cx); let panel = dock.active_panel().cloned(); if let Some(panel) = panel.as_ref() { - let should_show = show(&**panel, cx); - if should_show { + if should_focus(&**panel, cx) { dock.set_open(true, cx); cx.focus(panel.as_any()); - zoom_out = true; + reveal_dock = true; } else { - if panel.is_zoomed(cx) { - dock.set_open(false, cx); - } + // if panel.is_zoomed(cx) { + // dock.set_open(false, cx); + // } focus_center = true; } } panel }); - if zoom_out { - self.zoom_out_everything_except(position, cx); - } if focus_center { cx.focus_self(); } @@ -1691,6 +1673,16 @@ impl Workspace { None } + pub fn panel(&self, cx: &WindowContext) -> Option> { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + let dock = dock.read(cx); + if let Some(panel) = dock.panel::() { + return Some(panel); + } + } + None + } + fn zoom_out(&mut self, cx: &mut ViewContext) { for pane in &self.panes { pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); @@ -1705,28 +1697,38 @@ impl Workspace { cx.notify(); } - fn zoom_out_everything_except( + fn dismiss_zoomed_items_to_reveal( &mut self, - except_position: DockPosition, + dock_to_reveal: Option, cx: &mut ViewContext, ) { + // If a center pane is zoomed, unzoom it. for pane in &self.panes { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - } - - if except_position != DockPosition::Left { - self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + if pane != &self.active_pane || dock_to_reveal.is_some() { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + } } - if except_position != DockPosition::Bottom { - self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + // If another dock is zoomed, hide it. + let mut focus_center = false; + for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { + dock.update(cx, |dock, cx| { + if Some(dock.position()) != dock_to_reveal { + if let Some(panel) = dock.active_panel() { + if panel.is_zoomed(cx) { + focus_center |= panel.has_focus(cx); + dock.set_open(false, cx); + } + } + } + }); } - if except_position != DockPosition::Right { - self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + if focus_center { + cx.focus_self(); } - if self.zoomed_position != Some(except_position) { + if self.zoomed_position != dock_to_reveal { self.zoomed = None; self.zoomed_position = None; } @@ -1937,6 +1939,7 @@ impl Workspace { self.last_active_center_pane = Some(pane.downgrade()); } + self.dismiss_zoomed_items_to_reveal(None, cx); if pane.read(cx).is_zoomed() { self.zoomed = Some(pane.downgrade().into_any()); } else { @@ -1998,7 +2001,6 @@ impl Workspace { } pane::Event::ZoomIn => { if pane == self.active_pane { - self.zoom_out(cx); pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); if pane.read(cx).has_focus() { self.zoomed = Some(pane.downgrade().into_any()); @@ -2007,7 +2009,13 @@ impl Workspace { cx.notify(); } } - pane::Event::ZoomOut => self.zoom_out(cx), + pane::Event::ZoomOut => { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + if self.zoomed_position.is_none() { + self.zoomed = None; + } + cx.notify(); + } } self.serialize_workspace(cx); @@ -3101,6 +3109,17 @@ impl Workspace { } } +fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { + ZED_WINDOW_POSITION + .zip(*ZED_WINDOW_SIZE) + .map(|(position, size)| { + WindowBounds::Fixed(RectF::new( + cx.platform().screens()[0].bounds().origin() + position, + size, + )) + }) +} + async fn open_items( serialized_workspace: Option, workspace: &WeakViewHandle, @@ -3190,6 +3209,87 @@ async fn open_items( opened_items } +fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { + const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; + const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; + const MESSAGE_ID: usize = 2; + + if workspace + .read_with(cx, |workspace, cx| { + workspace.has_shown_notification_once::(MESSAGE_ID, cx) + }) + .unwrap_or(false) + { + return; + } + + if db::kvp::KEY_VALUE_STORE + .read_kvp(NEW_DOCK_HINT_KEY) + .ok() + .flatten() + .is_some() + { + if !workspace + .read_with(cx, |workspace, cx| { + workspace.has_shown_notification_once::(MESSAGE_ID, cx) + }) + .unwrap_or(false) + { + cx.update(|cx| { + cx.update_global::(|tracker, _| { + let entry = tracker + .entry(TypeId::of::()) + .or_default(); + if !entry.contains(&MESSAGE_ID) { + entry.push(MESSAGE_ID); + } + }); + }); + } + + return; + } + + cx.spawn(|_| async move { + db::kvp::KEY_VALUE_STORE + .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) + .await + .ok(); + }) + .detach(); + + workspace + .update(cx, |workspace, cx| { + workspace.show_notification_once(2, cx, |cx| { + cx.add_view(|_| { + MessageNotification::new_element(|text, _| { + Text::new( + "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", + text, + ) + .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| { + let code_span_background_color = settings::get::(cx) + .theme + .editor + .document_highlight_read_background; + + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radius: 2.0, + }) + }) + .into_any() + }) + .with_click_message("Read more about the new panel system") + .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) + }) + }) + }) + .ok(); +} + fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; @@ -3206,7 +3306,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut Asy } else { let backup_path = (*db::BACKUP_DB_PATH).read(); if let Some(backup_path) = backup_path.clone() { - workspace.show_notification_once(0, cx, move |cx| { + workspace.show_notification_once(1, cx, move |cx| { cx.add_view(move |_| { MessageNotification::new(format!( "Database file was corrupted. Old database backed up to {}", @@ -3278,32 +3378,36 @@ impl View for Workspace { enum ZoomBackground {} let zoomed = zoomed.upgrade(cx)?; - let mut foreground_style; - match self.zoomed_position { - Some(DockPosition::Left) => { - foreground_style = - theme.workspace.zoomed_panel_foreground; - foreground_style.margin.left = 0.; - foreground_style.margin.top = 0.; - foreground_style.margin.bottom = 0.; - } - Some(DockPosition::Right) => { - foreground_style = - theme.workspace.zoomed_panel_foreground; - foreground_style.margin.right = 0.; - foreground_style.margin.top = 0.; - foreground_style.margin.bottom = 0.; - } - Some(DockPosition::Bottom) => { - foreground_style = - theme.workspace.zoomed_panel_foreground; - foreground_style.margin.left = 0.; - foreground_style.margin.right = 0.; - foreground_style.margin.bottom = 0.; - } - None => { - foreground_style = - theme.workspace.zoomed_pane_foreground; + let mut foreground_style = + theme.workspace.zoomed_pane_foreground; + if let Some(zoomed_dock_position) = self.zoomed_position { + foreground_style = + theme.workspace.zoomed_panel_foreground; + let margin = foreground_style.margin.top; + let border = foreground_style.border.top; + + // Only include a margin and border on the opposite side. + foreground_style.margin.top = 0.; + foreground_style.margin.left = 0.; + foreground_style.margin.bottom = 0.; + foreground_style.margin.right = 0.; + foreground_style.border.top = false; + foreground_style.border.left = false; + foreground_style.border.bottom = false; + foreground_style.border.right = false; + match zoomed_dock_position { + DockPosition::Left => { + foreground_style.margin.right = margin; + foreground_style.border.right = border; + } + DockPosition::Right => { + foreground_style.margin.left = margin; + foreground_style.border.left = border; + } + DockPosition::Bottom => { + foreground_style.margin.top = margin; + foreground_style.border.top = border; + } } } @@ -3548,8 +3652,13 @@ pub fn join_remote_project( }) .await?; + let window_bounds_override = window_bounds_env_override(&cx); let (_, workspace) = cx.add_window( - (app_state.build_window_options)(None, None, cx.platform().as_ref()), + (app_state.build_window_options)( + window_bounds_override, + None, + cx.platform().as_ref(), + ), |cx| Workspace::new(0, project, app_state.clone(), cx), ); (app_state.initialize_workspace)( @@ -4257,6 +4366,12 @@ mod tests { panel }); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + pane.update(cx, |pane, cx| { + let item = cx.add_view(|_| TestItem::new()); + pane.add_item(Box::new(item), true, true, None, cx); + }); + // Transfer focus from center to panel workspace.update(cx, |workspace, cx| { workspace.toggle_panel_focus::(cx); @@ -4324,7 +4439,7 @@ mod tests { assert!(!panel.has_focus(cx)); }); - // Transfering focus back to the panel keeps it zoomed + // Transferring focus back to the panel keeps it zoomed workspace.update(cx, |workspace, cx| { workspace.toggle_panel_focus::(cx); }); @@ -4358,6 +4473,25 @@ mod tests { assert!(workspace.zoomed.is_some()); assert!(panel.has_focus(cx)); }); + + // Unzoom and close the panel, zoom the active pane. + panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); + + // Opening a dock unzooms the pane. + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + workspace.read_with(cx, |workspace, cx| { + let pane = pane.read(cx); + assert!(!pane.is_zoomed()); + assert!(pane.has_focus()); + assert!(workspace.right_dock().read(cx).is_open()); + assert!(workspace.zoomed.is_none()); + }); } #[gpui::test] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e96dff122186cb07e17eff54ff909d36c1454731..81f89c0430dd7e6f07e4b6abaf3265d48548113a 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.89.0" +version = "0.91.0" publish = false [lib] @@ -17,7 +17,6 @@ path = "src/main.rs" [dependencies] activity_indicator = { path = "../activity_indicator" } -assets = { path = "../assets" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } @@ -107,7 +106,7 @@ tree-sitter = "0.20" tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } -tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" } +tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" } tree-sitter-embedded-template = "0.20.0" tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } @@ -120,7 +119,7 @@ tree-sitter-ruby = "0.20.0" tree-sitter-html = "0.19.0" tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"} tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"} -tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "9050a4a4a847ed29e25485b1292a36eab8ae3492"} +tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "5694b7f290cd9ef998829a0a6d8391a666370886"} tree-sitter-lua = "0.0.14" url = "2.2" urlencoding = "2.1.2" diff --git a/crates/zed/build.rs b/crates/zed/build.rs index bb971041a5b08f863817cef5ffeec7bee8c015fd..b83afba7474f80f5082168e42fb01a41b850f69a 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -1,9 +1,8 @@ +use std::process::Command; + fn main() { println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); - if let Ok(value) = std::env::var("ZED_MIXPANEL_TOKEN") { - println!("cargo:rustc-env=ZED_MIXPANEL_TOKEN={value}"); - } if let Ok(value) = std::env::var("ZED_PREVIEW_CHANNEL") { println!("cargo:rustc-env=ZED_PREVIEW_CHANNEL={value}"); } @@ -24,4 +23,32 @@ fn main() { // Register exported Objective-C selectors, protocols, etc println!("cargo:rustc-link-arg=-Wl,-ObjC"); + + // Install dependencies for theme-generation + let output = Command::new("npm") + .current_dir("../../styles") + .args(["install", "--no-save"]) + .output() + .expect("failed to run npm"); + if !output.status.success() { + panic!( + "failed to install theme dependencies {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + // Regenerate themes + let output = Command::new("npm") + .current_dir("../../styles") + .args(["run", "build"]) + .output() + .expect("failed to run npm"); + if !output.status.success() { + panic!( + "build script failed {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + println!("cargo:rerun-if-changed=../../styles/src"); } diff --git a/crates/assets/src/assets.rs b/crates/zed/src/assets.rs similarity index 84% rename from crates/assets/src/assets.rs rename to crates/zed/src/assets.rs index 7d5748e43b7647bbb5967b44696b620117f7c528..6eb8a44f0fb613a637af1fb6e20d2a80a359bf9f 100644 --- a/crates/assets/src/assets.rs +++ b/crates/zed/src/assets.rs @@ -4,6 +4,10 @@ use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "../../assets"] +#[include = "fonts/**/*"] +#[include = "icons/**/*"] +#[include = "themes/**/*"] +#[include = "*.md"] #[exclude = "*.DS_Store"] pub struct Assets; diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 1f2b359af1523212d97de8dfd43cf4bccaae6dfb..3ae564d13b55c83f8035d23d0a1f5879ae2f1382 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -3,6 +3,7 @@ pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; +use util::asset_str; mod c; mod elixir; @@ -179,10 +180,7 @@ fn load_query(name: &str, filename_prefix: &str) -> Option> { for path in LanguageDir::iter() { if let Some(remainder) = path.strip_prefix(name) { if remainder.starts_with(filename_prefix) { - let contents = match LanguageDir::get(path.as_ref()).unwrap().data { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - }; + let contents = asset_str::(path.as_ref()); match &mut result { None => result = Some(contents), Some(r) => r.to_mut().push_str(contents.as_ref()), diff --git a/crates/zed/src/languages/elixir/highlights.scm b/crates/zed/src/languages/elixir/highlights.scm index 5c256f341cd5a2e4e045ec4cc3610eabeac3a88a..deea51c436386eb36b1ed41d61cb5d21a787ad20 100644 --- a/crates/zed/src/languages/elixir/highlights.scm +++ b/crates/zed/src/languages/elixir/highlights.scm @@ -1,20 +1,5 @@ ["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword -(unary_operator - operator: "@" @comment.doc - operand: (call - target: (identifier) @comment.doc.__attribute__ - (arguments - [ - (string) @comment.doc - (charlist) @comment.doc - (sigil - quoted_start: _ @comment.doc - quoted_end: _ @comment.doc) @comment.doc - (boolean) @comment.doc - ])) - (#match? @comment.doc.__attribute__ "^(moduledoc|typedoc|doc)$")) - (unary_operator operator: "&" operand: (integer) @operator) @@ -84,6 +69,11 @@ quoted_start: _ @string.special quoted_end: _ @string.special) @string.special +( + (identifier) @comment.unused + (#match? @comment.unused "^_") +) + (call target: [ (identifier) @function @@ -99,17 +89,12 @@ (binary_operator left: (identifier) @function operator: "when") + (binary_operator + operator: "|>" + right: (identifier)) ]) (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) -(call - target: (identifier) @keyword - (arguments - (binary_operator - operator: "|>" - right: (identifier))) - (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) - (binary_operator operator: "|>" right: (identifier) @function) @@ -127,10 +112,18 @@ (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") ) -( - (identifier) @comment.unused - (#match? @comment.unused "^_") -) +(unary_operator + operator: "@" @comment.doc + operand: (call + target: (identifier) @__attribute__ @comment.doc + (arguments + [ + (string) + (charlist) + (sigil) + (boolean) + ] @comment.doc)) + (#match? @__attribute__ "^(moduledoc|typedoc|doc)$")) (comment) @comment diff --git a/crates/zed/src/languages/elixir/indents.scm b/crates/zed/src/languages/elixir/indents.scm index e4139841fc2d2d0ec0bc03f6f76786b76621f77e..ab6fc4da67c95aa1ba8fabdbc10d53b5d7c5e40e 100644 --- a/crates/zed/src/languages/elixir/indents.scm +++ b/crates/zed/src/languages/elixir/indents.scm @@ -1,6 +1,4 @@ -[ - (call) -] @indent +(call) @indent (_ "[" "]" @end) @indent (_ "{" "}" @end) @indent diff --git a/crates/zed/src/languages/elixir/outline.scm b/crates/zed/src/languages/elixir/outline.scm index 985c8ffdca68ab420470a2eaa4f71ab658a9ec30..a3311fb6d4640aa4ff5469c638022c1fde02e912 100644 --- a/crates/zed/src/languages/elixir/outline.scm +++ b/crates/zed/src/languages/elixir/outline.scm @@ -8,9 +8,19 @@ (arguments [ (identifier) @name - (call target: (identifier) @name) + (call + target: (identifier) @name + (arguments + "(" @context.extra + _* @context.extra + ")" @context.extra)) (binary_operator - left: (call target: (identifier) @name) + left: (call + target: (identifier) @name + (arguments + "(" @context.extra + _* @context.extra + ")" @context.extra)) operator: "when") ]) (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 406d54cc03907e94d2b76c4e87e97941458ac694..e1f3da9e0238f0a91179447e7c15ce2df5239ec1 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -6,7 +6,7 @@ use gpui::AppContext; use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; use node_runtime::NodeRuntime; use serde_json::json; -use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore}; +use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; use staff_mode::StaffMode; use std::{ @@ -135,12 +135,15 @@ impl LspAdapter for JsonLspAdapter { }, "schemas": [ { - "fileMatch": [schema_file_match(&paths::SETTINGS)], + "fileMatch": [ + schema_file_match(&paths::SETTINGS), + &*paths::LOCAL_SETTINGS_RELATIVE_PATH, + ], "schema": settings_schema, }, { "fileMatch": [schema_file_match(&paths::KEYMAP)], - "schema": keymap_file_json_schema(&action_names), + "schema": KeymapFile::generate_json_schema(&action_names), } ] } diff --git a/crates/zed/src/languages/markdown/config.toml b/crates/zed/src/languages/markdown/config.toml index 55204cc7a57ad051004a4fc0d76746057908aa20..2fa3ff3cf2aba297517494cbd1f2e0608daaa402 100644 --- a/crates/zed/src/languages/markdown/config.toml +++ b/crates/zed/src/languages/markdown/config.toml @@ -1,5 +1,5 @@ name = "Markdown" -path_suffixes = ["md", "mdx", "zmd"] +path_suffixes = ["md", "mdx"] brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, diff --git a/crates/zed/src/languages/markdown/highlights.scm b/crates/zed/src/languages/markdown/highlights.scm index 83bf7b57a48df2b77648dcd33fbb8fdb675c9e51..971c27686803c45a6200a5fc5497c1171b6d77c6 100644 --- a/crates/zed/src/languages/markdown/highlights.scm +++ b/crates/zed/src/languages/markdown/highlights.scm @@ -14,11 +14,11 @@ (list_marker_parenthesis) ] @punctuation.list_marker -[ - (indented_code_block) - (fenced_code_block) - (code_span) -] @text.literal +(code_span) @text.literal + +(fenced_code_block + (info_string + (language) @text.literal)) (link_destination) @link_uri (link_text) @link_text diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 429a5d942151cd9dbd3661b2ebb6bbc16a6505e3..7d2d580857780a714496a5f8c2c850de36986d26 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -207,7 +207,7 @@ impl LspAdapter for EsLintLspAdapter { http: Arc, ) -> Result> { // At the time of writing the latest vscode-eslint release was released in 2020 and requires - // special custom LSP protocol extensions be handled to fully initalize. Download the latest + // special custom LSP protocol extensions be handled to fully initialize. Download the latest // prerelease instead to sidestep this issue let release = latest_github_release("microsoft/vscode-eslint", true, http).await?; Ok(Box::new(GitHubLspBinaryVersion { @@ -327,10 +327,10 @@ mod tests { .map(|item| (item.text.as_str(), item.depth)) .collect::>(), &[ - ("function a ( )", 0), - ("async function a2 ( )", 1), + ("function a()", 0), + ("async function a2()", 1), ("let b", 0), - ("function getB ( )", 0), + ("function getB()", 0), ("const d", 0), ] ); diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index bd5f2b4c021e7d8239d76ea29d8ef88ddcf8015b..7f87a7caedb764588a698b5e863fbd418c3859ed 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{ - language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, + language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, }; use node_runtime::NodeRuntime; use serde_json::Value; @@ -101,13 +101,16 @@ impl LspAdapter for YamlLspAdapter { } fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { + let tab_size = all_language_settings(None, cx) + .language(Some("YAML")) + .tab_size; Some( future::ready(serde_json::json!({ "yaml": { "keyOrdering": false }, "[yaml]": { - "editor.tabSize": language_settings(Some("YAML"), cx).tab_size, + "editor.tabSize": tab_size, } })) .boxed(), diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 31f331ef93ef17eecb4870f3ed23c9f963a5b3aa..2393d0df3b1ce38bd41c56786ad8d232d5734b6b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1,8 +1,7 @@ -// Allow binary to be called Zed for a nice application menu when running executable direcly +// Allow binary to be called Zed for a nice application menu when running executable directly #![allow(non_snake_case)] use anyhow::{anyhow, Context, Result}; -use assets::Assets; use backtrace::Backtrace; use cli::{ ipc::{self, IpcSender}, @@ -32,6 +31,7 @@ use std::{ ffi::OsStr, fs::OpenOptions, io::Write as _, + ops::Not, os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, @@ -41,7 +41,7 @@ use std::{ Arc, Weak, }, thread, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use sum_tree::Bias; use terminal_view::{get_working_directory, TerminalSettings, TerminalView}; @@ -55,11 +55,10 @@ use fs::RealFs; #[cfg(debug_assertions)] use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; -use workspace::{ - item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace, -}; +use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use zed::{ - self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, + assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, + languages, menus, }; fn main() { @@ -70,10 +69,7 @@ fn main() { log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); - let app_version = ZED_APP_VERSION - .or_else(|| app.platform().app_version().ok()) - .map_or("dev".to_string(), |v| v.to_string()); - init_panic_hook(app_version); + init_panic_hook(&app); app.background(); @@ -164,6 +160,8 @@ fn main() { ai::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); + cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) + .detach(); languages.set_theme(theme::current(cx).clone()); cx.observe_global::({ @@ -173,11 +171,6 @@ fn main() { .detach(); client.telemetry().start(); - client.telemetry().report_mixpanel_event( - "start app", - Default::default(), - *settings::get::(cx), - ); let app_state = Arc::new(AppState { languages, @@ -374,32 +367,96 @@ struct Panic { #[serde(skip_serializing_if = "Option::is_none")] location_data: Option, backtrace: Vec, - // TODO - // stripped_backtrace: String, + app_version: String, + release_channel: String, + os_name: String, + os_version: Option, + architecture: String, + panicked_on: u128, + identifying_backtrace: Option>, } #[derive(Serialize)] struct PanicRequest { panic: Panic, - version: String, token: String, } -fn init_panic_hook(app_version: String) { +fn init_panic_hook(app: &App) { let is_pty = stdout_is_a_pty(); + let platform = app.platform(); + panic::set_hook(Box::new(move |info| { - let backtrace = Backtrace::new(); + let app_version = ZED_APP_VERSION + .or_else(|| platform.app_version().ok()) + .map_or("dev".to_string(), |v| v.to_string()); let thread = thread::current(); let thread = thread.name().unwrap_or(""); - let payload = match info.payload().downcast_ref::<&'static str>() { - Some(s) => *s, - None => match info.payload().downcast_ref::() { - Some(s) => &**s, - None => "Box", - }, - }; + let payload = info.payload(); + let payload = None + .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string())) + .or_else(|| payload.downcast_ref::().map(|s| s.clone())) + .unwrap_or_else(|| "Box".to_string()); + + let backtrace = Backtrace::new(); + let backtrace = backtrace + .frames() + .iter() + .filter_map(|frame| { + let symbol = frame.symbols().first()?; + let path = symbol.filename()?; + Some((path, symbol.lineno(), format!("{:#}", symbol.name()?))) + }) + .collect::>(); + + let this_file_path = Path::new(file!()); + + // Find the first frame in the backtrace for this panic hook itself. Exclude + // that frame and all frames before it. + let mut start_frame_ix = 0; + let mut codebase_root_path = None; + for (ix, (path, _, _)) in backtrace.iter().enumerate() { + if path.ends_with(this_file_path) { + start_frame_ix = ix + 1; + codebase_root_path = path.ancestors().nth(this_file_path.components().count()); + break; + } + } + + // Exclude any subsequent frames inside of rust's panic handling system. + while let Some((path, _, _)) = backtrace.get(start_frame_ix) { + if path.starts_with("/rustc") { + start_frame_ix += 1; + } else { + break; + } + } + + // Build two backtraces: + // * one for display, which includes symbol names for all frames, and files + // and line numbers for symbols in this codebase + // * one for identification and de-duplication, which only includes symbol + // names for symbols in this codebase. + let mut display_backtrace = Vec::new(); + let mut identifying_backtrace = Vec::new(); + for (path, line, symbol) in &backtrace[start_frame_ix..] { + display_backtrace.push(symbol.clone()); + + if let Some(codebase_root_path) = &codebase_root_path { + if let Ok(suffix) = path.strip_prefix(&codebase_root_path) { + identifying_backtrace.push(symbol.clone()); + + let display_path = suffix.to_string_lossy(); + if let Some(line) = line { + display_backtrace.push(format!(" {display_path}:{line}")); + } else { + display_backtrace.push(format!(" {display_path}")); + } + } + } + } let panic_data = Panic { thread: thread.into(), @@ -408,11 +465,23 @@ fn init_panic_hook(app_version: String) { file: location.file().into(), line: location.line(), }), - backtrace: format!("{:?}", backtrace) - .split("\n") - .map(|line| line.to_string()) - .collect(), - // modified_backtrace: None, + app_version: app_version.clone(), + release_channel: RELEASE_CHANNEL.dev_name().into(), + os_name: platform.os_name().into(), + os_version: platform + .os_version() + .ok() + .map(|os_version| os_version.to_string()), + architecture: env::consts::ARCH.into(), + panicked_on: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + backtrace: display_backtrace, + identifying_backtrace: identifying_backtrace + .is_empty() + .not() + .then_some(identifying_backtrace), }; if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { @@ -422,8 +491,7 @@ fn init_panic_hook(app_version: String) { } let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); - let panic_file_path = - paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp)); + let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); let panic_file = std::fs::OpenOptions::new() .append(true) .create(true) @@ -458,15 +526,9 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { continue; }; - let mut components = filename.split('-'); - if components.next() != Some("zed") { + if !filename.starts_with("zed") { continue; } - let version = if let Some(version) = components.next() { - version - } else { - continue; - }; if telemetry_settings.diagnostics { let panic_data_text = smol::fs::read_to_string(&child_path) @@ -475,7 +537,6 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { let body = serde_json::to_string(&PanicRequest { panic: serde_json::from_str(&panic_data_text)?, - version: version.to_string(), token: ZED_SECRET_CLIENT_TOKEN.into(), }) .unwrap(); @@ -601,11 +662,30 @@ async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { Some(()) } +#[cfg(debug_assertions)] +async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { + let mut events = fs + .watch( + "crates/zed/src/languages".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + languages.reload(); + } + Some(()) +} + #[cfg(not(debug_assertions))] async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { None } +#[cfg(not(debug_assertions))] +async fn watch_languages(_: Arc, _: Arc) -> Option<()> { + None +} + fn connect_to_cli( server_name: &str, ) -> Result<(mpsc::Receiver, IpcSender)> { @@ -816,6 +896,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { ("Go to file", &file_finder::Toggle), ("Open command palette", &command_palette::Toggle), ("Open recent projects", &recent_projects::OpenRecent), - ("Change your settings", &OpenSettings), + ("Change your settings", &zed::OpenSettings), ] } diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 76e88325f5cc858091cd3fcc367340337d554a9c..9112cd207b5da73f6a7d20a0436db152f98114d2 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -12,10 +12,11 @@ pub fn menus() -> Vec> { MenuItem::submenu(Menu { name: "Preferences", items: vec![ - MenuItem::action("Open Settings", workspace::OpenSettings), + MenuItem::action("Open Settings", super::OpenSettings), MenuItem::action("Open Key Bindings", super::OpenKeymap), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), + MenuItem::action("Open Local Settings", super::OpenLocalSettings), MenuItem::action("Select Theme", theme_selector::Toggle), ], }), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6dbddae2ada970ee754c20c2aa735c3f46c86d91..ecdd1b7a180cee33fa938a39d901022855fe538a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,7 +1,10 @@ +pub mod assets; pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; + +use ai::AssistantPanel; use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; @@ -30,16 +33,22 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH}; +use settings::{initial_local_settings_content, KeymapFile, SettingsStore}; use std::{borrow::Cow, str, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; -use util::{channel::ReleaseChannel, paths, ResultExt}; +use util::{ + asset_str, + channel::ReleaseChannel, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + ResultExt, +}; use uuid::Uuid; use welcome::BaseKeymap; pub use workspace; use workspace::{ - create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow, - Workspace, WorkspaceSettings, + create_and_open_local_file, dock::PanelHandle, + notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, + NewWindow, Workspace, WorkspaceSettings, }; #[derive(Deserialize, Clone, PartialEq)] @@ -65,6 +74,8 @@ actions!( OpenLicenses, OpenTelemetryLog, OpenKeymap, + OpenSettings, + OpenLocalSettings, OpenDefaultSettings, OpenDefaultKeymap, IncreaseBufferFontSize, @@ -140,7 +151,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { open_bundled_file( workspace, - "licenses.md", + asset_str::("licenses.md"), "Open Source License Attribution", "Markdown", cx, @@ -157,11 +168,20 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx); }, ); + cx.add_action( + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + create_and_open_local_file(&paths::SETTINGS, cx, || { + settings::initial_user_settings_content().as_ref().into() + }) + .detach_and_log_err(cx); + }, + ); + cx.add_action(open_local_settings_file); cx.add_action( move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { open_bundled_file( workspace, - "keymaps/default.json", + settings::default_keymap(), "Default Key Bindings", "JSON", cx, @@ -174,7 +194,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx: &mut ViewContext| { open_bundled_file( workspace, - DEFAULT_SETTINGS_ASSET_PATH, + settings::default_settings(), "Default Settings", "JSON", cx, @@ -234,6 +254,13 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action( + |workspace: &mut Workspace, + _: &ai::assistant::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |_: &NewWindow, cx: &mut AppContext| { @@ -339,7 +366,9 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?; + let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + let (project_panel, terminal_panel, assistant_panel) = + futures::try_join!(project_panel, terminal_panel, assistant_panel)?; workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); @@ -357,7 +386,8 @@ pub fn initialize_workspace( workspace.toggle_dock(project_panel_position, cx); } - workspace.add_panel(terminal_panel, cx) + workspace.add_panel(terminal_panel, cx); + workspace.add_panel(assistant_panel, cx); })?; Ok(()) }) @@ -501,11 +531,11 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { pub fn load_default_keymap(cx: &mut AppContext) { for path in ["keymaps/default.json", "keymaps/vim.json"] { - KeymapFileContent::load_asset(path, cx).unwrap(); + KeymapFile::load_asset(path, cx).unwrap(); } if let Some(asset_path) = settings::get::(cx).asset_path() { - KeymapFileContent::load_asset(asset_path, cx).unwrap(); + KeymapFile::load_asset(asset_path, cx).unwrap(); } } @@ -516,7 +546,7 @@ pub fn handle_keymap_file_changes( cx.spawn(move |mut cx| async move { let mut settings_subscription = None; while let Some(user_keymap_content) = user_keymap_file_rx.next().await { - if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) { + if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) { cx.update(|cx| { cx.clear_bindings(); load_default_keymap(cx); @@ -544,6 +574,72 @@ pub fn handle_keymap_file_changes( .detach(); } +fn open_local_settings_file( + workspace: &mut Workspace, + _: &OpenLocalSettings, + cx: &mut ViewContext, +) { + let project = workspace.project().clone(); + let worktree = project + .read(cx) + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); + if let Some(worktree) = worktree { + let tree_id = worktree.read(cx).id(); + cx.spawn(|workspace, mut cx| async move { + let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; + + if let Some(dir_path) = file_path.parent() { + if worktree.read_with(&cx, |tree, _| tree.entry_for_path(dir_path).is_none()) { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + }) + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + } + + if worktree.read_with(&cx, |tree, _| tree.entry_for_path(file_path).is_none()) { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, file_path), false, cx) + }) + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + + let editor = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path((tree_id, file_path), None, true, cx) + })? + .await? + .downcast::() + .ok_or_else(|| anyhow!("unexpected item type"))?; + + editor + .downgrade() + .update(&mut cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + if buffer.read(cx).is_empty() { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_local_settings_content())], None, cx) + }); + } + } + }) + .ok(); + + anyhow::Ok(()) + }) + .detach(); + } else { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_| MessageNotification::new("This project has no folders open.")) + }) + } +} + fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { workspace.with_local_workspace(cx, move |workspace, cx| { let app_state = workspace.app_state().clone(); @@ -603,7 +699,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext, title: &'static str, language: &'static str, cx: &mut ViewContext, @@ -615,13 +711,9 @@ fn open_bundled_file( .update(&mut cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| { let project = workspace.project(); - let buffer = project.update(cx, |project, cx| { - let text = Assets::get(asset_path) - .map(|f| f.data) - .unwrap_or_else(|| Cow::Borrowed(b"File not found")); - let text = str::from_utf8(text.as_ref()).unwrap(); + let buffer = project.update(cx, move |project, cx| { project - .create_buffer(text, language, cx) + .create_buffer(text.as_ref(), language, cx) .expect("creating buffers on a local workspace always succeeds") }); let buffer = cx.add_model(|cx| { @@ -2108,6 +2200,7 @@ mod tests { pane::init(cx); project_panel::init(cx); terminal_view::init(cx); + ai::init(cx); app_state }) } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000000000000000000000000000000000..f78a67ddb344b48057437e80661698500a1cb302 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.70" +components = [ "rustfmt" ] +targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] diff --git a/script/mixpanel_release/main.py b/script/mixpanel_release/main.py deleted file mode 100644 index e2a0eeb990ce8c085fa6d41efeda2a1221c5d512..0000000000000000000000000000000000000000 --- a/script/mixpanel_release/main.py +++ /dev/null @@ -1,30 +0,0 @@ -import datetime -import sys -import requests - -def main(): - version = sys.argv[1] - version = version.removeprefix("v") - project_id = sys.argv[2] - account_username = sys.argv[3] - account_secret = sys.argv[4] - - current_datetime = datetime.datetime.now(datetime.timezone.utc) - current_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S") - - url = f"https://mixpanel.com/api/app/projects/{project_id}/annotations" - - payload = { - "date": current_datetime, - "description": version - } - - response = requests.post( - url, - auth=(account_username, account_secret), - json=payload - ) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/script/mixpanel_release/requirements.txt b/script/mixpanel_release/requirements.txt deleted file mode 100644 index 5e77405687287ea27fa8f90ccd80284639112588..0000000000000000000000000000000000000000 --- a/script/mixpanel_release/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.28.1 \ No newline at end of file diff --git a/styles/src/buildLicenses.ts b/styles/src/buildLicenses.ts index 3367e50ff027601db2b68ead242dc3a266b21e1e..2e1325044de7c8dfe173dab6022bcdab132f9b92 100644 --- a/styles/src/buildLicenses.ts +++ b/styles/src/buildLicenses.ts @@ -1,11 +1,9 @@ import * as fs from "fs" import toml from "toml" -import { schemeMeta } from "./colorSchemes" -import { Meta, Verification } from "./themes/common/colorScheme" -import https from "https" -import crypto from "crypto" +import { themes } from "./themes" +import { ThemeConfig } from "./common" -const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml` +const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml` // Use the cargo-about configuration file as the source of truth for supported licenses. function parseAcceptedToml(file: string): string[] { @@ -20,73 +18,31 @@ function parseAcceptedToml(file: string): string[] { return obj.accepted } -function checkLicenses(schemeMeta: Meta[], licenses: string[]) { - for (let meta of schemeMeta) { - // FIXME: Add support for conjuctions and conditions - if (licenses.indexOf(meta.license.SPDX) < 0) { - throw Error( - `License for theme ${meta.name} (${meta.license.SPDX}) is not supported` - ) +function checkLicenses(themes: ThemeConfig[]) { + for (const theme of themes) { + if (!theme.licenseFile) { + throw Error(`Theme ${theme.name} should have a LICENSE file`) } } } -function getLicenseText( - schemeMeta: Meta[], - callback: (meta: Meta, license_text: string) => void -) { - for (let meta of schemeMeta) { - if (typeof meta.license.license_text == "string") { - callback(meta, meta.license.license_text) - } else { - let license_text_obj: Verification = meta.license.license_text - // The following copied from the example code on nodejs.org: - // https://nodejs.org/api/http.html#httpgetoptions-callback - https - .get(license_text_obj.https_url, (res) => { - const { statusCode } = res - - if (statusCode < 200 || statusCode >= 300) { - throw new Error( - `Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}` - ) - } - - res.setEncoding("utf8") - let rawData = "" - res.on("data", (chunk) => { - rawData += chunk - }) - res.on("end", () => { - const hash = crypto - .createHash("sha256") - .update(rawData) - .digest("hex") - if (license_text_obj.license_checksum == hash) { - callback(meta, rawData) - } else { - throw Error( - `Checksum for ${meta.name} did not match file downloaded from ${license_text_obj.https_url}` - ) - } - }) - }) - .on("error", (e) => { - throw e - }) - } +function generateLicenseFile(themes: ThemeConfig[]) { + checkLicenses(themes) + for (const theme of themes) { + const licenseText = fs.readFileSync(theme.licenseFile).toString() + writeLicense(theme.name, theme.licenseUrl, licenseText) } } -function writeLicense(schemeMeta: Meta, text: String) { +function writeLicense( + themeName: string, + licenseUrl: string, + licenseText: String +) { process.stdout.write( - `## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n` + `## [${themeName}](${licenseUrl})\n\n${licenseText}\n********************************************************************************\n\n` ) } -const accepted_licenses = parseAcceptedToml(accepted_licenses_file) -checkLicenses(schemeMeta, accepted_licenses) - -getLicenseText(schemeMeta, (meta, text) => { - writeLicense(meta, text) -}) +const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE) +generateLicenseFile(themes) diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts index 2a63a407cc4f83dba146991a5edf3757abdfab65..bba300989fe6e29590318fc9a72f46b3a93feb86 100644 --- a/styles/src/buildThemes.ts +++ b/styles/src/buildThemes.ts @@ -1,15 +1,12 @@ import * as fs from "fs" import { tmpdir } from "os" import * as path from "path" -import colorSchemes, { staffColorSchemes } from "./colorSchemes" import app from "./styleTree/app" -import { ColorScheme } from "./themes/common/colorScheme" +import { ColorScheme, createColorScheme } from "./themes/common/colorScheme" import snakeCase from "./utils/snakeCase" +import { themes } from "./themes" const assetsDirectory = `${__dirname}/../../assets` -const themeDirectory = `${assetsDirectory}/themes` -const staffDirectory = `${themeDirectory}/staff` - const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes")) // Clear existing themes @@ -19,23 +16,14 @@ function clearThemes(themeDirectory: string) { } else { for (const file of fs.readdirSync(themeDirectory)) { if (file.endsWith(".json")) { - const name = file.replace(/\.json$/, "") - if ( - !colorSchemes.find( - (colorScheme) => colorScheme.name === name - ) - ) { - fs.unlinkSync(path.join(themeDirectory, file)) - } + fs.unlinkSync(path.join(themeDirectory, file)) } } } } -clearThemes(themeDirectory) -clearThemes(staffDirectory) - function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) { + clearThemes(outputDirectory) for (let colorScheme of colorSchemes) { let styleTree = snakeCase(app(colorScheme)) let styleTreeJSON = JSON.stringify(styleTree, null, 2) @@ -47,6 +35,7 @@ function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) { } } +const colorSchemes: ColorScheme[] = themes.map((theme) => createColorScheme(theme)) + // Write new themes to theme directory -writeThemes(colorSchemes, themeDirectory) -writeThemes(staffColorSchemes, staffDirectory) +writeThemes(colorSchemes, `${assetsDirectory}/themes`) diff --git a/styles/src/colorSchemes.ts b/styles/src/colorSchemes.ts deleted file mode 100644 index 4d2d7f6e0253c6b6dd8d6d579000f3d9cb53ad5f..0000000000000000000000000000000000000000 --- a/styles/src/colorSchemes.ts +++ /dev/null @@ -1,54 +0,0 @@ -import fs from "fs" -import path from "path" -import { ColorScheme, Meta } from "./themes/common/colorScheme" - -const colorSchemes: ColorScheme[] = [] -export default colorSchemes - -const schemeMeta: Meta[] = [] -export { schemeMeta } - -const staffColorSchemes: ColorScheme[] = [] -export { staffColorSchemes } - -const experimentalColorSchemes: ColorScheme[] = [] -export { experimentalColorSchemes } - -const themes_directory = path.resolve(`${__dirname}/themes`) - -function for_all_color_schemes_in( - themesPath: string, - callback: (module: any, path: string) => void -) { - for (const fileName of fs.readdirSync(themesPath)) { - if (fileName == "template.ts") continue - const filePath = path.join(themesPath, fileName) - - if (fs.statSync(filePath).isFile()) { - const colorScheme = require(filePath) - callback(colorScheme, path.basename(filePath)) - } - } -} - -function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) { - for_all_color_schemes_in(themesPath, (colorScheme, _path) => { - if (colorScheme.dark) colorSchemes.push(colorScheme.dark) - if (colorScheme.light) colorSchemes.push(colorScheme.light) - }) -} - -fillColorSchemes(themes_directory, colorSchemes) -fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes) - -function fillMeta(themesPath: string, meta: Meta[]) { - for_all_color_schemes_in(themesPath, (colorScheme, path) => { - if (colorScheme.meta) { - meta.push(colorScheme.meta) - } else { - throw Error(`Public theme ${path} must have a meta field`) - } - }) -} - -fillMeta(themes_directory, schemeMeta) diff --git a/styles/src/common.ts b/styles/src/common.ts index 1b4fb6b37b5c4a660afe20595937a211d7704da5..5b6ddff4226fe379e7589cda8658ef7986f18838 100644 --- a/styles/src/common.ts +++ b/styles/src/common.ts @@ -1,3 +1,7 @@ +import chroma from "chroma-js" +export * from "./themes/common" +export { chroma } + export const fontFamilies = { sans: "Zed Sans", mono: "Zed Mono", @@ -23,6 +27,7 @@ export type FontWeight = | "bold" | "extra_bold" | "black" + export const fontWeights: { [key: string]: FontWeight } = { thin: "thin", extra_light: "extra_light", diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 6238e1abe1e07a8df61094c82c6aacd11ac32548..a9700a8d9994f0b8f63b74862b8db26c873a37da 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -22,6 +22,7 @@ import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" import welcome from "./welcome" import copilot from "./copilot" +import assistant from "./assistant" export default function app(colorScheme: ColorScheme): Object { return { @@ -50,6 +51,7 @@ export default function app(colorScheme: ColorScheme): Object { simpleMessageNotification: simpleMessageNotification(colorScheme), tooltip: tooltip(colorScheme), terminal: terminal(colorScheme), + assistant: assistant(colorScheme), feedback: feedback(colorScheme), colorScheme: { ...colorScheme, diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e33967b50c30975184ead9094f75d2b3fdbe71d --- /dev/null +++ b/styles/src/styleTree/assistant.ts @@ -0,0 +1,85 @@ +import { ColorScheme } from "../themes/common/colorScheme" +import { text, border, background, foreground } from "./components" +import editor from "./editor" + +export default function assistant(colorScheme: ColorScheme) { + const layer = colorScheme.highest; + return { + container: { + background: editor(colorScheme).background, + padding: { left: 12 } + }, + header: { + border: border(layer, "default", { bottom: true, top: true }), + margin: { bottom: 6, top: 6 }, + background: editor(colorScheme).background + }, + userSender: { + ...text(layer, "sans", "default", { size: "sm", weight: "bold" }), + }, + assistantSender: { + ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), + }, + systemSender: { + ...text(layer, "sans", "variant", { size: "sm", weight: "bold" }), + }, + sentAt: { + margin: { top: 2, left: 8 }, + ...text(layer, "sans", "default", { size: "2xs" }), + }, + modelInfoContainer: { + margin: { right: 16, top: 4 }, + }, + model: { + background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + padding: 4, + cornerRadius: 4, + ...text(layer, "sans", "default", { size: "xs" }), + hover: { + background: background(layer, "on", "hovered"), + } + }, + remainingTokens: { + background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + padding: 4, + margin: { left: 4 }, + cornerRadius: 4, + ...text(layer, "sans", "positive", { size: "xs" }), + }, + noRemainingTokens: { + background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + padding: 4, + margin: { left: 4 }, + cornerRadius: 4, + ...text(layer, "sans", "negative", { size: "xs" }), + }, + errorIcon: { + margin: { left: 8 }, + color: foreground(layer, "negative"), + width: 12, + }, + apiKeyEditor: { + background: background(layer, "on"), + cornerRadius: 6, + text: text(layer, "mono", "on"), + placeholderText: text(layer, "mono", "on", "disabled", { + size: "xs", + }), + selection: colorScheme.players[0], + border: border(layer, "on"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + }, + apiKeyPrompt: { + padding: 10, + ...text(layer, "sans", "default", { size: "xs" }), + } + } +} diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index 42eb3b994980a6942f33d9a32b21c7fd33d859eb..7b76cde444a68ff6ab57a0a22afa2dcfb71e52db 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -2,7 +2,7 @@ import picker from "./picker" import { ColorScheme } from "../themes/common/colorScheme" import { background, border, foreground, text } from "./components" -export default function contactFinder(colorScheme: ColorScheme) { +export default function contactFinder(colorScheme: ColorScheme): any { let layer = colorScheme.middle const sideMargin = 6 diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index fa0aec2012dee96d07e4359761c1d723fc41f0bd..66074aa684bc5719ac7ce7061e4afb8b28b7e95e 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -112,7 +112,7 @@ export default function editor(colorScheme: ColorScheme) { widthEm: 0.15, cornerRadius: 0.05, }, - /** Highlights matching occurences of what is under the cursor + /** Highlights matching occurrences of what is under the cursor * as well as matched brackets */ documentHighlightReadBackground: withOpacity( diff --git a/styles/src/styleTree/picker.ts b/styles/src/styleTree/picker.ts index 3245112345b289f456881f4f8e9846a1cbdf6ff5..66a0d2c126a0f31fb07bece5d3725c9e375a73ca 100644 --- a/styles/src/styleTree/picker.ts +++ b/styles/src/styleTree/picker.ts @@ -2,7 +2,7 @@ import { ColorScheme } from "../themes/common/colorScheme" import { withOpacity } from "../utils/color" import { background, border, text } from "./components" -export default function picker(colorScheme: ColorScheme) { +export default function picker(colorScheme: ColorScheme): any { let layer = colorScheme.lowest const container = { background: background(layer), @@ -28,7 +28,7 @@ export default function picker(colorScheme: ColorScheme) { bottom: 4, }, } - const emptyInputEditor = { ...inputEditor } + const emptyInputEditor: any = { ...inputEditor } delete emptyInputEditor.border delete emptyInputEditor.margin diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 08117bf6b01d5fe7b14f2e0ace22dc680c5cefac..d2cc1294526470f33d75cee3da4cbdb52eee00e0 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -8,10 +8,10 @@ export default function projectPanel(colorScheme: ColorScheme) { let layer = colorScheme.middle let baseEntry = { - height: 24, + height: 22, iconColor: foreground(layer, "variant"), - iconSize: 8, - iconSpacing: 8, + iconSize: 7, + iconSpacing: 5, } let status = { @@ -71,8 +71,8 @@ export default function projectPanel(colorScheme: ColorScheme) { }, }, background: background(layer), - padding: { left: 12, right: 12, top: 6, bottom: 6 }, - indentWidth: 8, + padding: { left: 6, right: 6, top: 0, bottom: 6 }, + indentWidth: 12, entry, draggedEntry: { ...baseEntry, @@ -83,7 +83,12 @@ export default function projectPanel(colorScheme: ColorScheme) { }, ignoredEntry: { ...entry, + iconColor: foreground(layer, "disabled"), text: text(layer, "mono", "disabled"), + active: { + ...entry.active, + iconColor: foreground(layer, "variant"), + } }, cutEntry: { ...entry, diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts index 6fc8a95d7dbde571103d6d1ebebf4265837ba1b4..2094a2e369739327d968a6616ebab0b373f916f0 100644 --- a/styles/src/styleTree/search.ts +++ b/styles/src/styleTree/search.ts @@ -33,7 +33,7 @@ export default function search(colorScheme: ColorScheme) { }; return { - // TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive matchBackground: withOpacity(foreground(layer, "accent"), 0.4), optionButton: { ...text(layer, "mono", "on"), diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index 61d30be7e03c8eceb026ea3f0fa1c39eab9cca60..39a1ef0407ce92b966e45f19f17a89c3c248c59c 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) { hover: { color: foreground(layer, "hovered"), }, + active: { + color: foreground(layer, "accent"), + } }, paneButtonContainer: { background: tab.background, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index cf5234aa004eb29022a967f6bbdfcd135b0e5dbf..ec992c9c0b368aa7ee792aa3b2ce5c2da3045d2c 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -13,6 +13,7 @@ import tabBar from "./tabBar" export default function workspace(colorScheme: ColorScheme) { const layer = colorScheme.lowest + const isLight = colorScheme.isLight const itemSpacing = 8 const titlebarButton = { cornerRadius: 6, @@ -120,16 +121,18 @@ export default function workspace(colorScheme: ColorScheme) { }, zoomedBackground: { cursor: "Arrow", - background: withOpacity(background(colorScheme.lowest), 0.85) + background: isLight + ? withOpacity(background(colorScheme.lowest), 0.8) + : withOpacity(background(colorScheme.highest), 0.6) }, zoomedPaneForeground: { - margin: 10, + margin: 16, shadow: colorScheme.modalShadow, - border: border(colorScheme.highest, { overlay: true }), + border: border(colorScheme.lowest, { overlay: true }), }, zoomedPanelForeground: { - margin: 18, - border: border(colorScheme.highest, { overlay: true }), + margin: 16, + border: border(colorScheme.lowest, { overlay: true }), }, dock: { left: { diff --git a/styles/src/system/types.ts b/styles/src/system/types.ts index 8bfa2dd7dbf9608f4549fdc2adde412c7da5022d..8de65a37eb131ba412fe269529106d58070669ee 100644 --- a/styles/src/system/types.ts +++ b/styles/src/system/types.ts @@ -1,6 +1,6 @@ import { Curve } from "./ref/curves" -export interface ColorAccessiblityValue { +export interface ColorAccessibilityValue { value: number aaPass: boolean aaaPass: boolean @@ -12,14 +12,14 @@ export interface ColorAccessiblityValue { * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette. * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information. */ -export interface ColorAccessiblity { - black: ColorAccessiblityValue - white: ColorAccessiblityValue +export interface ColorAccessibility { + black: ColorAccessibilityValue + white: ColorAccessibilityValue } export type Color = { step: number - contrast: ColorAccessiblity + contrast: ColorAccessibility hex: string lch: number[] rgba: number[] diff --git a/styles/src/themeConfig.ts b/styles/src/themeConfig.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c62b94c52b2f4ea560c6a77093753b39581ab6d --- /dev/null +++ b/styles/src/themeConfig.ts @@ -0,0 +1,143 @@ +import { Scale, Color } from "chroma-js" +import { Syntax } from "./themes/common/syntax" + +interface ThemeMeta { + /** The name of the theme */ + name: string + /** The theme's appearance. Either `light` or `dark`. */ + appearance: ThemeAppearance + /** The author of the theme + * + * Ideally formatted as `Full Name ` + * + * Example: `John Doe ` + */ + author: string + /** SPDX License string + * + * Example: `MIT` + */ + licenseType?: string | ThemeLicenseType + licenseUrl?: string + licenseFile: string + themeUrl?: string +} + +export interface ThemeConfigInputColors { + neutral: Scale + red: Scale + orange: Scale + yellow: Scale + green: Scale + cyan: Scale + blue: Scale + violet: Scale + magenta: Scale +} + +export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors + +/** Allow any part of a syntax highlight style to be overriden by the theme + * + * Example: + * ```ts + * override: { + * syntax: { + * boolean: { + * underline: true, + * }, + * }, + * } + * ``` + */ +export type ThemeConfigInputSyntax = Partial + +interface ThemeConfigOverrides { + syntax: ThemeConfigInputSyntax +} + +type ThemeConfigProperties = ThemeMeta & { + inputColor: ThemeConfigInputColors + override: ThemeConfigOverrides +} + +// This should be the format a theme is defined as +export type ThemeConfig = { + [K in keyof ThemeConfigProperties]: ThemeConfigProperties[K] +} + +interface ThemeColors { + neutral: string[] + red: string[] + orange: string[] + yellow: string[] + green: string[] + cyan: string[] + blue: string[] + violet: string[] + magenta: string[] +} + +type ThemeSyntax = Required + +export type ThemeProperties = ThemeMeta & { + color: ThemeColors + syntax: ThemeSyntax +} + +// This should be a theme after all its properties have been resolved +export type Theme = { + [K in keyof ThemeProperties]: ThemeProperties[K] +} + +export enum ThemeAppearance { + Light = "light", + Dark = "dark", +} + +export enum ThemeLicenseType { + MIT = "MIT", + Apache2 = "Apache License 2.0", +} + +export type ThemeFamilyItem = + | ThemeConfig + | { light: ThemeConfig; dark: ThemeConfig } + +type ThemeFamilyProperties = Partial> & { + name: string + default: ThemeFamilyItem + variants: { + [key: string]: ThemeFamilyItem + } +} + +// Idea: A theme family is a collection of themes that share the same name +// For example, a theme family could be `One Dark` and have a `light` and `dark` variant +// The Ayu family could have `light`, `mirage`, and `dark` variants + +type ThemeFamily = { + [K in keyof ThemeFamilyProperties]: ThemeFamilyProperties[K] +} + +/** The collection of all themes + * + * Example: + * ```ts + * { + * one_dark, + * one_light, + * ayu: { + * name: 'Ayu', + * default: 'ayu_mirage', + * variants: { + * light: 'ayu_light', + * mirage: 'ayu_mirage', + * dark: 'ayu_dark', + * }, + * }, + * ... + * } + * ``` + */ +export type ThemeIndex = Record diff --git a/styles/src/themes/andromeda.ts b/styles/src/themes/andromeda.ts deleted file mode 100644 index 7eba7b1481aeba8b1429b323bb51c0f78c0a6069..0000000000000000000000000000000000000000 --- a/styles/src/themes/andromeda.ts +++ /dev/null @@ -1,45 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Andromeda" - -const ramps = { - neutral: chroma - .scale([ - "#1E2025", - "#23262E", - "#292E38", - "#2E323C", - "#ACA8AE", - "#CBC9CF", - "#E1DDE4", - "#F7F7F8", - ]) - .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]), - red: colorRamp(chroma("#F92672")), - orange: colorRamp(chroma("#F39C12")), - yellow: colorRamp(chroma("#FFE66D")), - green: colorRamp(chroma("#96E072")), - cyan: colorRamp(chroma("#00E8C6")), - blue: colorRamp(chroma("#0CA793")), - violet: colorRamp(chroma("#8A3FA6")), - magenta: colorRamp(chroma("#C74DED")), -} - -export const dark = createColorScheme(name, false, ramps) - -export const meta: Meta = { - name, - author: "EliverLara", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md", - license_checksum: - "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89", - }, - }, - url: "https://github.com/EliverLara/Andromeda", -} diff --git a/styles/src/themes/andromeda/LICENSE b/styles/src/themes/andromeda/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..bdd549491fac6822878157337aa5dc4d09ef53f2 --- /dev/null +++ b/styles/src/themes/andromeda/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/andromeda/andromeda.ts b/styles/src/themes/andromeda/andromeda.ts new file mode 100644 index 0000000000000000000000000000000000000000..52c29bb2ec2071670c2c0b7f4f6dfd990d834381 --- /dev/null +++ b/styles/src/themes/andromeda/andromeda.ts @@ -0,0 +1,39 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +export const dark: ThemeConfig = { + name: "Andromeda", + author: "EliverLara", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/EliverLara/Andromeda", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma + .scale([ + "#1E2025", + "#23262E", + "#292E38", + "#2E323C", + "#ACA8AE", + "#CBC9CF", + "#E1DDE4", + "#F7F7F8", + ]) + .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]), + red: colorRamp(chroma("#F92672")), + orange: colorRamp(chroma("#F39C12")), + yellow: colorRamp(chroma("#FFE66D")), + green: colorRamp(chroma("#96E072")), + cyan: colorRamp(chroma("#00E8C6")), + blue: colorRamp(chroma("#0CA793")), + violet: colorRamp(chroma("#8A3FA6")), + magenta: colorRamp(chroma("#C74DED")), + }, + override: { syntax: {} }, +} diff --git a/styles/src/themes/atelier/LICENSE b/styles/src/themes/atelier/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..9f92967a0436d7118c20cf29bfb8844dba2699b1 --- /dev/null +++ b/styles/src/themes/atelier/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 Bram de Haan, http://atelierbramdehaan.nl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/atelier-cave-dark.ts b/styles/src/themes/atelier/atelier-cave-dark.ts similarity index 66% rename from styles/src/themes/atelier-cave-dark.ts rename to styles/src/themes/atelier/atelier-cave-dark.ts index a56e22cd92373997cc40d19eddf5b05e9cad5a02..ebec67b4c276639f22427182bd21dc72e8acbcfb 100644 --- a/styles/src/themes/atelier-cave-dark.ts +++ b/styles/src/themes/atelier/atelier-cave-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Cave Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", - }, colors: { base00: "#19171c", base01: "#26232a", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Cave Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-cave-light.ts b/styles/src/themes/atelier/atelier-cave-light.ts similarity index 68% rename from styles/src/themes/atelier-cave-light.ts rename to styles/src/themes/atelier/atelier-cave-light.ts index 3b180752cf97ea1bcc515c904deb20eafc03aea6..c1b7a05d4718171a81e4f8c45e0a70ee62e73625 100644 --- a/styles/src/themes/atelier-cave-light.ts +++ b/styles/src/themes/atelier/atelier-cave-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Cave Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", - }, colors: { base00: "#efecf4", base01: "#e2dfe7", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Cave Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-dune-dark.ts b/styles/src/themes/atelier/atelier-dune-dark.ts similarity index 66% rename from styles/src/themes/atelier-dune-dark.ts rename to styles/src/themes/atelier/atelier-dune-dark.ts index 0ab402a99d4adf8f1e0ed41979d5cc9973696404..c2ebc424e77c6b30ad693c86e0c932f204f91a20 100644 --- a/styles/src/themes/atelier-dune-dark.ts +++ b/styles/src/themes/atelier/atelier-dune-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Dune Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/", - }, colors: { base00: "#20201d", base01: "#292824", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Dune Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-dune-light.ts b/styles/src/themes/atelier/atelier-dune-light.ts similarity index 68% rename from styles/src/themes/atelier-dune-light.ts rename to styles/src/themes/atelier/atelier-dune-light.ts index e6a09985d5e09f8612d0a41f7b3a898991b9b720..01cb1d67cba048c2437c44c495a769eeab71d68e 100644 --- a/styles/src/themes/atelier-dune-light.ts +++ b/styles/src/themes/atelier/atelier-dune-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Dune Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune/", - }, colors: { base00: "#fefbec", base01: "#e8e4cf", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Dune Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-estuary-dark.ts b/styles/src/themes/atelier/atelier-estuary-dark.ts similarity index 66% rename from styles/src/themes/atelier-estuary-dark.ts rename to styles/src/themes/atelier/atelier-estuary-dark.ts index c4ec50c1e00ed7731d069ed3f559204b9030b10c..8e32c1f68f5d78d115c7bf4aec999c442d882a67 100644 --- a/styles/src/themes/atelier-estuary-dark.ts +++ b/styles/src/themes/atelier/atelier-estuary-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Estuary Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary/", - }, colors: { base00: "#22221b", base01: "#302f27", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Estuary Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-estuary-light.ts b/styles/src/themes/atelier/atelier-estuary-light.ts similarity index 67% rename from styles/src/themes/atelier-estuary-light.ts rename to styles/src/themes/atelier/atelier-estuary-light.ts index 6fce0e44833d2d0bf8657a3465bcfb601b262644..75fcb8e8302d26ab747bb42b3c07cda630fa1c7f 100644 --- a/styles/src/themes/atelier-estuary-light.ts +++ b/styles/src/themes/atelier/atelier-estuary-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Estuary Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary/", - }, colors: { base00: "#f4f3ec", base01: "#e7e6df", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Estuary Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-forest-dark.ts b/styles/src/themes/atelier/atelier-forest-dark.ts similarity index 66% rename from styles/src/themes/atelier-forest-dark.ts rename to styles/src/themes/atelier/atelier-forest-dark.ts index 7c47c55a830ed780f662d83d9f768d26207dc31e..7ee7ae4ab13681a5413dc4cc97f3166c5dbed1a2 100644 --- a/styles/src/themes/atelier-forest-dark.ts +++ b/styles/src/themes/atelier/atelier-forest-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Forest Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest/", - }, colors: { base00: "#1b1918", base01: "#2c2421", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Forest Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-forest-light.ts b/styles/src/themes/atelier/atelier-forest-light.ts similarity index 67% rename from styles/src/themes/atelier-forest-light.ts rename to styles/src/themes/atelier/atelier-forest-light.ts index 8ce06164769a7a8dfa15fc74a77b6aa0f3c9e3af..17d3b63d8832cf7bd1cee3ba4ae65cae102338be 100644 --- a/styles/src/themes/atelier-forest-light.ts +++ b/styles/src/themes/atelier/atelier-forest-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Forest Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest/", - }, colors: { base00: "#f1efee", base01: "#e6e2e0", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Forest Light`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-heath-dark.ts b/styles/src/themes/atelier/atelier-heath-dark.ts similarity index 66% rename from styles/src/themes/atelier-heath-dark.ts rename to styles/src/themes/atelier/atelier-heath-dark.ts index 87458ab8f5ab68a30e602de9b62edbe1e4cf5c18..11751367a3f9d01d490d8ec9961e3f0cc092464d 100644 --- a/styles/src/themes/atelier-heath-dark.ts +++ b/styles/src/themes/atelier/atelier-heath-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Heath Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/", - }, colors: { base00: "#1b181b", base01: "#292329", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Heath Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-heath-light.ts b/styles/src/themes/atelier/atelier-heath-light.ts similarity index 67% rename from styles/src/themes/atelier-heath-light.ts rename to styles/src/themes/atelier/atelier-heath-light.ts index 3db34370418f1cd27e305a3175e045e591e36ae9..07f4a9b3cb3194abda570408f1642355815a66f6 100644 --- a/styles/src/themes/atelier-heath-light.ts +++ b/styles/src/themes/atelier/atelier-heath-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Heath Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath/", - }, colors: { base00: "#f7f3f7", base01: "#d8cad8", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Heath Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-lakeside-dark.ts b/styles/src/themes/atelier/atelier-lakeside-dark.ts similarity index 66% rename from styles/src/themes/atelier-lakeside-dark.ts rename to styles/src/themes/atelier/atelier-lakeside-dark.ts index a8297ef9fd307243f1c9c84f3caa1e627120a19e..b1c98ddfdf20aa45b614392436e4b764482b3288 100644 --- a/styles/src/themes/atelier-lakeside-dark.ts +++ b/styles/src/themes/atelier/atelier-lakeside-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Lakeside Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/", - }, colors: { base00: "#161b1d", base01: "#1f292e", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Lakeside Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-lakeside-light.ts b/styles/src/themes/atelier/atelier-lakeside-light.ts similarity index 67% rename from styles/src/themes/atelier-lakeside-light.ts rename to styles/src/themes/atelier/atelier-lakeside-light.ts index 408fabcaf36a50df5229ea0034d12c67bd3b3562..d960444defa0ef3615c480d7e2d82e5573435089 100644 --- a/styles/src/themes/atelier-lakeside-light.ts +++ b/styles/src/themes/atelier/atelier-lakeside-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Lakeside Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside/", - }, colors: { base00: "#ebf8ff", base01: "#c1e4f6", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Lakeside Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-plateau-dark.ts b/styles/src/themes/atelier/atelier-plateau-dark.ts similarity index 66% rename from styles/src/themes/atelier-plateau-dark.ts rename to styles/src/themes/atelier/atelier-plateau-dark.ts index 731cb824e44eba0590c5f1dc4804121aa7f97cd9..74693b24fd35e7f705ecbd71feb1360aa38e3133 100644 --- a/styles/src/themes/atelier-plateau-dark.ts +++ b/styles/src/themes/atelier/atelier-plateau-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Plateau Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau/", - }, colors: { base00: "#1b1818", base01: "#292424", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Plateau Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-plateau-light.ts b/styles/src/themes/atelier/atelier-plateau-light.ts similarity index 67% rename from styles/src/themes/atelier-plateau-light.ts rename to styles/src/themes/atelier/atelier-plateau-light.ts index 96f295a69518b1216482776e44d5b36912f526da..dd3130cea0d191966e0c6d4cb4d11fdfa41047b1 100644 --- a/styles/src/themes/atelier-plateau-light.ts +++ b/styles/src/themes/atelier/atelier-plateau-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Plateau Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau/", - }, colors: { base00: "#f4ecec", base01: "#e7dfdf", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Plateau Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-savanna-dark.ts b/styles/src/themes/atelier/atelier-savanna-dark.ts similarity index 66% rename from styles/src/themes/atelier-savanna-dark.ts rename to styles/src/themes/atelier/atelier-savanna-dark.ts index dfcb4f27cbedd41608b941da97172d083e7a8e76..c387ac5ae989ee75fcb5bd8eddfc7fd41840a53f 100644 --- a/styles/src/themes/atelier-savanna-dark.ts +++ b/styles/src/themes/atelier/atelier-savanna-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Savanna Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna/", - }, colors: { base00: "#171c19", base01: "#232a25", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Savanna Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-savanna-light.ts b/styles/src/themes/atelier/atelier-savanna-light.ts similarity index 67% rename from styles/src/themes/atelier-savanna-light.ts rename to styles/src/themes/atelier/atelier-savanna-light.ts index 4bc1389fc9cf3cb3b4870a58665a510a65d4c9e2..64edd406a8982abe6f6b76e043931a0ff4b20c3c 100644 --- a/styles/src/themes/atelier-savanna-light.ts +++ b/styles/src/themes/atelier/atelier-savanna-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Savanna Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna/", - }, colors: { base00: "#ecf4ee", base01: "#dfe7e2", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Savanna Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-seaside-dark.ts b/styles/src/themes/atelier/atelier-seaside-dark.ts similarity index 66% rename from styles/src/themes/atelier-seaside-dark.ts rename to styles/src/themes/atelier/atelier-seaside-dark.ts index 1326a277861e417711c3ea30d39230b227c9ac51..dbccb96013c8a06cadc05224d9857dbd3637bdd1 100644 --- a/styles/src/themes/atelier-seaside-dark.ts +++ b/styles/src/themes/atelier/atelier-seaside-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Seaside Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/", - }, colors: { base00: "#131513", base01: "#242924", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Seaside Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-seaside-light.ts b/styles/src/themes/atelier/atelier-seaside-light.ts similarity index 67% rename from styles/src/themes/atelier-seaside-light.ts rename to styles/src/themes/atelier/atelier-seaside-light.ts index 6f6823718aed9ba4e3d7325b9968b11ba5472b40..a9c034ed4406c449baeac12a10644704b2d3dc4d 100644 --- a/styles/src/themes/atelier-seaside-light.ts +++ b/styles/src/themes/atelier/atelier-seaside-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Seaside Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside/", - }, colors: { base00: "#f4fbf4", base01: "#cfe8cf", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Seaside Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-sulphurpool-dark.ts b/styles/src/themes/atelier/atelier-sulphurpool-dark.ts similarity index 66% rename from styles/src/themes/atelier-sulphurpool-dark.ts rename to styles/src/themes/atelier/atelier-sulphurpool-dark.ts index dcfc0d932a67d7081f5bcdb195f6fc57cfc2d79f..edfc518b8e9d4972bdc5132f6e2e4811ad2166b4 100644 --- a/styles/src/themes/atelier-sulphurpool-dark.ts +++ b/styles/src/themes/atelier/atelier-sulphurpool-dark.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Sulphurpool Dark`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/", - }, colors: { base00: "#202746", base01: "#293256", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - false, - { + return { + name: `${meta.name} Sulphurpool Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -57,10 +54,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/atelier-sulphurpool-light.ts b/styles/src/themes/atelier/atelier-sulphurpool-light.ts similarity index 67% rename from styles/src/themes/atelier-sulphurpool-light.ts rename to styles/src/themes/atelier/atelier-sulphurpool-light.ts index b2b5f7c328ba86be296be0fb938bf850571dbef9..fbef6683bf99d83a9ba1c125c4a0790d7ce23981 100644 --- a/styles/src/themes/atelier-sulphurpool-light.ts +++ b/styles/src/themes/atelier/atelier-sulphurpool-light.ts @@ -1,14 +1,7 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" -import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common" +import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" +import { meta, buildSyntax, Variant } from "./common" const variant: Variant = { - meta: { - name: `${name} Sulphurpool Light`, - ...metaCommon, - url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool/", - }, colors: { base00: "#f5f7ff", base01: "#dfe2f1", @@ -31,13 +24,17 @@ const variant: Variant = { const syntax = buildSyntax(variant) -const theme = (variant: Variant) => { - const { meta, colors } = variant +const getTheme = (variant: Variant): ThemeConfig => { + const { colors } = variant - return createColorScheme( - meta.name, - true, - { + return { + name: `${meta.name} Sulphurpool Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: { neutral: chroma.scale( [ colors.base00, @@ -59,10 +56,8 @@ const theme = (variant: Variant) => { violet: colorRamp(chroma(colors.base0E)), magenta: colorRamp(chroma(colors.base0F)), }, - syntax - ) + override: { syntax }, + } } -export const dark = theme(variant) - -export const meta: Meta = variant.meta +export const theme = getTheme(variant) diff --git a/styles/src/themes/common/atelier-common.ts b/styles/src/themes/atelier/common.ts similarity index 77% rename from styles/src/themes/common/atelier-common.ts rename to styles/src/themes/atelier/common.ts index 08a915d01948f300441d0904cbff2fd406e3dcba..961ca64270135eb28affe6818b3a8c409ade864e 100644 --- a/styles/src/themes/common/atelier-common.ts +++ b/styles/src/themes/atelier/common.ts @@ -1,7 +1,6 @@ -import { License, Meta, ThemeSyntax } from "./colorScheme" +import { ThemeLicenseType, ThemeConfig, ThemeSyntax } from "../../common" export interface Variant { - meta: Meta colors: { base00: string base01: string @@ -22,19 +21,12 @@ export interface Variant { } } -export const metaCommon: { - author: string - license: License -} = { +export const meta: Partial = { + name: "Atelier", author: "Bram de Haan (http://atelierbramdehaan.nl)", - license: { - SPDX: "MIT", - license_text: { - https_url: "https://atelierbram.mit-license.org/license.txt", - license_checksum: - "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5", - }, - }, + licenseType: ThemeLicenseType.MIT, + licenseUrl: + "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", } export const buildSyntax = (variant: Variant): ThemeSyntax => { @@ -62,5 +54,3 @@ export const buildSyntax = (variant: Variant): ThemeSyntax => { keyword: { color: colors.base0E }, } } - -export const name = "Atelier" diff --git a/styles/src/themes/ayu-dark.ts b/styles/src/themes/ayu-dark.ts deleted file mode 100644 index c7e86994feec6a78879b383e5bf71941084c0b9e..0000000000000000000000000000000000000000 --- a/styles/src/themes/ayu-dark.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createColorScheme } from "./common/ramps" -import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common" - -export const meta = { - ...themeMeta, - name: `${themeMeta.name} Dark` -} - -const variant = ayu.dark -const theme = buildTheme(variant, false) - -export const dark = createColorScheme( - meta.name, - false, - theme.ramps, - theme.syntax -) diff --git a/styles/src/themes/ayu-light.ts b/styles/src/themes/ayu-light.ts deleted file mode 100644 index 9acabf6a3957a20aca2e3fa0181cf675840ddae4..0000000000000000000000000000000000000000 --- a/styles/src/themes/ayu-light.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createColorScheme } from "./common/ramps" -import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common" - -export const meta = { - ...themeMeta, - name: `${themeMeta.name} Light` -} - -const variant = ayu.light -const theme = buildTheme(variant, true) - -export const light = createColorScheme( - meta.name, - true, - theme.ramps, - theme.syntax -) diff --git a/styles/src/themes/ayu-mirage.ts b/styles/src/themes/ayu-mirage.ts deleted file mode 100644 index 2a01512673b73d104ead5fc20edf353b813c16bb..0000000000000000000000000000000000000000 --- a/styles/src/themes/ayu-mirage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createColorScheme } from "./common/ramps" -import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common" - -export const meta = { - ...themeMeta, - name: `${themeMeta.name} Mirage` -} - -const variant = ayu.mirage -const theme = buildTheme(variant, false) - -export const dark = createColorScheme( - meta.name, - false, - theme.ramps, - theme.syntax -) diff --git a/styles/src/themes/ayu/LICENSE b/styles/src/themes/ayu/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6b83ef0582f26b04f37f8b78ef5a3121b3f3a326 --- /dev/null +++ b/styles/src/themes/ayu/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Ike Ku + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/ayu/ayu-dark.ts b/styles/src/themes/ayu/ayu-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..7feddacd2b1543ecebba174709d3c8d59f12a278 --- /dev/null +++ b/styles/src/themes/ayu/ayu-dark.ts @@ -0,0 +1,16 @@ +import { ThemeAppearance, ThemeConfig } from "../../common" +import { ayu, meta, buildTheme } from "./common" + +const variant = ayu.dark +const { ramps, syntax } = buildTheme(variant, false) + +export const theme: ThemeConfig = { + name: `${meta.name} Dark`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: ramps, + override: { syntax }, +} diff --git a/styles/src/themes/ayu/ayu-light.ts b/styles/src/themes/ayu/ayu-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf023852471bdcbfe109127ce679c60695fdccaa --- /dev/null +++ b/styles/src/themes/ayu/ayu-light.ts @@ -0,0 +1,16 @@ +import { ThemeAppearance, ThemeConfig } from "../../common" +import { ayu, meta, buildTheme } from "./common" + +const variant = ayu.light +const { ramps, syntax } = buildTheme(variant, true) + +export const theme: ThemeConfig = { + name: `${meta.name} Light`, + author: meta.author, + appearance: ThemeAppearance.Light, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: ramps, + override: { syntax }, +} diff --git a/styles/src/themes/ayu/ayu-mirage.ts b/styles/src/themes/ayu/ayu-mirage.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2a69e7ab6a52fb8664516e4d3ee85b36fe19551 --- /dev/null +++ b/styles/src/themes/ayu/ayu-mirage.ts @@ -0,0 +1,16 @@ +import { ThemeAppearance, ThemeConfig } from "../../common" +import { ayu, meta, buildTheme } from "./common" + +const variant = ayu.mirage +const { ramps, syntax } = buildTheme(variant, false) + +export const theme: ThemeConfig = { + name: `${meta.name} Mirage`, + author: meta.author, + appearance: ThemeAppearance.Dark, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: ramps, + override: { syntax }, +} diff --git a/styles/src/themes/common/ayu-common.ts b/styles/src/themes/ayu/common.ts similarity index 83% rename from styles/src/themes/common/ayu-common.ts rename to styles/src/themes/ayu/common.ts index f08817ef492e7c56fac0e593ffd0d74d1e4d735b..26e21c19105796bc782844dd2760cb39dda8db09 100644 --- a/styles/src/themes/common/ayu-common.ts +++ b/styles/src/themes/ayu/common.ts @@ -1,8 +1,11 @@ import { dark, light, mirage } from "ayu" -import { ThemeSyntax } from "./syntax" -import chroma from "chroma-js" -import { colorRamp } from "./ramps" -import { Meta } from "./colorScheme" +import { + chroma, + colorRamp, + ThemeLicenseType, + ThemeConfig, + ThemeSyntax, +} from "../../common" export const ayu = { dark, @@ -74,17 +77,9 @@ export const buildSyntax = (t: typeof dark): ThemeSyntax => { } } -export const meta: Meta = { +export const meta: Partial = { name: "Ayu", author: "dempfi", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/dempfi/ayu/master/LICENSE", - license_checksum: - "e0af0e0d1754c18ca075649d42f5c6d9a60f8bdc03c20dfd97105f2253a94173", - }, - }, - url: "https://github.com/dempfi/ayu", + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/dempfi/ayu", } diff --git a/styles/src/themes/common/colorScheme.ts b/styles/src/themes/common/colorScheme.ts index 3b5b8f69329651f20a0ad5aef834eb9435e8b5d1..d4ef2ae0132068613664c494f7818e4d4021c916 100644 --- a/styles/src/themes/common/colorScheme.ts +++ b/styles/src/themes/common/colorScheme.ts @@ -1,6 +1,12 @@ -import { Scale } from "chroma-js" +import { Scale, Color } from "chroma-js" import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" export { Syntax, ThemeSyntax, SyntaxHighlightStyle } +import { + ThemeConfig, + ThemeAppearance, + ThemeConfigInputColors, +} from "../../themeConfig" +import { getRamps } from "./ramps" export interface ColorScheme { name: string @@ -28,13 +34,6 @@ export interface Meta { export interface License { SPDX: SPDXExpression - /// A url where we can download the license's text - license_text: Verification | string -} - -export interface Verification { - https_url: string - license_checksum: string } // License name -> License text @@ -105,3 +104,183 @@ export interface Style { border: string foreground: string } + +export function createColorScheme(theme: ThemeConfig): ColorScheme { + const { + name, + appearance, + inputColor, + override: { syntax }, + } = theme + + const isLight = appearance === ThemeAppearance.Light + const colorRamps: ThemeConfigInputColors = inputColor + + // Chromajs scales from 0 to 1 flipped if isLight is true + const ramps = getRamps(isLight, colorRamps) + const lowest = lowestLayer(ramps) + const middle = middleLayer(ramps) + const highest = highestLayer(ramps) + + const popoverShadow = { + blur: 4, + color: ramps + .neutral(isLight ? 7 : 0) + .darken() + .alpha(0.2) + .hex(), // TODO used blend previously. Replace with something else + offset: [1, 2], + } + + const modalShadow = { + blur: 16, + color: ramps + .neutral(isLight ? 7 : 0) + .darken() + .alpha(0.2) + .hex(), // TODO used blend previously. Replace with something else + offset: [0, 2], + } + + const players = { + "0": player(ramps.blue), + "1": player(ramps.green), + "2": player(ramps.magenta), + "3": player(ramps.orange), + "4": player(ramps.violet), + "5": player(ramps.cyan), + "6": player(ramps.red), + "7": player(ramps.yellow), + } + + return { + name, + isLight, + + ramps, + + lowest, + middle, + highest, + + popoverShadow, + modalShadow, + + players, + syntax, + } +} + +function player(ramp: Scale): Player { + return { + selection: ramp(0.5).alpha(0.24).hex(), + cursor: ramp(0.5).hex(), + } +} + +function lowestLayer(ramps: RampSet): Layer { + return { + base: buildStyleSet(ramps.neutral, 0.2, 1), + variant: buildStyleSet(ramps.neutral, 0.2, 0.7), + on: buildStyleSet(ramps.neutral, 0.1, 1), + accent: buildStyleSet(ramps.blue, 0.1, 0.5), + positive: buildStyleSet(ramps.green, 0.1, 0.5), + warning: buildStyleSet(ramps.yellow, 0.1, 0.5), + negative: buildStyleSet(ramps.red, 0.1, 0.5), + } +} + +function middleLayer(ramps: RampSet): Layer { + return { + base: buildStyleSet(ramps.neutral, 0.1, 1), + variant: buildStyleSet(ramps.neutral, 0.1, 0.7), + on: buildStyleSet(ramps.neutral, 0, 1), + accent: buildStyleSet(ramps.blue, 0.1, 0.5), + positive: buildStyleSet(ramps.green, 0.1, 0.5), + warning: buildStyleSet(ramps.yellow, 0.1, 0.5), + negative: buildStyleSet(ramps.red, 0.1, 0.5), + } +} + +function highestLayer(ramps: RampSet): Layer { + return { + base: buildStyleSet(ramps.neutral, 0, 1), + variant: buildStyleSet(ramps.neutral, 0, 0.7), + on: buildStyleSet(ramps.neutral, 0.1, 1), + accent: buildStyleSet(ramps.blue, 0.1, 0.5), + positive: buildStyleSet(ramps.green, 0.1, 0.5), + warning: buildStyleSet(ramps.yellow, 0.1, 0.5), + negative: buildStyleSet(ramps.red, 0.1, 0.5), + } +} + +function buildStyleSet( + ramp: Scale, + backgroundBase: number, + foregroundBase: number, + step: number = 0.08 +): StyleSet { + let styleDefinitions = buildStyleDefinition( + backgroundBase, + foregroundBase, + step + ) + + function colorString(indexOrColor: number | Color): string { + if (typeof indexOrColor === "number") { + return ramp(indexOrColor).hex() + } else { + return indexOrColor.hex() + } + } + + function buildStyle(style: Styles): Style { + return { + background: colorString(styleDefinitions.background[style]), + border: colorString(styleDefinitions.border[style]), + foreground: colorString(styleDefinitions.foreground[style]), + } + } + + return { + default: buildStyle("default"), + hovered: buildStyle("hovered"), + pressed: buildStyle("pressed"), + active: buildStyle("active"), + disabled: buildStyle("disabled"), + inverted: buildStyle("inverted"), + } +} + +function buildStyleDefinition( + bgBase: number, + fgBase: number, + step: number = 0.08 +) { + return { + background: { + default: bgBase, + hovered: bgBase + step, + pressed: bgBase + step * 1.5, + active: bgBase + step * 2.2, + disabled: bgBase, + inverted: fgBase + step * 6, + }, + border: { + default: bgBase + step * 1, + hovered: bgBase + step, + pressed: bgBase + step, + active: bgBase + step * 3, + disabled: bgBase + step * 0.5, + inverted: bgBase - step * 3, + }, + foreground: { + default: fgBase, + hovered: fgBase, + pressed: fgBase, + active: fgBase + step * 6, + disabled: bgBase + step * 4, + inverted: bgBase + step * 2, + }, + } +} diff --git a/styles/src/themes/common/index.ts b/styles/src/themes/common/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd7cf9b0f47b73d922f1877d7ed3ef1e2696788d --- /dev/null +++ b/styles/src/themes/common/index.ts @@ -0,0 +1,4 @@ +export * from "./colorScheme" +export * from "./ramps" +export * from "./syntax" +export * from "../../themeConfig" diff --git a/styles/src/themes/common/ramps.ts b/styles/src/themes/common/ramps.ts index 746b102a841dbc180a09a857f32ac3619df2e762..ef93ee163f05ff5f5557838fe4f1d0751f4c626f 100644 --- a/styles/src/themes/common/ramps.ts +++ b/styles/src/themes/common/ramps.ts @@ -1,14 +1,9 @@ import chroma, { Color, Scale } from "chroma-js" +import { RampSet } from "./colorScheme" import { - ColorScheme, - Layer, - Player, - RampSet, - Style, - Styles, - StyleSet, - ThemeSyntax, -} from "./colorScheme" + ThemeConfigInputColors, + ThemeConfigInputColorsKeys, +} from "../../themeConfig" export function colorRamp(color: Color): Scale { let endColor = color.desaturate(1).brighten(5) @@ -16,200 +11,37 @@ export function colorRamp(color: Color): Scale { return chroma.scale([startColor, color, endColor]).mode("lab") } -export function createColorScheme( - name: string, +/** + * Chromajs mutates the underlying ramp when you call domain. This causes problems because + we now store the ramps object in the theme so that we can pull colors out of them. + So instead of calling domain and storing the result, we have to construct new ramps for each + theme so that we don't modify the passed in ramps. + This combined with an error in the type definitions for chroma js means we have to cast the colors + function to any in order to get the colors back out from the original ramps. + * @param isLight + * @param colorRamps + * @returns + */ +export function getRamps( isLight: boolean, - colorRamps: { [rampName: string]: Scale }, - syntax?: ThemeSyntax -): ColorScheme { - // Chromajs scales from 0 to 1 flipped if isLight is true - let ramps: RampSet = {} as any + colorRamps: ThemeConfigInputColors +): RampSet { + const ramps: RampSet = {} as any + const colorsKeys = Object.keys(colorRamps) as ThemeConfigInputColorsKeys[] - // Chromajs mutates the underlying ramp when you call domain. This causes problems because - // we now store the ramps object in the theme so that we can pull colors out of them. - // So instead of calling domain and storing the result, we have to construct new ramps for each - // theme so that we don't modify the passed in ramps. - // This combined with an error in the type definitions for chroma js means we have to cast the colors - // function to any in order to get the colors back out from the original ramps. if (isLight) { - for (var rampName in colorRamps) { - ;(ramps as any)[rampName] = chroma.scale( + for (const rampName of colorsKeys) { + ramps[rampName] = chroma.scale( colorRamps[rampName].colors(100).reverse() ) } ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse()) } else { - for (var rampName in colorRamps) { - ;(ramps as any)[rampName] = chroma.scale( - colorRamps[rampName].colors(100) - ) + for (const rampName of colorsKeys) { + ramps[rampName] = chroma.scale(colorRamps[rampName].colors(100)) } ramps.neutral = chroma.scale(colorRamps.neutral.colors(100)) } - let lowest = lowestLayer(ramps) - let middle = middleLayer(ramps) - let highest = highestLayer(ramps) - - let popoverShadow = { - blur: 4, - color: ramps - .neutral(isLight ? 7 : 0) - .darken() - .alpha(0.2) - .hex(), // TODO used blend previously. Replace with something else - offset: [1, 2], - } - - let modalShadow = { - blur: 16, - color: ramps - .neutral(isLight ? 7 : 0) - .darken() - .alpha(0.2) - .hex(), // TODO used blend previously. Replace with something else - offset: [0, 2], - } - - let players = { - "0": player(ramps.blue), - "1": player(ramps.green), - "2": player(ramps.magenta), - "3": player(ramps.orange), - "4": player(ramps.violet), - "5": player(ramps.cyan), - "6": player(ramps.red), - "7": player(ramps.yellow), - } - - return { - name, - isLight, - - ramps, - - lowest, - middle, - highest, - - popoverShadow, - modalShadow, - - players, - syntax, - } -} - -function player(ramp: Scale): Player { - return { - selection: ramp(0.5).alpha(0.24).hex(), - cursor: ramp(0.5).hex(), - } -} - -function lowestLayer(ramps: RampSet): Layer { - return { - base: buildStyleSet(ramps.neutral, 0.2, 1), - variant: buildStyleSet(ramps.neutral, 0.2, 0.7), - on: buildStyleSet(ramps.neutral, 0.1, 1), - accent: buildStyleSet(ramps.blue, 0.1, 0.5), - positive: buildStyleSet(ramps.green, 0.1, 0.5), - warning: buildStyleSet(ramps.yellow, 0.1, 0.5), - negative: buildStyleSet(ramps.red, 0.1, 0.5), - } -} - -function middleLayer(ramps: RampSet): Layer { - return { - base: buildStyleSet(ramps.neutral, 0.1, 1), - variant: buildStyleSet(ramps.neutral, 0.1, 0.7), - on: buildStyleSet(ramps.neutral, 0, 1), - accent: buildStyleSet(ramps.blue, 0.1, 0.5), - positive: buildStyleSet(ramps.green, 0.1, 0.5), - warning: buildStyleSet(ramps.yellow, 0.1, 0.5), - negative: buildStyleSet(ramps.red, 0.1, 0.5), - } -} - -function highestLayer(ramps: RampSet): Layer { - return { - base: buildStyleSet(ramps.neutral, 0, 1), - variant: buildStyleSet(ramps.neutral, 0, 0.7), - on: buildStyleSet(ramps.neutral, 0.1, 1), - accent: buildStyleSet(ramps.blue, 0.1, 0.5), - positive: buildStyleSet(ramps.green, 0.1, 0.5), - warning: buildStyleSet(ramps.yellow, 0.1, 0.5), - negative: buildStyleSet(ramps.red, 0.1, 0.5), - } -} - -function buildStyleSet( - ramp: Scale, - backgroundBase: number, - foregroundBase: number, - step: number = 0.08 -): StyleSet { - let styleDefinitions = buildStyleDefinition( - backgroundBase, - foregroundBase, - step - ) - - function colorString(indexOrColor: number | Color): string { - if (typeof indexOrColor === "number") { - return ramp(indexOrColor).hex() - } else { - return indexOrColor.hex() - } - } - - function buildStyle(style: Styles): Style { - return { - background: colorString(styleDefinitions.background[style]), - border: colorString(styleDefinitions.border[style]), - foreground: colorString(styleDefinitions.foreground[style]), - } - } - - return { - default: buildStyle("default"), - hovered: buildStyle("hovered"), - pressed: buildStyle("pressed"), - active: buildStyle("active"), - disabled: buildStyle("disabled"), - inverted: buildStyle("inverted"), - } -} - -function buildStyleDefinition( - bgBase: number, - fgBase: number, - step: number = 0.08 -) { - return { - background: { - default: bgBase, - hovered: bgBase + step, - pressed: bgBase + step * 1.5, - active: bgBase + step * 2.2, - disabled: bgBase, - inverted: fgBase + step * 6, - }, - border: { - default: bgBase + step * 1, - hovered: bgBase + step, - pressed: bgBase + step, - active: bgBase + step * 3, - disabled: bgBase + step * 0.5, - inverted: bgBase - step * 3, - }, - foreground: { - default: fgBase, - hovered: fgBase, - pressed: fgBase, - active: fgBase + step * 6, - disabled: bgBase + step * 4, - inverted: bgBase + step * 2, - }, - } + return ramps } diff --git a/styles/src/themes/common/syntax.ts b/styles/src/themes/common/syntax.ts index 925ed7e5c14ba1308fee179113879c13db25d344..258b845142cc296491badf15ad6adc08beaf94d4 100644 --- a/styles/src/themes/common/syntax.ts +++ b/styles/src/themes/common/syntax.ts @@ -4,7 +4,7 @@ import { ColorScheme } from "./colorScheme" import chroma from "chroma-js" export interface SyntaxHighlightStyle { - color: string + color?: string weight?: FontWeight underline?: boolean italic?: boolean @@ -117,7 +117,7 @@ export interface Syntax { export type ThemeSyntax = Partial const defaultSyntaxHighlightStyle: Omit = { - weight: fontWeights.normal, + weight: "normal", underline: false, italic: false, } @@ -140,12 +140,14 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { // Mix the neutral and blue colors to get a // predictive color distinct from any other color in the theme - const predictive = chroma.mix( - colorScheme.ramps.neutral(0.4).hex(), - colorScheme.ramps.blue(0.4).hex(), - 0.45, - "lch" - ).hex() + const predictive = chroma + .mix( + colorScheme.ramps.neutral(0.4).hex(), + colorScheme.ramps.blue(0.4).hex(), + 0.45, + "lch" + ) + .hex() const color = { primary: colorScheme.ramps.neutral(1).hex(), diff --git a/styles/src/themes/gruvbox-dark-hard.ts b/styles/src/themes/gruvbox-dark-hard.ts deleted file mode 100644 index 3723de49015ab0cfe0f975a15943c4d4e7f35548..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox-dark-hard.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { darkHard as dark, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta } -meta.name = `${commonMeta.name} Dark Hard` - -export { dark, meta } diff --git a/styles/src/themes/gruvbox-dark-soft.ts b/styles/src/themes/gruvbox-dark-soft.ts deleted file mode 100644 index 2887572eadb6e757e0611b00301c3edb790fda59..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox-dark-soft.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { darkSoft as dark, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta } -meta.name = `${commonMeta.name} Dark Soft` - -export { dark, meta } diff --git a/styles/src/themes/gruvbox-dark.ts b/styles/src/themes/gruvbox-dark.ts deleted file mode 100644 index cff7bd8bf94af1a839c043bb93165d7077912c6f..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox-dark.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { darkDefault as dark, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta } -meta.name = `${commonMeta.name} Dark` - -export { dark, meta } diff --git a/styles/src/themes/gruvbox-light-hard.ts b/styles/src/themes/gruvbox-light-hard.ts deleted file mode 100644 index cf998ce588bfc56b168e05a3c03cd2c9a568dadd..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox-light-hard.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { lightHard as light, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta } -meta.name = `${commonMeta.name} Dark Soft` - -export { light, meta } diff --git a/styles/src/themes/gruvbox-light-soft.ts b/styles/src/themes/gruvbox-light-soft.ts deleted file mode 100644 index 90ec82e965e6ef5f36c173fee837162e0b1aefaf..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox-light-soft.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { lightSoft as light, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta } -meta.name = `${commonMeta.name} Light Soft` - -export { light, meta } diff --git a/styles/src/themes/gruvbox-light.ts b/styles/src/themes/gruvbox-light.ts deleted file mode 100644 index e8f355cd11482c3ca82462c5ca1fa239071e6b84..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox-light.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { lightDefault as light, meta as commonMeta } from "./gruvbox-common" - -let meta = { ...commonMeta } -meta.name = `${commonMeta.name} Light` - -export { light, meta } diff --git a/styles/src/themes/gruvbox/LICENSE b/styles/src/themes/gruvbox/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2a9230614399a48916e74cfb74bd4625686c7bcb --- /dev/null +++ b/styles/src/themes/gruvbox/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/gruvbox-common.ts b/styles/src/themes/gruvbox/gruvbox-common.ts similarity index 79% rename from styles/src/themes/gruvbox-common.ts rename to styles/src/themes/gruvbox/gruvbox-common.ts index c42362c11c007e5371ad7134c7300667fe04304e..f1c04a069c9d8e352eb95849e5232762b559a5bd 100644 --- a/styles/src/themes/gruvbox-common.ts +++ b/styles/src/themes/gruvbox/gruvbox-common.ts @@ -1,8 +1,18 @@ -import chroma from "chroma-js" -import { Meta, ThemeSyntax } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, + ThemeSyntax, +} from "../../common" -const name = "Gruvbox" +const meta: Partial = { + name: "Gruvbox", + author: "morhetz ", + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/morhetz/gruvbox", +} const color = { dark0_hard: "#1d2021", @@ -156,7 +166,7 @@ const variant: Variant[] = [ }, ] -const buildVariant = (variant: Variant) => { +const buildVariant = (variant: Variant): ThemeConfig => { const { colors } = variant const name = `Gruvbox ${variant.name}` @@ -233,7 +243,16 @@ const buildVariant = (variant: Variant) => { title: { color: colors.green }, } - return createColorScheme(name, isLight, ramps, syntax) + return { + name, + author: meta.author, + appearance: variant.appearance as ThemeAppearance, + licenseType: meta.licenseType, + licenseUrl: meta.licenseUrl, + licenseFile: `${__dirname}/LICENSE`, + inputColor: ramps, + override: { syntax }, + } } // Variants @@ -243,14 +262,3 @@ export const darkSoft = buildVariant(variant[2]) export const lightHard = buildVariant(variant[3]) export const lightDefault = buildVariant(variant[4]) export const lightSoft = buildVariant(variant[5]) - -export const meta: Meta = { - name, - license: { - SPDX: "MIT", // "MIT/X11" - license_text: - "Copyright \n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/ or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", - }, - author: "morhetz ", - url: "https://github.com/morhetz/gruvbox", -} diff --git a/styles/src/themes/gruvbox/gruvbox-dark-hard.ts b/styles/src/themes/gruvbox/gruvbox-dark-hard.ts new file mode 100644 index 0000000000000000000000000000000000000000..4102671189e856bbd0c508fc1d6be88ee55e8c0c --- /dev/null +++ b/styles/src/themes/gruvbox/gruvbox-dark-hard.ts @@ -0,0 +1 @@ +export { darkHard } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-dark-soft.ts b/styles/src/themes/gruvbox/gruvbox-dark-soft.ts new file mode 100644 index 0000000000000000000000000000000000000000..d550d63768d40aa97295c9a85922b3788cbf2f96 --- /dev/null +++ b/styles/src/themes/gruvbox/gruvbox-dark-soft.ts @@ -0,0 +1 @@ +export { darkSoft } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-dark.ts b/styles/src/themes/gruvbox/gruvbox-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..05850028a40cb583d8a3beb7b706c52e0523e81c --- /dev/null +++ b/styles/src/themes/gruvbox/gruvbox-dark.ts @@ -0,0 +1 @@ +export { darkDefault } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-light-hard.ts b/styles/src/themes/gruvbox/gruvbox-light-hard.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec3cddec758379f6bf84b165d04eed31373e34a4 --- /dev/null +++ b/styles/src/themes/gruvbox/gruvbox-light-hard.ts @@ -0,0 +1 @@ +export { lightHard } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-light-soft.ts b/styles/src/themes/gruvbox/gruvbox-light-soft.ts new file mode 100644 index 0000000000000000000000000000000000000000..0888e847aca9ffdfb8a7147d058cd44f35e4562c --- /dev/null +++ b/styles/src/themes/gruvbox/gruvbox-light-soft.ts @@ -0,0 +1 @@ +export { lightSoft } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-light.ts b/styles/src/themes/gruvbox/gruvbox-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f53ce529965a65a7cb0901ea2703f10b4502922 --- /dev/null +++ b/styles/src/themes/gruvbox/gruvbox-light.ts @@ -0,0 +1 @@ +export { lightDefault } from "./gruvbox-common" diff --git a/styles/src/themes/index.ts b/styles/src/themes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..52ce1b93272d8f1e0553943c44385ee10b56831e --- /dev/null +++ b/styles/src/themes/index.ts @@ -0,0 +1,82 @@ +import { ThemeConfig } from "./common" +import { darkDefault as gruvboxDark } from "./gruvbox/gruvbox-dark" +import { darkHard as gruvboxDarkHard } from "./gruvbox/gruvbox-dark-hard" +import { darkSoft as gruvboxDarkSoft } from "./gruvbox/gruvbox-dark-soft" +import { lightDefault as gruvboxLight } from "./gruvbox/gruvbox-light" +import { lightHard as gruvboxLightHard } from "./gruvbox/gruvbox-light-hard" +import { lightSoft as gruvboxLightSoft } from "./gruvbox/gruvbox-light-soft" +import { dark as solarizedDark } from "./solarized/solarized" +import { light as solarizedLight } from "./solarized/solarized" +import { dark as andromedaDark } from "./andromeda/andromeda" +import { theme as oneDark } from "./one/one-dark" +import { theme as oneLight } from "./one/one-light" +import { theme as ayuLight } from "./ayu/ayu-light" +import { theme as ayuDark } from "./ayu/ayu-dark" +import { theme as ayuMirage } from "./ayu/ayu-mirage" +import { theme as rosePine } from "./rose-pine/rose-pine" +import { theme as rosePineDawn } from "./rose-pine/rose-pine-dawn" +import { theme as rosePineMoon } from "./rose-pine/rose-pine-moon" +import { theme as sandcastle } from "./sandcastle/sandcastle" +import { theme as summercamp } from "./summercamp/summercamp" +import { theme as atelierCaveDark } from "./atelier/atelier-cave-dark" +import { theme as atelierCaveLight } from "./atelier/atelier-cave-light" +import { theme as atelierDuneDark } from "./atelier/atelier-dune-dark" +import { theme as atelierDuneLight } from "./atelier/atelier-dune-light" +import { theme as atelierEstuaryDark } from "./atelier/atelier-estuary-dark" +import { theme as atelierEstuaryLight } from "./atelier/atelier-estuary-light" +import { theme as atelierForestDark } from "./atelier/atelier-forest-dark" +import { theme as atelierForestLight } from "./atelier/atelier-forest-light" +import { theme as atelierHeathDark } from "./atelier/atelier-heath-dark" +import { theme as atelierHeathLight } from "./atelier/atelier-heath-light" +import { theme as atelierLakesideDark } from "./atelier/atelier-lakeside-dark" +import { theme as atelierLakesideLight } from "./atelier/atelier-lakeside-light" +import { theme as atelierPlateauDark } from "./atelier/atelier-plateau-dark" +import { theme as atelierPlateauLight } from "./atelier/atelier-plateau-light" +import { theme as atelierSavannaDark } from "./atelier/atelier-savanna-dark" +import { theme as atelierSavannaLight } from "./atelier/atelier-savanna-light" +import { theme as atelierSeasideDark } from "./atelier/atelier-seaside-dark" +import { theme as atelierSeasideLight } from "./atelier/atelier-seaside-light" +import { theme as atelierSulphurpoolDark } from "./atelier/atelier-sulphurpool-dark" +import { theme as atelierSulphurpoolLight } from "./atelier/atelier-sulphurpool-light" + +export const themes: ThemeConfig[] = [ + oneDark, + oneLight, + ayuLight, + ayuDark, + ayuMirage, + gruvboxDark, + gruvboxDarkHard, + gruvboxDarkSoft, + gruvboxLight, + gruvboxLightHard, + gruvboxLightSoft, + rosePine, + rosePineDawn, + rosePineMoon, + sandcastle, + solarizedDark, + solarizedLight, + andromedaDark, + summercamp, + atelierCaveDark, + atelierCaveLight, + atelierDuneDark, + atelierDuneLight, + atelierEstuaryDark, + atelierEstuaryLight, + atelierForestDark, + atelierForestLight, + atelierHeathDark, + atelierHeathLight, + atelierLakesideDark, + atelierLakesideLight, + atelierPlateauDark, + atelierPlateauLight, + atelierSavannaDark, + atelierSavannaLight, + atelierSeasideDark, + atelierSeasideLight, + atelierSulphurpoolDark, + atelierSulphurpoolLight, +] diff --git a/styles/src/themes/one-dark.ts b/styles/src/themes/one-dark.ts deleted file mode 100644 index 85417a0e68651341edded4689425b15a0d8f29b1..0000000000000000000000000000000000000000 --- a/styles/src/themes/one-dark.ts +++ /dev/null @@ -1,85 +0,0 @@ -import chroma from "chroma-js" -import { fontWeights } from "../common" -import { Meta, ThemeSyntax } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "One Dark" - -const color = { - white: "#ACB2BE", - grey: "#5D636F", - red: "#D07277", - darkRed: "#B1574B", - orange: "#C0966B", - yellow: "#DFC184", - green: "#A1C181", - teal: "#6FB4C0", - blue: "#74ADE9", - purple: "#B478CF", -} - -const ramps = { - neutral: chroma - .scale([ - "#282c34", - "#353b45", - "#3e4451", - "#545862", - "#565c64", - "#abb2bf", - "#b6bdca", - "#c8ccd4", - ]) - .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]), - red: colorRamp(chroma(color.red)), - orange: colorRamp(chroma(color.orange)), - yellow: colorRamp(chroma(color.yellow)), - green: colorRamp(chroma(color.green)), - cyan: colorRamp(chroma(color.teal)), - blue: colorRamp(chroma(color.blue)), - violet: colorRamp(chroma(color.purple)), - magenta: colorRamp(chroma("#be5046")), -} - -const syntax: ThemeSyntax = { - boolean: { color: color.orange }, - comment: { color: color.grey }, - enum: { color: color.red }, - "emphasis.strong": { color: color.orange }, - function: { color: color.blue }, - keyword: { color: color.purple }, - linkText: { color: color.blue, italic: false }, - linkUri: { color: color.teal }, - number: { color: color.orange }, - constant: { color: color.yellow }, - operator: { color: color.teal }, - primary: { color: color.white }, - property: { color: color.red }, - punctuation: { color: color.white }, - "punctuation.list_marker": { color: color.red }, - "punctuation.special": { color: color.darkRed }, - string: { color: color.green }, - title: { color: color.red, weight: fontWeights.normal }, - "text.literal": { color: color.green }, - type: { color: color.teal }, - "variable.special": { color: color.orange }, - variant: { color: color.blue }, - constructor: { color: color.blue }, -} - -export const dark = createColorScheme(name, false, ramps, syntax) - -export const meta: Meta = { - name, - author: "simurai", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md", - license_checksum: - "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8", - }, - }, - url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui", -} diff --git a/styles/src/themes/one-light.ts b/styles/src/themes/one-light.ts deleted file mode 100644 index 7bf21aee17d4871681b33174e3b20bea06ced9fe..0000000000000000000000000000000000000000 --- a/styles/src/themes/one-light.ts +++ /dev/null @@ -1,84 +0,0 @@ -import chroma from "chroma-js" -import { fontWeights } from "../common" -import { Meta, ThemeSyntax } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "One Light" - -const color = { - black: "#383A41", - grey: "#A2A3A7", - red: "#D36050", - darkRed: "#B92C46", - orange: "#AD6F26", - yellow: "#DFC184", - green: "#659F58", - teal: "#3982B7", - blue: "#5B79E3", - purple: "#A449AB", - magenta: "#994EA6", -} - -const ramps = { - neutral: chroma - .scale([ - "#383A41", - "#535456", - "#696c77", - "#9D9D9F", - "#A9A9A9", - "#DBDBDC", - "#EAEAEB", - "#FAFAFA", - ]) - .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]), - red: colorRamp(chroma(color.red)), - orange: colorRamp(chroma(color.orange)), - yellow: colorRamp(chroma(color.yellow)), - green: colorRamp(chroma(color.green)), - cyan: colorRamp(chroma(color.teal)), - blue: colorRamp(chroma(color.blue)), - violet: colorRamp(chroma(color.purple)), - magenta: colorRamp(chroma(color.magenta)), -} - -const syntax: ThemeSyntax = { - boolean: { color: color.orange }, - comment: { color: color.grey }, - enum: { color: color.red }, - "emphasis.strong": { color: color.orange }, - function: { color: color.blue }, - keyword: { color: color.purple }, - linkText: { color: color.blue }, - linkUri: { color: color.teal }, - number: { color: color.orange }, - operator: { color: color.teal }, - primary: { color: color.black }, - property: { color: color.red }, - punctuation: { color: color.black }, - "punctuation.list_marker": { color: color.red }, - "punctuation.special": { color: color.darkRed }, - string: { color: color.green }, - title: { color: color.red, weight: fontWeights.normal }, - "text.literal": { color: color.green }, - type: { color: color.teal }, - "variable.special": { color: color.orange }, - variant: { color: color.blue }, -} - -export const light = createColorScheme(name, true, ramps, syntax) - -export const meta: Meta = { - name, - author: "simurai", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md", - license_checksum: - "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8", - }, - }, - url: "https://github.com/atom/atom/tree/master/packages/one-light-ui", -} diff --git a/styles/src/themes/one/LICENSE b/styles/src/themes/one/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..dc07dc10ad0de56ebe0bfad8d65e82f0f5d627ef --- /dev/null +++ b/styles/src/themes/one/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/one/one-dark.ts b/styles/src/themes/one/one-dark.ts new file mode 100644 index 0000000000000000000000000000000000000000..69a5bd55755a55cce1fd6128a5a8922d3ce7cf31 --- /dev/null +++ b/styles/src/themes/one/one-dark.ts @@ -0,0 +1,79 @@ +import { + chroma, + fontWeights, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +const color = { + white: "#ACB2BE", + grey: "#5D636F", + red: "#D07277", + darkRed: "#B1574B", + orange: "#C0966B", + yellow: "#DFC184", + green: "#A1C181", + teal: "#6FB4C0", + blue: "#74ADE9", + purple: "#B478CF", +} + +export const theme: ThemeConfig = { + name: "One Dark", + author: "simurai", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/atom/atom/tree/master/packages/one-dark-ui", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma + .scale([ + "#282c34", + "#353b45", + "#3e4451", + "#545862", + "#565c64", + "#abb2bf", + "#b6bdca", + "#c8ccd4", + ]) + .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]), + red: colorRamp(chroma(color.red)), + orange: colorRamp(chroma(color.orange)), + yellow: colorRamp(chroma(color.yellow)), + green: colorRamp(chroma(color.green)), + cyan: colorRamp(chroma(color.teal)), + blue: colorRamp(chroma(color.blue)), + violet: colorRamp(chroma(color.purple)), + magenta: colorRamp(chroma("#be5046")), + }, + override: { + syntax: { + boolean: { color: color.orange }, + comment: { color: color.grey }, + enum: { color: color.red }, + "emphasis.strong": { color: color.orange }, + function: { color: color.blue }, + keyword: { color: color.purple }, + linkText: { color: color.blue, italic: false }, + linkUri: { color: color.teal }, + number: { color: color.orange }, + constant: { color: color.yellow }, + operator: { color: color.teal }, + primary: { color: color.white }, + property: { color: color.red }, + punctuation: { color: color.white }, + "punctuation.list_marker": { color: color.red }, + "punctuation.special": { color: color.darkRed }, + string: { color: color.green }, + title: { color: color.red, weight: fontWeights.normal }, + "text.literal": { color: color.green }, + type: { color: color.teal }, + "variable.special": { color: color.orange }, + variant: { color: color.blue }, + constructor: { color: color.blue }, + }, + }, +} diff --git a/styles/src/themes/one/one-light.ts b/styles/src/themes/one/one-light.ts new file mode 100644 index 0000000000000000000000000000000000000000..9123c8879d1bcc11f9258fc538aa4b377c2a3ec5 --- /dev/null +++ b/styles/src/themes/one/one-light.ts @@ -0,0 +1,79 @@ +import { + chroma, + fontWeights, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +const color = { + black: "#383A41", + grey: "#A2A3A7", + red: "#D36050", + darkRed: "#B92C46", + orange: "#AD6F26", + yellow: "#DFC184", + green: "#659F58", + teal: "#3982B7", + blue: "#5B79E3", + purple: "#A449AB", + magenta: "#994EA6", +} + +export const theme: ThemeConfig = { + name: "One Light", + author: "simurai", + appearance: ThemeAppearance.Light, + licenseType: ThemeLicenseType.MIT, + licenseUrl: + "https://github.com/atom/atom/tree/master/packages/one-light-ui", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma + .scale([ + "#383A41", + "#535456", + "#696c77", + "#9D9D9F", + "#A9A9A9", + "#DBDBDC", + "#EAEAEB", + "#FAFAFA", + ]) + .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]), + red: colorRamp(chroma(color.red)), + orange: colorRamp(chroma(color.orange)), + yellow: colorRamp(chroma(color.yellow)), + green: colorRamp(chroma(color.green)), + cyan: colorRamp(chroma(color.teal)), + blue: colorRamp(chroma(color.blue)), + violet: colorRamp(chroma(color.purple)), + magenta: colorRamp(chroma(color.magenta)), + }, + override: { + syntax: { + boolean: { color: color.orange }, + comment: { color: color.grey }, + enum: { color: color.red }, + "emphasis.strong": { color: color.orange }, + function: { color: color.blue }, + keyword: { color: color.purple }, + linkText: { color: color.blue }, + linkUri: { color: color.teal }, + number: { color: color.orange }, + operator: { color: color.teal }, + primary: { color: color.black }, + property: { color: color.red }, + punctuation: { color: color.black }, + "punctuation.list_marker": { color: color.red }, + "punctuation.special": { color: color.darkRed }, + string: { color: color.green }, + title: { color: color.red, weight: fontWeights.normal }, + "text.literal": { color: color.green }, + type: { color: color.teal }, + "variable.special": { color: color.orange }, + variant: { color: color.blue }, + }, + }, +} diff --git a/styles/src/themes/rose-pine-dawn.ts b/styles/src/themes/rose-pine-dawn.ts deleted file mode 100644 index 427b05f72b8e5672897b7457738d4c43e2c6f603..0000000000000000000000000000000000000000 --- a/styles/src/themes/rose-pine-dawn.ts +++ /dev/null @@ -1,45 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Rosé Pine Dawn" - -const ramps = { - neutral: chroma - .scale([ - "#575279", - "#797593", - "#9893A5", - "#B5AFB8", - "#D3CCCC", - "#F2E9E1", - "#FFFAF3", - "#FAF4ED", - ]) - .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]), - red: colorRamp(chroma("#B4637A")), - orange: colorRamp(chroma("#D7827E")), - yellow: colorRamp(chroma("#EA9D34")), - green: colorRamp(chroma("#679967")), - cyan: colorRamp(chroma("#286983")), - blue: colorRamp(chroma("#56949F")), - violet: colorRamp(chroma("#907AA9")), - magenta: colorRamp(chroma("#79549F")), -} - -export const light = createColorScheme(name, true, ramps) - -export const meta: Meta = { - name, - author: "edunfelt", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", - license_checksum: - "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a", - }, - }, - url: "https://github.com/edunfelt/base16-rose-pine-scheme", -} diff --git a/styles/src/themes/rose-pine-moon.ts b/styles/src/themes/rose-pine-moon.ts deleted file mode 100644 index be2f5a8dafd366803f13737c75bdb48d7e101aa9..0000000000000000000000000000000000000000 --- a/styles/src/themes/rose-pine-moon.ts +++ /dev/null @@ -1,45 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Rosé Pine Moon" - -const ramps = { - neutral: chroma - .scale([ - "#232136", - "#2A273F", - "#393552", - "#3E3A53", - "#56526C", - "#6E6A86", - "#908CAA", - "#E0DEF4", - ]) - .domain([0, 0.3, 0.55, 1]), - red: colorRamp(chroma("#EB6F92")), - orange: colorRamp(chroma("#EBBCBA")), - yellow: colorRamp(chroma("#F6C177")), - green: colorRamp(chroma("#8DBD8D")), - cyan: colorRamp(chroma("#409BBE")), - blue: colorRamp(chroma("#9CCFD8")), - violet: colorRamp(chroma("#C4A7E7")), - magenta: colorRamp(chroma("#AB6FE9")), -} - -export const dark = createColorScheme(name, false, ramps) - -export const meta: Meta = { - name, - author: "edunfelt", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", - license_checksum: - "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a", - }, - }, - url: "https://github.com/edunfelt/base16-rose-pine-scheme", -} diff --git a/styles/src/themes/rose-pine.ts b/styles/src/themes/rose-pine.ts deleted file mode 100644 index 944550f1250ad01146f8622a8199cf027f4658ab..0000000000000000000000000000000000000000 --- a/styles/src/themes/rose-pine.ts +++ /dev/null @@ -1,43 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Rosé Pine" - -const ramps = { - neutral: chroma.scale([ - "#191724", - "#1f1d2e", - "#26233A", - "#3E3A53", - "#56526C", - "#6E6A86", - "#908CAA", - "#E0DEF4", - ]), - red: colorRamp(chroma("#EB6F92")), - orange: colorRamp(chroma("#EBBCBA")), - yellow: colorRamp(chroma("#F6C177")), - green: colorRamp(chroma("#8DBD8D")), - cyan: colorRamp(chroma("#409BBE")), - blue: colorRamp(chroma("#9CCFD8")), - violet: colorRamp(chroma("#C4A7E7")), - magenta: colorRamp(chroma("#AB6FE9")), -} - -export const dark = createColorScheme(name, false, ramps) - -export const meta: Meta = { - name, - author: "edunfelt", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE", - license_checksum: - "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a", - }, - }, - url: "https://github.com/edunfelt/base16-rose-pine-scheme", -} diff --git a/styles/src/themes/rose-pine/LICENSE b/styles/src/themes/rose-pine/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..dfd60136f95374fbe3e112a6051a4854b61ac4ec --- /dev/null +++ b/styles/src/themes/rose-pine/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Emilia Dunfelt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/rose-pine/rose-pine-dawn.ts b/styles/src/themes/rose-pine/rose-pine-dawn.ts new file mode 100644 index 0000000000000000000000000000000000000000..a373ed378c0ccc8b07da7303a504aff3906a59b6 --- /dev/null +++ b/styles/src/themes/rose-pine/rose-pine-dawn.ts @@ -0,0 +1,39 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +export const theme: ThemeConfig = { + name: "Rosé Pine Dawn", + author: "edunfelt", + appearance: ThemeAppearance.Light, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma + .scale([ + "#575279", + "#797593", + "#9893A5", + "#B5AFB8", + "#D3CCCC", + "#F2E9E1", + "#FFFAF3", + "#FAF4ED", + ]) + .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]), + red: colorRamp(chroma("#B4637A")), + orange: colorRamp(chroma("#D7827E")), + yellow: colorRamp(chroma("#EA9D34")), + green: colorRamp(chroma("#679967")), + cyan: colorRamp(chroma("#286983")), + blue: colorRamp(chroma("#56949F")), + violet: colorRamp(chroma("#907AA9")), + magenta: colorRamp(chroma("#79549F")), + }, + override: { syntax: {} }, +} diff --git a/styles/src/themes/rose-pine/rose-pine-moon.ts b/styles/src/themes/rose-pine/rose-pine-moon.ts new file mode 100644 index 0000000000000000000000000000000000000000..94b8166cb30e3eb6cfe19a45532fbe0c7cb2e7ab --- /dev/null +++ b/styles/src/themes/rose-pine/rose-pine-moon.ts @@ -0,0 +1,39 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +export const theme: ThemeConfig = { + name: "Rosé Pine Moon", + author: "edunfelt", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma + .scale([ + "#232136", + "#2A273F", + "#393552", + "#3E3A53", + "#56526C", + "#6E6A86", + "#908CAA", + "#E0DEF4", + ]) + .domain([0, 0.3, 0.55, 1]), + red: colorRamp(chroma("#EB6F92")), + orange: colorRamp(chroma("#EBBCBA")), + yellow: colorRamp(chroma("#F6C177")), + green: colorRamp(chroma("#8DBD8D")), + cyan: colorRamp(chroma("#409BBE")), + blue: colorRamp(chroma("#9CCFD8")), + violet: colorRamp(chroma("#C4A7E7")), + magenta: colorRamp(chroma("#AB6FE9")), + }, + override: { syntax: {} }, +} diff --git a/styles/src/themes/rose-pine/rose-pine.ts b/styles/src/themes/rose-pine/rose-pine.ts new file mode 100644 index 0000000000000000000000000000000000000000..3aabe3f10e78b44603151c8225deacbb9ae7f227 --- /dev/null +++ b/styles/src/themes/rose-pine/rose-pine.ts @@ -0,0 +1,37 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +export const theme: ThemeConfig = { + name: "Rosé Pine", + author: "edunfelt", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma.scale([ + "#191724", + "#1f1d2e", + "#26233A", + "#3E3A53", + "#56526C", + "#6E6A86", + "#908CAA", + "#E0DEF4", + ]), + red: colorRamp(chroma("#EB6F92")), + orange: colorRamp(chroma("#EBBCBA")), + yellow: colorRamp(chroma("#F6C177")), + green: colorRamp(chroma("#8DBD8D")), + cyan: colorRamp(chroma("#409BBE")), + blue: colorRamp(chroma("#9CCFD8")), + violet: colorRamp(chroma("#C4A7E7")), + magenta: colorRamp(chroma("#AB6FE9")), + }, + override: { syntax: {} }, +} diff --git a/styles/src/themes/sandcastle.ts b/styles/src/themes/sandcastle.ts deleted file mode 100644 index 483f01b27a1850463348e0a9c575086a014d5f1b..0000000000000000000000000000000000000000 --- a/styles/src/themes/sandcastle.ts +++ /dev/null @@ -1,43 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Sandcastle" - -const ramps = { - neutral: chroma.scale([ - "#282c34", - "#2c323b", - "#3e4451", - "#665c54", - "#928374", - "#a89984", - "#d5c4a1", - "#fdf4c1", - ]), - red: colorRamp(chroma("#B4637A")), - orange: colorRamp(chroma("#a07e3b")), - yellow: colorRamp(chroma("#a07e3b")), - green: colorRamp(chroma("#83a598")), - cyan: colorRamp(chroma("#83a598")), - blue: colorRamp(chroma("#528b8b")), - violet: colorRamp(chroma("#d75f5f")), - magenta: colorRamp(chroma("#a87322")), -} - -export const dark = createColorScheme(name, false, ramps) - -export const meta: Meta = { - name, - author: "gessig", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE", - license_checksum: - "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc", - }, - }, - url: "https://github.com/gessig/base16-sandcastle-scheme", -} diff --git a/styles/src/themes/sandcastle/LICENSE b/styles/src/themes/sandcastle/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..c66a06c51b46671cfe20194ac8ff545683c7a7e3 --- /dev/null +++ b/styles/src/themes/sandcastle/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 George Essig + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/sandcastle/sandcastle.ts b/styles/src/themes/sandcastle/sandcastle.ts new file mode 100644 index 0000000000000000000000000000000000000000..753828c66579e48d958066defb3a95aa1431d71c --- /dev/null +++ b/styles/src/themes/sandcastle/sandcastle.ts @@ -0,0 +1,37 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +export const theme: ThemeConfig = { + name: "Sandcastle", + author: "gessig", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/gessig/base16-sandcastle-scheme", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma.scale([ + "#282c34", + "#2c323b", + "#3e4451", + "#665c54", + "#928374", + "#a89984", + "#d5c4a1", + "#fdf4c1", + ]), + red: colorRamp(chroma("#B4637A")), + orange: colorRamp(chroma("#a07e3b")), + yellow: colorRamp(chroma("#a07e3b")), + green: colorRamp(chroma("#83a598")), + cyan: colorRamp(chroma("#83a598")), + blue: colorRamp(chroma("#528b8b")), + violet: colorRamp(chroma("#d75f5f")), + magenta: colorRamp(chroma("#a87322")), + }, + override: { syntax: {} }, +} diff --git a/styles/src/themes/solarized.ts b/styles/src/themes/solarized.ts deleted file mode 100644 index 1210c4380608e812d39dec0f36d6ec69dab37e9b..0000000000000000000000000000000000000000 --- a/styles/src/themes/solarized.ts +++ /dev/null @@ -1,46 +0,0 @@ -import chroma from "chroma-js" -import { Meta as Metadata } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Solarized" - -const ramps = { - neutral: chroma - .scale([ - "#002b36", - "#073642", - "#586e75", - "#657b83", - "#839496", - "#93a1a1", - "#eee8d5", - "#fdf6e3", - ]) - .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]), - red: colorRamp(chroma("#dc322f")), - orange: colorRamp(chroma("#cb4b16")), - yellow: colorRamp(chroma("#b58900")), - green: colorRamp(chroma("#859900")), - cyan: colorRamp(chroma("#2aa198")), - blue: colorRamp(chroma("#268bd2")), - violet: colorRamp(chroma("#6c71c4")), - magenta: colorRamp(chroma("#d33682")), -} - -export const dark = createColorScheme(`${name} Dark`, false, ramps) -export const light = createColorScheme(`${name} Light`, true, ramps) - -export const meta: Metadata = { - name, - author: "Ethan Schoonover", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE", - license_checksum: - "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6", - }, - }, - url: "https://github.com/altercation/solarized", -} diff --git a/styles/src/themes/solarized/LICENSE b/styles/src/themes/solarized/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..221eee6f152873e2e6c52ca4a89ac1d65118843b --- /dev/null +++ b/styles/src/themes/solarized/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 Ethan Schoonover + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/solarized/solarized.ts b/styles/src/themes/solarized/solarized.ts new file mode 100644 index 0000000000000000000000000000000000000000..4084757525cd131933bd6ae874f5cfc1415990fb --- /dev/null +++ b/styles/src/themes/solarized/solarized.ts @@ -0,0 +1,52 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +const ramps = { + neutral: chroma + .scale([ + "#002b36", + "#073642", + "#586e75", + "#657b83", + "#839496", + "#93a1a1", + "#eee8d5", + "#fdf6e3", + ]) + .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]), + red: colorRamp(chroma("#dc322f")), + orange: colorRamp(chroma("#cb4b16")), + yellow: colorRamp(chroma("#b58900")), + green: colorRamp(chroma("#859900")), + cyan: colorRamp(chroma("#2aa198")), + blue: colorRamp(chroma("#268bd2")), + violet: colorRamp(chroma("#6c71c4")), + magenta: colorRamp(chroma("#d33682")), +} + +export const dark: ThemeConfig = { + name: "Solarized Dark", + author: "Ethan Schoonover", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/altercation/solarized", + licenseFile: `${__dirname}/LICENSE`, + inputColor: ramps, + override: { syntax: {} }, +} + +export const light: ThemeConfig = { + name: "Solarized Light", + author: "Ethan Schoonover", + appearance: ThemeAppearance.Light, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/altercation/solarized", + licenseFile: `${__dirname}/LICENSE`, + inputColor: ramps, + override: { syntax: {} }, +} diff --git a/styles/src/themes/summercamp.ts b/styles/src/themes/summercamp.ts deleted file mode 100644 index 7df125e86606d299e52ffb07140f156744e086ce..0000000000000000000000000000000000000000 --- a/styles/src/themes/summercamp.ts +++ /dev/null @@ -1,44 +0,0 @@ -import chroma from "chroma-js" -import { Meta } from "./common/colorScheme" -import { colorRamp, createColorScheme } from "./common/ramps" - -const name = "Summercamp" - -const ramps = { - neutral: chroma - .scale([ - "#1c1810", - "#2a261c", - "#3a3527", - "#3a3527", - "#5f5b45", - "#736e55", - "#bab696", - "#f8f5de", - ]) - .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]), - red: colorRamp(chroma("#e35142")), - orange: colorRamp(chroma("#fba11b")), - yellow: colorRamp(chroma("#f2ff27")), - green: colorRamp(chroma("#5ceb5a")), - cyan: colorRamp(chroma("#5aebbc")), - blue: colorRamp(chroma("#489bf0")), - violet: colorRamp(chroma("#FF8080")), - magenta: colorRamp(chroma("#F69BE7")), -} - -export const dark = createColorScheme(name, false, ramps) -export const meta: Meta = { - name, - author: "zoefiri", - url: "https://github.com/zoefiri/base16-sc", - license: { - SPDX: "MIT", - license_text: { - https_url: - "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE", - license_checksum: - "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf", - }, - }, -} diff --git a/styles/src/themes/summercamp/LICENSE b/styles/src/themes/summercamp/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..d7525414ad01c246c21e908666064d6db4233901 --- /dev/null +++ b/styles/src/themes/summercamp/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Zoe FiriH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/styles/src/themes/summercamp/summercamp.ts b/styles/src/themes/summercamp/summercamp.ts new file mode 100644 index 0000000000000000000000000000000000000000..08098d2e2fd009e8858d8967d6714800dd8d656a --- /dev/null +++ b/styles/src/themes/summercamp/summercamp.ts @@ -0,0 +1,39 @@ +import { + chroma, + colorRamp, + ThemeAppearance, + ThemeLicenseType, + ThemeConfig, +} from "../../common" + +export const theme: ThemeConfig = { + name: "Summercamp", + author: "zoefiri", + appearance: ThemeAppearance.Dark, + licenseType: ThemeLicenseType.MIT, + licenseUrl: "https://github.com/zoefiri/base16-sc", + licenseFile: `${__dirname}/LICENSE`, + inputColor: { + neutral: chroma + .scale([ + "#1c1810", + "#2a261c", + "#3a3527", + "#3a3527", + "#5f5b45", + "#736e55", + "#bab696", + "#f8f5de", + ]) + .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]), + red: colorRamp(chroma("#e35142")), + orange: colorRamp(chroma("#fba11b")), + yellow: colorRamp(chroma("#f2ff27")), + green: colorRamp(chroma("#5ceb5a")), + cyan: colorRamp(chroma("#5aebbc")), + blue: colorRamp(chroma("#489bf0")), + violet: colorRamp(chroma("#FF8080")), + magenta: colorRamp(chroma("#F69BE7")), + }, + override: { syntax: {} }, +}