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/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 3751b34d3156da64fc0efea74e707fc9222bb324..807ed777b2c19fdf1b5bf1af887a5dc5296f268c 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.12.5" 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" @@ -2587,7 +2589,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -4348,7 +4350,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -4793,6 +4795,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" @@ -4828,9 +4840,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", ] @@ -5107,9 +5119,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", ] @@ -6053,7 +6065,7 @@ checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6096,7 +6108,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6128,7 +6140,6 @@ name = "settings" version = "0.1.0" dependencies = [ "anyhow", - "assets", "collections", "fs", "futures 0.3.28", @@ -6137,6 +6148,7 @@ dependencies = [ "lazy_static", "postage", "pretty_assertions", + "rust-embed", "schemars", "serde", "serde_derive", @@ -6583,12 +6595,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" @@ -6660,9 +6666,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", @@ -6848,15 +6854,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" @@ -6930,7 +6927,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6960,6 +6957,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" @@ -7088,7 +7100,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7276,7 +7288,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7381,8 +7393,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", @@ -7778,6 +7790,7 @@ dependencies = [ "lazy_static", "log", "rand 0.8.5", + "rust-embed", "serde", "serde_json", "smol", @@ -7837,12 +7850,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" @@ -7854,7 +7861,6 @@ name = "vim" version = "0.1.0" dependencies = [ "anyhow", - "assets", "async-compat", "async-trait", "collections", @@ -8006,7 +8012,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-shared", ] @@ -8040,7 +8046,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8682,7 +8688,6 @@ name = "workspace" version = "0.1.0" dependencies = [ "anyhow", - "assets", "async-recursion 1.0.4", "bincode", "call", @@ -8777,12 +8782,11 @@ dependencies = [ [[package]] name = "zed" -version = "0.89.0" +version = "0.90.0" dependencies = [ "activity_indicator", "ai", "anyhow", - "assets", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -8907,7 +8911,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..07d2cdc60a6439a96a1a93bd9d212e0207bb59ac 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.12.5" publish = false [[bin]] diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7c6a49f179c2258cc2f18a834e8d63c9c70a6df8..b0b2a684d9501bf135f4280022437ef1c39f1230 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -112,6 +112,16 @@ CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_rep CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_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, 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 fd28fb910177099d0cf7d639cfd802aa4f3dab25..1deca1baa8036113c4ad2c4f0535a18085cb5db1 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, }; @@ -1638,6 +1640,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) @@ -2637,6 +2658,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, @@ -2707,6 +2780,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, }, @@ -2819,6 +2893,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) @@ -3482,6 +3575,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, } @@ -3537,10 +3631,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 53726113db7d23ce0aa6f7ab04be2ba7c75aa2d2..5bc1a4e414d393a346af706b961f59236eb656bc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3114,6 +3114,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..a1e354a4bc20a880ce7b69eae00677d6e9d9e9ab 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 @@ -2372,7 +2394,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 +2547,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 +3229,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 +3569,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 +5235,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 +5703,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 +5716,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 +6494,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 +6774,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 +7017,7 @@ impl Editor { multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } - multi_buffer::Event::LanguageChanged => {} + _ => {} } } @@ -7076,11 +7215,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 +7232,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 +7240,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 +7431,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 +7937,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..a63f3404d3fbc79e4fbe31f8b0a8844a710a3d1b 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, @@ -3107,6 +3108,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 +4322,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 +4436,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 +4777,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { two threeˇ "}, - "overlapping aditional edit", + "overlapping additional edit", ), ( indoc! {" @@ -5225,7 +5277,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 +5815,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/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 99562405b5b909e30e27df8b713e13c7e6c30cc5..4c6b6c24d300273b1cbe3f2386cc51ba19cdc5d5 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -32,7 +32,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)] @@ -77,13 +77,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/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..490f3bde17e58ea0844d49bdec0003da6dcee02e 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1717,8 +1717,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 fd522c5061fc37b1688a13aa11e12eaff1d54e12..39d8ea85129b0bdd282e4fac243929457c9b0e57 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::*; @@ -460,6 +463,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 +523,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 +592,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 +690,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 +732,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 +1107,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 +1234,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 +2238,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 +2365,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 +2801,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 +2825,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 +2861,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 +3486,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 +4073,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 +4516,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 +4929,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 +4950,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 +5240,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 +5557,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 +6671,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 +7042,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 dc3c172775721c5142aedab6fc4f0f5d66a1c913..97656b7b10053e4d6b7a30982719ebd75adab057 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -157,7 +157,7 @@ impl RepositoryEntry { self.statuses .iter_from(&repo_path) .take_while(|(key, _)| key.starts_with(&repo_path)) - // Short circut once we've found the highest level + // Short circuit once we've found the highest level .take_until(|(_, status)| status == &&GitFileStatus::Conflict) .map(|(_, status)| status) .reduce( @@ -677,6 +677,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 { @@ -684,14 +689,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, @@ -1544,6 +1541,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() } @@ -2383,6 +2388,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 } @@ -2447,6 +2456,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, @@ -2507,7 +2527,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, @@ -3603,7 +3623,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 848cc1c2fa3213ce55f317e63b7a6d7db293671f..ca3ec7cafb899a8ccbec6aec548c79d97ea30daa 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/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index b929de95961e756d369f9b1549dc693d6536bbe0..bef6efa529fd514b24789a5d294dbc33409974ab 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 56; +pub const PROTOCOL_VERSION: u32 = 57; 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 9d787e1389a9742c44e505a96ddf44ee71d04671..79f3c68514c11eadf62acd8ebf1d805085739eb8 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..54919445cb2a3326000d2acb8b9ae99021bd206e 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.90.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" } 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/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/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/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[]