diff --git a/.gitignore b/.gitignore index 15a0a9f5f2..2d8807a4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea **/target **/cargo-target /zed.xcworkspace diff --git a/Cargo.lock b/Cargo.lock index 775e1d2b8e..2f549c568d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "log", "menu", "ordered-float", + "parking_lot 0.11.2", "project", "rand 0.8.5", "regex", @@ -1453,7 +1454,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.20.0" +version = "0.21.0" dependencies = [ "anyhow", "async-trait", @@ -6738,6 +6739,7 @@ dependencies = [ "lazy_static", "log", "matrixmultiply", + "ordered-float", "parking_lot 0.11.2", "parse_duration", "picker", @@ -9793,7 +9795,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.104.0" +version = "0.105.0" dependencies = [ "activity_indicator", "ai", diff --git a/Procfile b/Procfile index fcc03f55dc..127fffbed1 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,4 @@ web: cd ../zed.dev && PORT=3000 npx vercel dev collab: cd crates/collab && cargo run serve -livekit: livekit-server --dev \ No newline at end of file +livekit: livekit-server --dev +postgrest: postgrest crates/collab/admin_api.conf diff --git a/README.md b/README.md index 2ee426a2a6..6c502ebc74 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea ``` sudo xcodebuild -license ``` - + * Install homebrew, node and rustup-init (rutup, rust, cargo, etc.) ``` /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew install node rustup-init rustup-init # follow the installation steps ``` - + * Install postgres and configure the database ``` brew install postgresql@15 @@ -27,11 +27,12 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres psql -U postgres -c "CREATE DATABASE zed" ``` - -* Install the `LiveKit` server and the `foreman` process supervisor: + +* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor: ``` brew install livekit + brew install postgrest brew install foreman ``` diff --git a/assets/icons/Icons/exit.svg b/assets/icons/Icons/exit.svg deleted file mode 100644 index 6d76849248..0000000000 --- a/assets/icons/Icons/exit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_down_12.svg b/assets/icons/arrow_down_12.svg deleted file mode 100644 index dfad5d4876..0000000000 --- a/assets/icons/arrow_down_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_down_16.svg b/assets/icons/arrow_down_16.svg deleted file mode 100644 index ec757a8ab4..0000000000 --- a/assets/icons/arrow_down_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_down_8.svg b/assets/icons/arrow_down_8.svg deleted file mode 100644 index f70f3920a3..0000000000 --- a/assets/icons/arrow_down_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_left_12.svg b/assets/icons/arrow_left_12.svg deleted file mode 100644 index aaccf25eaf..0000000000 --- a/assets/icons/arrow_left_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_left_16.svg b/assets/icons/arrow_left_16.svg deleted file mode 100644 index 317c31e9f0..0000000000 --- a/assets/icons/arrow_left_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_left_8.svg b/assets/icons/arrow_left_8.svg deleted file mode 100644 index e2071d55eb..0000000000 --- a/assets/icons/arrow_left_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_right_12.svg b/assets/icons/arrow_right_12.svg deleted file mode 100644 index c5f70a4958..0000000000 --- a/assets/icons/arrow_right_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_right_16.svg b/assets/icons/arrow_right_16.svg deleted file mode 100644 index b41e8fc810..0000000000 --- a/assets/icons/arrow_right_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_right_8.svg b/assets/icons/arrow_right_8.svg deleted file mode 100644 index fb3f836ef0..0000000000 --- a/assets/icons/arrow_right_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_up_12.svg b/assets/icons/arrow_up_12.svg deleted file mode 100644 index c9f35d868b..0000000000 --- a/assets/icons/arrow_up_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_up_16.svg b/assets/icons/arrow_up_16.svg deleted file mode 100644 index 0d8add4ed7..0000000000 --- a/assets/icons/arrow_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/arrow_up_8.svg b/assets/icons/arrow_up_8.svg deleted file mode 100644 index 0a1e2c44bf..0000000000 --- a/assets/icons/arrow_up_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/arrow_up_right_8.svg b/assets/icons/arrow_up_right.svg similarity index 100% rename from assets/icons/arrow_up_right_8.svg rename to assets/icons/arrow_up_right.svg diff --git a/assets/icons/assist_15.svg b/assets/icons/assist_15.svg deleted file mode 100644 index 3baf8df3e9..0000000000 --- a/assets/icons/assist_15.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/backspace _12.svg b/assets/icons/backspace _12.svg deleted file mode 100644 index 68bad3da26..0000000000 --- a/assets/icons/backspace _12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/backspace _16.svg b/assets/icons/backspace _16.svg deleted file mode 100644 index 965470690e..0000000000 --- a/assets/icons/backspace _16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/backspace _8.svg b/assets/icons/backspace _8.svg deleted file mode 100644 index 60972007b6..0000000000 --- a/assets/icons/backspace _8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_8.svg b/assets/icons/bolt.svg similarity index 100% rename from assets/icons/bolt_8.svg rename to assets/icons/bolt.svg diff --git a/assets/icons/bolt_12.svg b/assets/icons/bolt_12.svg deleted file mode 100644 index 0125c733e2..0000000000 --- a/assets/icons/bolt_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_16.svg b/assets/icons/bolt_16.svg deleted file mode 100644 index aca476ef50..0000000000 --- a/assets/icons/bolt_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_slash_12.svg b/assets/icons/bolt_slash_12.svg deleted file mode 100644 index 80d99be616..0000000000 --- a/assets/icons/bolt_slash_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/bolt_slash_16.svg b/assets/icons/bolt_slash_16.svg deleted file mode 100644 index 9520a626c1..0000000000 --- a/assets/icons/bolt_slash_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/bolt_slash_8.svg b/assets/icons/bolt_slash_8.svg deleted file mode 100644 index 3781a91299..0000000000 --- a/assets/icons/bolt_slash_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/radix/caret-down.svg b/assets/icons/caret_down.svg similarity index 100% rename from assets/icons/radix/caret-down.svg rename to assets/icons/caret_down.svg diff --git a/assets/icons/caret_down_12.svg b/assets/icons/caret_down_12.svg deleted file mode 100644 index 6208814bc2..0000000000 --- a/assets/icons/caret_down_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_down_16.svg b/assets/icons/caret_down_16.svg deleted file mode 100644 index cba930287e..0000000000 --- a/assets/icons/caret_down_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_down_8.svg b/assets/icons/caret_down_8.svg deleted file mode 100644 index 932376d6f8..0000000000 --- a/assets/icons/caret_down_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/caret_left_12.svg b/assets/icons/caret_left_12.svg deleted file mode 100644 index 6b6c32513e..0000000000 --- a/assets/icons/caret_left_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_left_16.svg b/assets/icons/caret_left_16.svg deleted file mode 100644 index 5ffd176c59..0000000000 --- a/assets/icons/caret_left_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_left_8.svg b/assets/icons/caret_left_8.svg deleted file mode 100644 index 1b04877a31..0000000000 --- a/assets/icons/caret_left_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/caret_right_12.svg b/assets/icons/caret_right_12.svg deleted file mode 100644 index 6670b80cf8..0000000000 --- a/assets/icons/caret_right_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_right_16.svg b/assets/icons/caret_right_16.svg deleted file mode 100644 index da239b95d7..0000000000 --- a/assets/icons/caret_right_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_right_8.svg b/assets/icons/caret_right_8.svg deleted file mode 100644 index d1350ee809..0000000000 --- a/assets/icons/caret_right_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/radix/caret-up.svg b/assets/icons/caret_up.svg similarity index 100% rename from assets/icons/radix/caret-up.svg rename to assets/icons/caret_up.svg diff --git a/assets/icons/caret_up_12.svg b/assets/icons/caret_up_12.svg deleted file mode 100644 index 9fe93c47ae..0000000000 --- a/assets/icons/caret_up_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_up_16.svg b/assets/icons/caret_up_16.svg deleted file mode 100644 index 10f45523a4..0000000000 --- a/assets/icons/caret_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/caret_up_8.svg b/assets/icons/caret_up_8.svg deleted file mode 100644 index bf79244d7d..0000000000 --- a/assets/icons/caret_up_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/case_insensitive_12.svg b/assets/icons/case_insensitive.svg similarity index 100% rename from assets/icons/case_insensitive_12.svg rename to assets/icons/case_insensitive.svg diff --git a/assets/icons/channel_hash.svg b/assets/icons/channel_hash.svg deleted file mode 100644 index edd0462678..0000000000 --- a/assets/icons/channel_hash.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/check_12.svg b/assets/icons/check_12.svg deleted file mode 100644 index 3e15dd7d1f..0000000000 --- a/assets/icons/check_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/check_16.svg b/assets/icons/check_16.svg deleted file mode 100644 index 7e959b5924..0000000000 --- a/assets/icons/check_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/check_8.svg b/assets/icons/check_8.svg deleted file mode 100644 index 268f8bb498..0000000000 --- a/assets/icons/check_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_12.svg b/assets/icons/chevron_down_12.svg deleted file mode 100644 index 7bba37857a..0000000000 --- a/assets/icons/chevron_down_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_16.svg b/assets/icons/chevron_down_16.svg deleted file mode 100644 index cc7228cdc9..0000000000 --- a/assets/icons/chevron_down_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_down_8.svg b/assets/icons/chevron_down_8.svg deleted file mode 100644 index fe60b4968a..0000000000 --- a/assets/icons/chevron_down_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left_12.svg b/assets/icons/chevron_left_12.svg deleted file mode 100644 index a230007c7b..0000000000 --- a/assets/icons/chevron_left_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left_16.svg b/assets/icons/chevron_left_16.svg deleted file mode 100644 index 2cd1bbd4d2..0000000000 --- a/assets/icons/chevron_left_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_left_8.svg b/assets/icons/chevron_left_8.svg deleted file mode 100644 index 88ca274f51..0000000000 --- a/assets/icons/chevron_left_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right_12.svg b/assets/icons/chevron_right_12.svg deleted file mode 100644 index b463182705..0000000000 --- a/assets/icons/chevron_right_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right_16.svg b/assets/icons/chevron_right_16.svg deleted file mode 100644 index 270a33db70..0000000000 --- a/assets/icons/chevron_right_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_right_8.svg b/assets/icons/chevron_right_8.svg deleted file mode 100644 index 7349274681..0000000000 --- a/assets/icons/chevron_right_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_12.svg b/assets/icons/chevron_up_12.svg deleted file mode 100644 index c6bbee4ff7..0000000000 --- a/assets/icons/chevron_up_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_16.svg b/assets/icons/chevron_up_16.svg deleted file mode 100644 index ba2d4e6668..0000000000 --- a/assets/icons/chevron_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/chevron_up_8.svg b/assets/icons/chevron_up_8.svg deleted file mode 100644 index 41525aa3ea..0000000000 --- a/assets/icons/chevron_up_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_check_16.svg b/assets/icons/circle_check.svg similarity index 100% rename from assets/icons/circle_check_16.svg rename to assets/icons/circle_check.svg diff --git a/assets/icons/circle_check_12.svg b/assets/icons/circle_check_12.svg deleted file mode 100644 index cb28c8a051..0000000000 --- a/assets/icons/circle_check_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_check_8.svg b/assets/icons/circle_check_8.svg deleted file mode 100644 index c4150f058c..0000000000 --- a/assets/icons/circle_check_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_info_12.svg b/assets/icons/circle_info_12.svg deleted file mode 100644 index 26a569737d..0000000000 --- a/assets/icons/circle_info_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_info_16.svg b/assets/icons/circle_info_16.svg deleted file mode 100644 index 48bd4f79a8..0000000000 --- a/assets/icons/circle_info_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_info_8.svg b/assets/icons/circle_info_8.svg deleted file mode 100644 index 49bb03224d..0000000000 --- a/assets/icons/circle_info_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_up_12.svg b/assets/icons/circle_up_12.svg deleted file mode 100644 index 4236037fbd..0000000000 --- a/assets/icons/circle_up_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_up_16.svg b/assets/icons/circle_up_16.svg deleted file mode 100644 index 4eb3886fe4..0000000000 --- a/assets/icons/circle_up_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_up_8.svg b/assets/icons/circle_up_8.svg deleted file mode 100644 index e08e0ad492..0000000000 --- a/assets/icons/circle_up_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_x_mark_12.svg b/assets/icons/circle_x_mark_12.svg deleted file mode 100644 index 5f11a71ece..0000000000 --- a/assets/icons/circle_x_mark_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/circle_x_mark_16.svg b/assets/icons/circle_x_mark_16.svg deleted file mode 100644 index db3f401615..0000000000 --- a/assets/icons/circle_x_mark_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/circle_x_mark_8.svg b/assets/icons/circle_x_mark_8.svg deleted file mode 100644 index a0acfc3899..0000000000 --- a/assets/icons/circle_x_mark_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/cloud_12.svg b/assets/icons/cloud_12.svg deleted file mode 100644 index 2ed58f4966..0000000000 --- a/assets/icons/cloud_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_8.svg b/assets/icons/cloud_8.svg deleted file mode 100644 index 0e0337e7ab..0000000000 --- a/assets/icons/cloud_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_slash_8.svg b/assets/icons/cloud_slash_8.svg deleted file mode 100644 index 785ded0683..0000000000 --- a/assets/icons/cloud_slash_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/copilot_16.svg b/assets/icons/copilot_16.svg deleted file mode 100644 index e14b61ce8b..0000000000 --- a/assets/icons/copilot_16.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/copilot_disabled_16.svg b/assets/icons/copilot_disabled.svg similarity index 100% rename from assets/icons/copilot_disabled_16.svg rename to assets/icons/copilot_disabled.svg diff --git a/assets/icons/copilot_error_16.svg b/assets/icons/copilot_error.svg similarity index 100% rename from assets/icons/copilot_error_16.svg rename to assets/icons/copilot_error.svg diff --git a/assets/icons/copilot_init_16.svg b/assets/icons/copilot_init.svg similarity index 100% rename from assets/icons/copilot_init_16.svg rename to assets/icons/copilot_init.svg diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg deleted file mode 100644 index 4aa44979c3..0000000000 --- a/assets/icons/copy.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/delete_12.svg b/assets/icons/delete_12.svg deleted file mode 100644 index 68bad3da26..0000000000 --- a/assets/icons/delete_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/delete_16.svg b/assets/icons/delete_16.svg deleted file mode 100644 index 965470690e..0000000000 --- a/assets/icons/delete_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/delete_8.svg b/assets/icons/delete_8.svg deleted file mode 100644 index 60972007b6..0000000000 --- a/assets/icons/delete_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/desktop.svg b/assets/icons/desktop.svg similarity index 100% rename from assets/icons/radix/desktop.svg rename to assets/icons/desktop.svg diff --git a/assets/icons/disable_screen_sharing_12.svg b/assets/icons/disable_screen_sharing_12.svg deleted file mode 100644 index c2a4edd45b..0000000000 --- a/assets/icons/disable_screen_sharing_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/cloud_slash_12.svg b/assets/icons/disconnected.svg similarity index 100% rename from assets/icons/cloud_slash_12.svg rename to assets/icons/disconnected.svg diff --git a/assets/icons/dock_bottom_12.svg b/assets/icons/dock_bottom_12.svg deleted file mode 100644 index a8099443be..0000000000 --- a/assets/icons/dock_bottom_12.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_bottom_8.svg b/assets/icons/dock_bottom_8.svg deleted file mode 100644 index 005ac423ad..0000000000 --- a/assets/icons/dock_bottom_8.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_modal_12.svg b/assets/icons/dock_modal_12.svg deleted file mode 100644 index 792baee49c..0000000000 --- a/assets/icons/dock_modal_12.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_modal_8.svg b/assets/icons/dock_modal_8.svg deleted file mode 100644 index c6f4039004..0000000000 --- a/assets/icons/dock_modal_8.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_right_12.svg b/assets/icons/dock_right_12.svg deleted file mode 100644 index 84cc1a0c2b..0000000000 --- a/assets/icons/dock_right_12.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/dock_right_8.svg b/assets/icons/dock_right_8.svg deleted file mode 100644 index 842f41bc8c..0000000000 --- a/assets/icons/dock_right_8.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/assets/icons/download_12.svg b/assets/icons/download.svg similarity index 100% rename from assets/icons/download_12.svg rename to assets/icons/download.svg diff --git a/assets/icons/download_8.svg b/assets/icons/download_8.svg deleted file mode 100644 index fb8b021d6b..0000000000 --- a/assets/icons/download_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/ellipsis_14.svg b/assets/icons/ellipsis_14.svg deleted file mode 100644 index 5d45af2b6f..0000000000 --- a/assets/icons/ellipsis_14.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/enable_screen_sharing_12.svg b/assets/icons/enable_screen_sharing_12.svg deleted file mode 100644 index 6ae37649d2..0000000000 --- a/assets/icons/enable_screen_sharing_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg index 7e45535773..2cc6ce120d 100644 --- a/assets/icons/exit.svg +++ b/assets/icons/exit.svg @@ -1,4 +1,8 @@ - - - + + diff --git a/assets/icons/link_out_12.svg b/assets/icons/external_link.svg similarity index 100% rename from assets/icons/link_out_12.svg rename to assets/icons/external_link.svg diff --git a/assets/icons/feedback_16.svg b/assets/icons/feedback_16.svg deleted file mode 100644 index b85a40b353..0000000000 --- a/assets/icons/feedback_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/file.svg b/assets/icons/file.svg similarity index 100% rename from assets/icons/radix/file.svg rename to assets/icons/file.svg diff --git a/assets/icons/file_12.svg b/assets/icons/file_12.svg deleted file mode 100644 index 191e3d7fae..0000000000 --- a/assets/icons/file_12.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_16.svg b/assets/icons/file_16.svg deleted file mode 100644 index 79fd1f81cb..0000000000 --- a/assets/icons/file_16.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/file_8.svg b/assets/icons/file_8.svg deleted file mode 100644 index 2e636bd3b3..0000000000 --- a/assets/icons/file_8.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/filter_12.svg b/assets/icons/filter_12.svg deleted file mode 100644 index 9c1ad5ba5c..0000000000 --- a/assets/icons/filter_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/filter_14.svg b/assets/icons/filter_14.svg deleted file mode 100644 index 379be15b51..0000000000 --- a/assets/icons/filter_14.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/folder_tree_12.svg b/assets/icons/folder_tree_12.svg deleted file mode 100644 index 580514f74d..0000000000 --- a/assets/icons/folder_tree_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/folder_tree_16.svg b/assets/icons/folder_tree_16.svg deleted file mode 100644 index a264a32573..0000000000 --- a/assets/icons/folder_tree_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/folder_tree_8.svg b/assets/icons/folder_tree_8.svg deleted file mode 100644 index 07ac18e19f..0000000000 --- a/assets/icons/folder_tree_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/git_diff_12.svg b/assets/icons/git_diff_12.svg deleted file mode 100644 index 0a3bb473c2..0000000000 --- a/assets/icons/git_diff_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/git_diff_8.svg b/assets/icons/git_diff_8.svg deleted file mode 100644 index 64290de860..0000000000 --- a/assets/icons/git_diff_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/github-copilot-dummy.svg b/assets/icons/github-copilot-dummy.svg deleted file mode 100644 index 4a7ded3976..0000000000 --- a/assets/icons/github-copilot-dummy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/html.svg b/assets/icons/html.svg deleted file mode 100644 index 1e676fe313..0000000000 --- a/assets/icons/html.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/kebab.svg b/assets/icons/kebab.svg deleted file mode 100644 index 1858c65520..0000000000 --- a/assets/icons/kebab.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/leave_12.svg b/assets/icons/leave_12.svg deleted file mode 100644 index 84491384b8..0000000000 --- a/assets/icons/leave_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg deleted file mode 100644 index 652f45a7e8..0000000000 --- a/assets/icons/lock.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/lock_8.svg b/assets/icons/lock_8.svg deleted file mode 100644 index 8df83dc0b5..0000000000 --- a/assets/icons/lock_8.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/logo_96.svg b/assets/icons/logo_96.svg deleted file mode 100644 index dc98bb8bc2..0000000000 --- a/assets/icons/logo_96.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/magic-wand.svg b/assets/icons/magic-wand.svg similarity index 100% rename from assets/icons/radix/magic-wand.svg rename to assets/icons/magic-wand.svg diff --git a/assets/icons/magnifying_glass_12.svg b/assets/icons/magnifying_glass_12.svg deleted file mode 100644 index b9ac5d35b2..0000000000 --- a/assets/icons/magnifying_glass_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/magnifying_glass_16.svg b/assets/icons/magnifying_glass_16.svg deleted file mode 100644 index f35343e8d3..0000000000 --- a/assets/icons/magnifying_glass_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/magnifying_glass_8.svg b/assets/icons/magnifying_glass_8.svg deleted file mode 100644 index d0deb1cdba..0000000000 --- a/assets/icons/magnifying_glass_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/match_case.svg b/assets/icons/match_case.svg deleted file mode 100644 index 82f4529c1b..0000000000 --- a/assets/icons/match_case.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/match_word.svg b/assets/icons/match_word.svg deleted file mode 100644 index 69ba8eb9e6..0000000000 --- a/assets/icons/match_word.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index 4dc7755714..f37f6a2087 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/maximize_8.svg b/assets/icons/maximize_8.svg deleted file mode 100644 index 76d29a9d22..0000000000 --- a/assets/icons/maximize_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/hamburger_15.svg b/assets/icons/menu.svg similarity index 100% rename from assets/icons/hamburger_15.svg rename to assets/icons/menu.svg diff --git a/assets/icons/radix/mic-mute.svg b/assets/icons/mic-mute.svg similarity index 100% rename from assets/icons/radix/mic-mute.svg rename to assets/icons/mic-mute.svg diff --git a/assets/icons/radix/mic.svg b/assets/icons/mic.svg similarity index 100% rename from assets/icons/radix/mic.svg rename to assets/icons/mic.svg diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg deleted file mode 100644 index 8974fd939d..0000000000 --- a/assets/icons/microphone.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index d8941ee1f0..ec78f152e1 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/minimize_8.svg b/assets/icons/minimize_8.svg deleted file mode 100644 index b511cbd355..0000000000 --- a/assets/icons/minimize_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index a54dd0ad66..57ce90219b 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,3 +1,8 @@ - - + + diff --git a/assets/icons/plus_12.svg b/assets/icons/plus_12.svg deleted file mode 100644 index f1770fa248..0000000000 --- a/assets/icons/plus_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/plus_16.svg b/assets/icons/plus_16.svg deleted file mode 100644 index c595cf597a..0000000000 --- a/assets/icons/plus_16.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/plus_8.svg b/assets/icons/plus_8.svg deleted file mode 100644 index 72efa1574e..0000000000 --- a/assets/icons/plus_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/radix/quote.svg b/assets/icons/quote.svg similarity index 100% rename from assets/icons/radix/quote.svg rename to assets/icons/quote.svg diff --git a/assets/icons/quote_15.svg b/assets/icons/quote_15.svg deleted file mode 100644 index be5eabd9b0..0000000000 --- a/assets/icons/quote_15.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/radix/accessibility.svg b/assets/icons/radix/accessibility.svg deleted file mode 100644 index 32d78f2d8d..0000000000 --- a/assets/icons/radix/accessibility.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/activity-log.svg b/assets/icons/radix/activity-log.svg deleted file mode 100644 index 8feab7d449..0000000000 --- a/assets/icons/radix/activity-log.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-baseline.svg b/assets/icons/radix/align-baseline.svg deleted file mode 100644 index 07213dc1ae..0000000000 --- a/assets/icons/radix/align-baseline.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-bottom.svg b/assets/icons/radix/align-bottom.svg deleted file mode 100644 index 7d11c0cd5a..0000000000 --- a/assets/icons/radix/align-bottom.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-center-horizontally.svg b/assets/icons/radix/align-center-horizontally.svg deleted file mode 100644 index 69509a7d09..0000000000 --- a/assets/icons/radix/align-center-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-center-vertically.svg b/assets/icons/radix/align-center-vertically.svg deleted file mode 100644 index 4f1b50cc43..0000000000 --- a/assets/icons/radix/align-center-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-center.svg b/assets/icons/radix/align-center.svg deleted file mode 100644 index caaec36477..0000000000 --- a/assets/icons/radix/align-center.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-end.svg b/assets/icons/radix/align-end.svg deleted file mode 100644 index 18f1b64912..0000000000 --- a/assets/icons/radix/align-end.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-horizontal-centers.svg b/assets/icons/radix/align-horizontal-centers.svg deleted file mode 100644 index 2d1d64ea4b..0000000000 --- a/assets/icons/radix/align-horizontal-centers.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-left.svg b/assets/icons/radix/align-left.svg deleted file mode 100644 index 0d5dba095c..0000000000 --- a/assets/icons/radix/align-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-right.svg b/assets/icons/radix/align-right.svg deleted file mode 100644 index 1b6b3f0ffa..0000000000 --- a/assets/icons/radix/align-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-start.svg b/assets/icons/radix/align-start.svg deleted file mode 100644 index ada50e1079..0000000000 --- a/assets/icons/radix/align-start.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-stretch.svg b/assets/icons/radix/align-stretch.svg deleted file mode 100644 index 3cb28605cb..0000000000 --- a/assets/icons/radix/align-stretch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-top.svg b/assets/icons/radix/align-top.svg deleted file mode 100644 index 23db80f4dd..0000000000 --- a/assets/icons/radix/align-top.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/align-vertical-centers.svg b/assets/icons/radix/align-vertical-centers.svg deleted file mode 100644 index 07eaee7bf7..0000000000 --- a/assets/icons/radix/align-vertical-centers.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/all-sides.svg b/assets/icons/radix/all-sides.svg deleted file mode 100644 index 8ace7df03f..0000000000 --- a/assets/icons/radix/all-sides.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/angle.svg b/assets/icons/radix/angle.svg deleted file mode 100644 index a0d93f3460..0000000000 --- a/assets/icons/radix/angle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/archive.svg b/assets/icons/radix/archive.svg deleted file mode 100644 index 74063f1d1e..0000000000 --- a/assets/icons/radix/archive.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-bottom-left.svg b/assets/icons/radix/arrow-bottom-left.svg deleted file mode 100644 index 7a4511aa2d..0000000000 --- a/assets/icons/radix/arrow-bottom-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-bottom-right.svg b/assets/icons/radix/arrow-bottom-right.svg deleted file mode 100644 index 2ba9fef101..0000000000 --- a/assets/icons/radix/arrow-bottom-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-down.svg b/assets/icons/radix/arrow-down.svg deleted file mode 100644 index 5dc21a6689..0000000000 --- a/assets/icons/radix/arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-left.svg b/assets/icons/radix/arrow-left.svg deleted file mode 100644 index 3a64c8394f..0000000000 --- a/assets/icons/radix/arrow-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-right.svg b/assets/icons/radix/arrow-right.svg deleted file mode 100644 index e3d30988d5..0000000000 --- a/assets/icons/radix/arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-top-left.svg b/assets/icons/radix/arrow-top-left.svg deleted file mode 100644 index 69fef41dee..0000000000 --- a/assets/icons/radix/arrow-top-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-top-right.svg b/assets/icons/radix/arrow-top-right.svg deleted file mode 100644 index c1016376e3..0000000000 --- a/assets/icons/radix/arrow-top-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/arrow-up.svg b/assets/icons/radix/arrow-up.svg deleted file mode 100644 index ba426119e9..0000000000 --- a/assets/icons/radix/arrow-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/aspect-ratio.svg b/assets/icons/radix/aspect-ratio.svg deleted file mode 100644 index 0851f2e1e9..0000000000 --- a/assets/icons/radix/aspect-ratio.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/avatar.svg b/assets/icons/radix/avatar.svg deleted file mode 100644 index cb229c77fe..0000000000 --- a/assets/icons/radix/avatar.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/backpack.svg b/assets/icons/radix/backpack.svg deleted file mode 100644 index a5c9cedbd3..0000000000 --- a/assets/icons/radix/backpack.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/badge.svg b/assets/icons/radix/badge.svg deleted file mode 100644 index aa764d4726..0000000000 --- a/assets/icons/radix/badge.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bar-chart.svg b/assets/icons/radix/bar-chart.svg deleted file mode 100644 index f8054781d9..0000000000 --- a/assets/icons/radix/bar-chart.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bell.svg b/assets/icons/radix/bell.svg deleted file mode 100644 index ea1c6dd42e..0000000000 --- a/assets/icons/radix/bell.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/blending-mode.svg b/assets/icons/radix/blending-mode.svg deleted file mode 100644 index bd58cf4ee3..0000000000 --- a/assets/icons/radix/blending-mode.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bookmark-filled.svg b/assets/icons/radix/bookmark-filled.svg deleted file mode 100644 index 5b725cd88d..0000000000 --- a/assets/icons/radix/bookmark-filled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/bookmark.svg b/assets/icons/radix/bookmark.svg deleted file mode 100644 index 90c4d827f1..0000000000 --- a/assets/icons/radix/bookmark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-all.svg b/assets/icons/radix/border-all.svg deleted file mode 100644 index 3bfde7d59b..0000000000 --- a/assets/icons/radix/border-all.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/radix/border-bottom.svg b/assets/icons/radix/border-bottom.svg deleted file mode 100644 index f2d3c3d554..0000000000 --- a/assets/icons/radix/border-bottom.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-dashed.svg b/assets/icons/radix/border-dashed.svg deleted file mode 100644 index 85fdcdfe5d..0000000000 --- a/assets/icons/radix/border-dashed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-dotted.svg b/assets/icons/radix/border-dotted.svg deleted file mode 100644 index 5eb514ed2a..0000000000 --- a/assets/icons/radix/border-dotted.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-left.svg b/assets/icons/radix/border-left.svg deleted file mode 100644 index 5deb197da5..0000000000 --- a/assets/icons/radix/border-left.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-none.svg b/assets/icons/radix/border-none.svg deleted file mode 100644 index 1ad3f59d7c..0000000000 --- a/assets/icons/radix/border-none.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-right.svg b/assets/icons/radix/border-right.svg deleted file mode 100644 index c939095ad7..0000000000 --- a/assets/icons/radix/border-right.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-solid.svg b/assets/icons/radix/border-solid.svg deleted file mode 100644 index 5c0d26a058..0000000000 --- a/assets/icons/radix/border-solid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-split.svg b/assets/icons/radix/border-split.svg deleted file mode 100644 index 7fdf6cc34e..0000000000 --- a/assets/icons/radix/border-split.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-style.svg b/assets/icons/radix/border-style.svg deleted file mode 100644 index f729cb993b..0000000000 --- a/assets/icons/radix/border-style.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/border-top.svg b/assets/icons/radix/border-top.svg deleted file mode 100644 index bde739d755..0000000000 --- a/assets/icons/radix/border-top.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/border-width.svg b/assets/icons/radix/border-width.svg deleted file mode 100644 index 37c270756e..0000000000 --- a/assets/icons/radix/border-width.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/box-model.svg b/assets/icons/radix/box-model.svg deleted file mode 100644 index 45d1a7ce41..0000000000 --- a/assets/icons/radix/box-model.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/box.svg b/assets/icons/radix/box.svg deleted file mode 100644 index 6e035c21ed..0000000000 --- a/assets/icons/radix/box.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/button.svg b/assets/icons/radix/button.svg deleted file mode 100644 index 31622bcf15..0000000000 --- a/assets/icons/radix/button.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/calendar.svg b/assets/icons/radix/calendar.svg deleted file mode 100644 index 2adbe0bc28..0000000000 --- a/assets/icons/radix/calendar.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/camera.svg b/assets/icons/radix/camera.svg deleted file mode 100644 index d7cccf74c2..0000000000 --- a/assets/icons/radix/camera.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/card-stack-minus.svg b/assets/icons/radix/card-stack-minus.svg deleted file mode 100644 index 04d8e51178..0000000000 --- a/assets/icons/radix/card-stack-minus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/card-stack-plus.svg b/assets/icons/radix/card-stack-plus.svg deleted file mode 100644 index a184f4bc1a..0000000000 --- a/assets/icons/radix/card-stack-plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/card-stack.svg b/assets/icons/radix/card-stack.svg deleted file mode 100644 index defea0e165..0000000000 --- a/assets/icons/radix/card-stack.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/caret-left.svg b/assets/icons/radix/caret-left.svg deleted file mode 100644 index 969bc3b95c..0000000000 --- a/assets/icons/radix/caret-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/caret-right.svg b/assets/icons/radix/caret-right.svg deleted file mode 100644 index 75c55d8676..0000000000 --- a/assets/icons/radix/caret-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/caret-sort.svg b/assets/icons/radix/caret-sort.svg deleted file mode 100644 index a65e20b660..0000000000 --- a/assets/icons/radix/caret-sort.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chat-bubble.svg b/assets/icons/radix/chat-bubble.svg deleted file mode 100644 index 5766f46de8..0000000000 --- a/assets/icons/radix/chat-bubble.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/check-circled.svg b/assets/icons/radix/check-circled.svg deleted file mode 100644 index 19ee22eb51..0000000000 --- a/assets/icons/radix/check-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/check.svg b/assets/icons/radix/check.svg deleted file mode 100644 index 476a3baa18..0000000000 --- a/assets/icons/radix/check.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/checkbox.svg b/assets/icons/radix/checkbox.svg deleted file mode 100644 index d6bb3c7ef2..0000000000 --- a/assets/icons/radix/checkbox.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-down.svg b/assets/icons/radix/chevron-down.svg deleted file mode 100644 index 175c1312fd..0000000000 --- a/assets/icons/radix/chevron-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-left.svg b/assets/icons/radix/chevron-left.svg deleted file mode 100644 index d7628202f2..0000000000 --- a/assets/icons/radix/chevron-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-right.svg b/assets/icons/radix/chevron-right.svg deleted file mode 100644 index e3ebd73d99..0000000000 --- a/assets/icons/radix/chevron-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/chevron-up.svg b/assets/icons/radix/chevron-up.svg deleted file mode 100644 index 0e8e796dab..0000000000 --- a/assets/icons/radix/chevron-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/circle-backslash.svg b/assets/icons/radix/circle-backslash.svg deleted file mode 100644 index 40c4dd5398..0000000000 --- a/assets/icons/radix/circle-backslash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/circle.svg b/assets/icons/radix/circle.svg deleted file mode 100644 index ba4a8f22fe..0000000000 --- a/assets/icons/radix/circle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/clipboard-copy.svg b/assets/icons/radix/clipboard-copy.svg deleted file mode 100644 index 5293fdc493..0000000000 --- a/assets/icons/radix/clipboard-copy.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/clipboard.svg b/assets/icons/radix/clipboard.svg deleted file mode 100644 index e18b32943b..0000000000 --- a/assets/icons/radix/clipboard.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/clock.svg b/assets/icons/radix/clock.svg deleted file mode 100644 index ac3b526fbb..0000000000 --- a/assets/icons/radix/clock.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/code.svg b/assets/icons/radix/code.svg deleted file mode 100644 index 70fe381b68..0000000000 --- a/assets/icons/radix/code.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/codesandbox-logo.svg b/assets/icons/radix/codesandbox-logo.svg deleted file mode 100644 index 4a3f549c2f..0000000000 --- a/assets/icons/radix/codesandbox-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/color-wheel.svg b/assets/icons/radix/color-wheel.svg deleted file mode 100644 index 2153b84428..0000000000 --- a/assets/icons/radix/color-wheel.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/column-spacing.svg b/assets/icons/radix/column-spacing.svg deleted file mode 100644 index aafcf555cb..0000000000 --- a/assets/icons/radix/column-spacing.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/columns.svg b/assets/icons/radix/columns.svg deleted file mode 100644 index e1607611b1..0000000000 --- a/assets/icons/radix/columns.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/commit.svg b/assets/icons/radix/commit.svg deleted file mode 100644 index ac128a2b08..0000000000 --- a/assets/icons/radix/commit.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-1.svg b/assets/icons/radix/component-1.svg deleted file mode 100644 index e3e9f38af1..0000000000 --- a/assets/icons/radix/component-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-2.svg b/assets/icons/radix/component-2.svg deleted file mode 100644 index df2091d143..0000000000 --- a/assets/icons/radix/component-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-boolean.svg b/assets/icons/radix/component-boolean.svg deleted file mode 100644 index 942e8832eb..0000000000 --- a/assets/icons/radix/component-boolean.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-instance.svg b/assets/icons/radix/component-instance.svg deleted file mode 100644 index 048c401291..0000000000 --- a/assets/icons/radix/component-instance.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-none.svg b/assets/icons/radix/component-none.svg deleted file mode 100644 index a622c3ee96..0000000000 --- a/assets/icons/radix/component-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/component-placeholder.svg b/assets/icons/radix/component-placeholder.svg deleted file mode 100644 index b8892d5d23..0000000000 --- a/assets/icons/radix/component-placeholder.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/assets/icons/radix/container.svg b/assets/icons/radix/container.svg deleted file mode 100644 index 1c2a4fd0e1..0000000000 --- a/assets/icons/radix/container.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cookie.svg b/assets/icons/radix/cookie.svg deleted file mode 100644 index 8c165601a2..0000000000 --- a/assets/icons/radix/cookie.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/copy.svg b/assets/icons/radix/copy.svg deleted file mode 100644 index bf2b504ecf..0000000000 --- a/assets/icons/radix/copy.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-bottom-left.svg b/assets/icons/radix/corner-bottom-left.svg deleted file mode 100644 index 26df9dbad8..0000000000 --- a/assets/icons/radix/corner-bottom-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-bottom-right.svg b/assets/icons/radix/corner-bottom-right.svg deleted file mode 100644 index 15e3957123..0000000000 --- a/assets/icons/radix/corner-bottom-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-top-left.svg b/assets/icons/radix/corner-top-left.svg deleted file mode 100644 index 8fc1b84b82..0000000000 --- a/assets/icons/radix/corner-top-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corner-top-right.svg b/assets/icons/radix/corner-top-right.svg deleted file mode 100644 index 533ea6c678..0000000000 --- a/assets/icons/radix/corner-top-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/corners.svg b/assets/icons/radix/corners.svg deleted file mode 100644 index c41c4e0183..0000000000 --- a/assets/icons/radix/corners.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/countdown-timer.svg b/assets/icons/radix/countdown-timer.svg deleted file mode 100644 index 58494bd416..0000000000 --- a/assets/icons/radix/countdown-timer.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/counter-clockwise-clock.svg b/assets/icons/radix/counter-clockwise-clock.svg deleted file mode 100644 index 0b3acbcebf..0000000000 --- a/assets/icons/radix/counter-clockwise-clock.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crop.svg b/assets/icons/radix/crop.svg deleted file mode 100644 index 008457fff6..0000000000 --- a/assets/icons/radix/crop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cross-1.svg b/assets/icons/radix/cross-1.svg deleted file mode 100644 index 62135d27ed..0000000000 --- a/assets/icons/radix/cross-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cross-2.svg b/assets/icons/radix/cross-2.svg deleted file mode 100644 index 4c55700928..0000000000 --- a/assets/icons/radix/cross-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cross-circled.svg b/assets/icons/radix/cross-circled.svg deleted file mode 100644 index df3cb896c8..0000000000 --- a/assets/icons/radix/cross-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crosshair-1.svg b/assets/icons/radix/crosshair-1.svg deleted file mode 100644 index 05b22f8461..0000000000 --- a/assets/icons/radix/crosshair-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crosshair-2.svg b/assets/icons/radix/crosshair-2.svg deleted file mode 100644 index f5ee0a92af..0000000000 --- a/assets/icons/radix/crosshair-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/crumpled-paper.svg b/assets/icons/radix/crumpled-paper.svg deleted file mode 100644 index 33e9b65581..0000000000 --- a/assets/icons/radix/crumpled-paper.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cube.svg b/assets/icons/radix/cube.svg deleted file mode 100644 index b327158be4..0000000000 --- a/assets/icons/radix/cube.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cursor-arrow.svg b/assets/icons/radix/cursor-arrow.svg deleted file mode 100644 index b0227e4ded..0000000000 --- a/assets/icons/radix/cursor-arrow.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/cursor-text.svg b/assets/icons/radix/cursor-text.svg deleted file mode 100644 index 05939503b8..0000000000 --- a/assets/icons/radix/cursor-text.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dash.svg b/assets/icons/radix/dash.svg deleted file mode 100644 index d70daf7fed..0000000000 --- a/assets/icons/radix/dash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dashboard.svg b/assets/icons/radix/dashboard.svg deleted file mode 100644 index 38008c64e4..0000000000 --- a/assets/icons/radix/dashboard.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/desktop-mute.svg b/assets/icons/radix/desktop-mute.svg deleted file mode 100644 index 83d249176f..0000000000 --- a/assets/icons/radix/desktop-mute.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/radix/dimensions.svg b/assets/icons/radix/dimensions.svg deleted file mode 100644 index 767d1d2896..0000000000 --- a/assets/icons/radix/dimensions.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/disc.svg b/assets/icons/radix/disc.svg deleted file mode 100644 index 6e19caab35..0000000000 --- a/assets/icons/radix/disc.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/discord-logo.svg b/assets/icons/radix/discord-logo.svg deleted file mode 100644 index 50567c212e..0000000000 --- a/assets/icons/radix/discord-logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - diff --git a/assets/icons/radix/divider-horizontal.svg b/assets/icons/radix/divider-horizontal.svg deleted file mode 100644 index 59e43649c9..0000000000 --- a/assets/icons/radix/divider-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/divider-vertical.svg b/assets/icons/radix/divider-vertical.svg deleted file mode 100644 index 95f5cc8f2f..0000000000 --- a/assets/icons/radix/divider-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dot-filled.svg b/assets/icons/radix/dot-filled.svg deleted file mode 100644 index 0c1a17b3bd..0000000000 --- a/assets/icons/radix/dot-filled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/dot-solid.svg b/assets/icons/radix/dot-solid.svg deleted file mode 100644 index 0c1a17b3bd..0000000000 --- a/assets/icons/radix/dot-solid.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/dot.svg b/assets/icons/radix/dot.svg deleted file mode 100644 index c553a1422d..0000000000 --- a/assets/icons/radix/dot.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dots-horizontal.svg b/assets/icons/radix/dots-horizontal.svg deleted file mode 100644 index 347d1ae13d..0000000000 --- a/assets/icons/radix/dots-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dots-vertical.svg b/assets/icons/radix/dots-vertical.svg deleted file mode 100644 index 5ca1a181e3..0000000000 --- a/assets/icons/radix/dots-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-down.svg b/assets/icons/radix/double-arrow-down.svg deleted file mode 100644 index 8b86db2f8a..0000000000 --- a/assets/icons/radix/double-arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-left.svg b/assets/icons/radix/double-arrow-left.svg deleted file mode 100644 index 0ef30ff955..0000000000 --- a/assets/icons/radix/double-arrow-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-right.svg b/assets/icons/radix/double-arrow-right.svg deleted file mode 100644 index 9997fdc403..0000000000 --- a/assets/icons/radix/double-arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/double-arrow-up.svg b/assets/icons/radix/double-arrow-up.svg deleted file mode 100644 index 8d571fcd66..0000000000 --- a/assets/icons/radix/double-arrow-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/download.svg b/assets/icons/radix/download.svg deleted file mode 100644 index 49a05d5f47..0000000000 --- a/assets/icons/radix/download.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drag-handle-dots-1.svg b/assets/icons/radix/drag-handle-dots-1.svg deleted file mode 100644 index fc046bb9d9..0000000000 --- a/assets/icons/radix/drag-handle-dots-1.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/radix/drag-handle-dots-2.svg b/assets/icons/radix/drag-handle-dots-2.svg deleted file mode 100644 index aed0e702d7..0000000000 --- a/assets/icons/radix/drag-handle-dots-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drag-handle-horizontal.svg b/assets/icons/radix/drag-handle-horizontal.svg deleted file mode 100644 index c1bb138a24..0000000000 --- a/assets/icons/radix/drag-handle-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drag-handle-vertical.svg b/assets/icons/radix/drag-handle-vertical.svg deleted file mode 100644 index 8d48c7894a..0000000000 --- a/assets/icons/radix/drag-handle-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/drawing-pin-filled.svg b/assets/icons/radix/drawing-pin-filled.svg deleted file mode 100644 index e1894619c3..0000000000 --- a/assets/icons/radix/drawing-pin-filled.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/assets/icons/radix/drawing-pin-solid.svg b/assets/icons/radix/drawing-pin-solid.svg deleted file mode 100644 index e1894619c3..0000000000 --- a/assets/icons/radix/drawing-pin-solid.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/assets/icons/radix/drawing-pin.svg b/assets/icons/radix/drawing-pin.svg deleted file mode 100644 index 5625e7588f..0000000000 --- a/assets/icons/radix/drawing-pin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/dropdown-menu.svg b/assets/icons/radix/dropdown-menu.svg deleted file mode 100644 index c938052be8..0000000000 --- a/assets/icons/radix/dropdown-menu.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/enter-full-screen.svg b/assets/icons/radix/enter-full-screen.svg deleted file mode 100644 index d368a6d415..0000000000 --- a/assets/icons/radix/enter-full-screen.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/enter.svg b/assets/icons/radix/enter.svg deleted file mode 100644 index cc57d74cea..0000000000 --- a/assets/icons/radix/enter.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/envelope-closed.svg b/assets/icons/radix/envelope-closed.svg deleted file mode 100644 index 4b5e037840..0000000000 --- a/assets/icons/radix/envelope-closed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/envelope-open.svg b/assets/icons/radix/envelope-open.svg deleted file mode 100644 index df1e3fea95..0000000000 --- a/assets/icons/radix/envelope-open.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eraser.svg b/assets/icons/radix/eraser.svg deleted file mode 100644 index bb448d4d23..0000000000 --- a/assets/icons/radix/eraser.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/exclamation-triangle.svg b/assets/icons/radix/exclamation-triangle.svg deleted file mode 100644 index 210d4c45c6..0000000000 --- a/assets/icons/radix/exclamation-triangle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/exit-full-screen.svg b/assets/icons/radix/exit-full-screen.svg deleted file mode 100644 index 9b6439b043..0000000000 --- a/assets/icons/radix/exit-full-screen.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/exit.svg b/assets/icons/radix/exit.svg deleted file mode 100644 index 2cc6ce120d..0000000000 --- a/assets/icons/radix/exit.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/external-link.svg b/assets/icons/radix/external-link.svg deleted file mode 100644 index 0ee7420162..0000000000 --- a/assets/icons/radix/external-link.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eye-closed.svg b/assets/icons/radix/eye-closed.svg deleted file mode 100644 index f824fe55f9..0000000000 --- a/assets/icons/radix/eye-closed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eye-none.svg b/assets/icons/radix/eye-none.svg deleted file mode 100644 index d4beecd33a..0000000000 --- a/assets/icons/radix/eye-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/eye-open.svg b/assets/icons/radix/eye-open.svg deleted file mode 100644 index d39d26b2c1..0000000000 --- a/assets/icons/radix/eye-open.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/face.svg b/assets/icons/radix/face.svg deleted file mode 100644 index 81b14dd8d7..0000000000 --- a/assets/icons/radix/face.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/figma-logo.svg b/assets/icons/radix/figma-logo.svg deleted file mode 100644 index 6c19276554..0000000000 --- a/assets/icons/radix/figma-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/file-minus.svg b/assets/icons/radix/file-minus.svg deleted file mode 100644 index bd1a841881..0000000000 --- a/assets/icons/radix/file-minus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/file-plus.svg b/assets/icons/radix/file-plus.svg deleted file mode 100644 index 2396e20015..0000000000 --- a/assets/icons/radix/file-plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/file-text.svg b/assets/icons/radix/file-text.svg deleted file mode 100644 index f341ab8abf..0000000000 --- a/assets/icons/radix/file-text.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-bold.svg b/assets/icons/radix/font-bold.svg deleted file mode 100644 index 7dc6caf3b0..0000000000 --- a/assets/icons/radix/font-bold.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/font-family.svg b/assets/icons/radix/font-family.svg deleted file mode 100644 index 9134b9086d..0000000000 --- a/assets/icons/radix/font-family.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/font-italic.svg b/assets/icons/radix/font-italic.svg deleted file mode 100644 index 6e6288d6bc..0000000000 --- a/assets/icons/radix/font-italic.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-roman.svg b/assets/icons/radix/font-roman.svg deleted file mode 100644 index c595b790fc..0000000000 --- a/assets/icons/radix/font-roman.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-size.svg b/assets/icons/radix/font-size.svg deleted file mode 100644 index e389a58d73..0000000000 --- a/assets/icons/radix/font-size.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/font-style.svg b/assets/icons/radix/font-style.svg deleted file mode 100644 index 31c3730130..0000000000 --- a/assets/icons/radix/font-style.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/frame.svg b/assets/icons/radix/frame.svg deleted file mode 100644 index ec61a48efa..0000000000 --- a/assets/icons/radix/frame.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/framer-logo.svg b/assets/icons/radix/framer-logo.svg deleted file mode 100644 index 68be3b317b..0000000000 --- a/assets/icons/radix/framer-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/gear.svg b/assets/icons/radix/gear.svg deleted file mode 100644 index 52f9e17312..0000000000 --- a/assets/icons/radix/gear.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/github-logo.svg b/assets/icons/radix/github-logo.svg deleted file mode 100644 index e46612cf56..0000000000 --- a/assets/icons/radix/github-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/globe.svg b/assets/icons/radix/globe.svg deleted file mode 100644 index 4728b827df..0000000000 --- a/assets/icons/radix/globe.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - diff --git a/assets/icons/radix/grid.svg b/assets/icons/radix/grid.svg deleted file mode 100644 index 5d9af33572..0000000000 --- a/assets/icons/radix/grid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/group.svg b/assets/icons/radix/group.svg deleted file mode 100644 index c3c91d211f..0000000000 --- a/assets/icons/radix/group.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/half-1.svg b/assets/icons/radix/half-1.svg deleted file mode 100644 index 9890e26bb8..0000000000 --- a/assets/icons/radix/half-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/half-2.svg b/assets/icons/radix/half-2.svg deleted file mode 100644 index 4db1d564cb..0000000000 --- a/assets/icons/radix/half-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/hamburger-menu.svg b/assets/icons/radix/hamburger-menu.svg deleted file mode 100644 index 039168055b..0000000000 --- a/assets/icons/radix/hamburger-menu.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/hand.svg b/assets/icons/radix/hand.svg deleted file mode 100644 index 12afac8f5f..0000000000 --- a/assets/icons/radix/hand.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/heading.svg b/assets/icons/radix/heading.svg deleted file mode 100644 index 0a5e2caaf1..0000000000 --- a/assets/icons/radix/heading.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/heart-filled.svg b/assets/icons/radix/heart-filled.svg deleted file mode 100644 index 94928accd7..0000000000 --- a/assets/icons/radix/heart-filled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/heart.svg b/assets/icons/radix/heart.svg deleted file mode 100644 index 91cbc450fd..0000000000 --- a/assets/icons/radix/heart.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/height.svg b/assets/icons/radix/height.svg deleted file mode 100644 index 28424f4d51..0000000000 --- a/assets/icons/radix/height.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/hobby-knife.svg b/assets/icons/radix/hobby-knife.svg deleted file mode 100644 index c2ed3fb1ed..0000000000 --- a/assets/icons/radix/hobby-knife.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/home.svg b/assets/icons/radix/home.svg deleted file mode 100644 index 733bd79113..0000000000 --- a/assets/icons/radix/home.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/iconjar-logo.svg b/assets/icons/radix/iconjar-logo.svg deleted file mode 100644 index c154b4e864..0000000000 --- a/assets/icons/radix/iconjar-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/id-card.svg b/assets/icons/radix/id-card.svg deleted file mode 100644 index efde9ffa7e..0000000000 --- a/assets/icons/radix/id-card.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/image.svg b/assets/icons/radix/image.svg deleted file mode 100644 index 0ff4475252..0000000000 --- a/assets/icons/radix/image.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/info-circled.svg b/assets/icons/radix/info-circled.svg deleted file mode 100644 index 4ab1b260e3..0000000000 --- a/assets/icons/radix/info-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/inner-shadow.svg b/assets/icons/radix/inner-shadow.svg deleted file mode 100644 index 1056a7bffc..0000000000 --- a/assets/icons/radix/inner-shadow.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/input.svg b/assets/icons/radix/input.svg deleted file mode 100644 index 4ed4605b2c..0000000000 --- a/assets/icons/radix/input.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/instagram-logo.svg b/assets/icons/radix/instagram-logo.svg deleted file mode 100644 index 5d78937966..0000000000 --- a/assets/icons/radix/instagram-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/assets/icons/radix/justify-center.svg b/assets/icons/radix/justify-center.svg deleted file mode 100644 index 7999a4ea46..0000000000 --- a/assets/icons/radix/justify-center.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/justify-end.svg b/assets/icons/radix/justify-end.svg deleted file mode 100644 index bb52f493d7..0000000000 --- a/assets/icons/radix/justify-end.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/justify-start.svg b/assets/icons/radix/justify-start.svg deleted file mode 100644 index 648ca0b603..0000000000 --- a/assets/icons/radix/justify-start.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/justify-stretch.svg b/assets/icons/radix/justify-stretch.svg deleted file mode 100644 index 83df0a8959..0000000000 --- a/assets/icons/radix/justify-stretch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/keyboard.svg b/assets/icons/radix/keyboard.svg deleted file mode 100644 index fc6f86bfc2..0000000000 --- a/assets/icons/radix/keyboard.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/assets/icons/radix/lap-timer.svg b/assets/icons/radix/lap-timer.svg deleted file mode 100644 index 1de0b3be6c..0000000000 --- a/assets/icons/radix/lap-timer.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/laptop.svg b/assets/icons/radix/laptop.svg deleted file mode 100644 index 6aff5d6d44..0000000000 --- a/assets/icons/radix/laptop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/layers.svg b/assets/icons/radix/layers.svg deleted file mode 100644 index 821993fc70..0000000000 --- a/assets/icons/radix/layers.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/layout.svg b/assets/icons/radix/layout.svg deleted file mode 100644 index 8e4a352f50..0000000000 --- a/assets/icons/radix/layout.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-capitalize.svg b/assets/icons/radix/letter-case-capitalize.svg deleted file mode 100644 index 16617ecf7e..0000000000 --- a/assets/icons/radix/letter-case-capitalize.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-lowercase.svg b/assets/icons/radix/letter-case-lowercase.svg deleted file mode 100644 index 61aefb9aad..0000000000 --- a/assets/icons/radix/letter-case-lowercase.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-toggle.svg b/assets/icons/radix/letter-case-toggle.svg deleted file mode 100644 index a021a2b922..0000000000 --- a/assets/icons/radix/letter-case-toggle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-case-uppercase.svg b/assets/icons/radix/letter-case-uppercase.svg deleted file mode 100644 index ccd2be04e7..0000000000 --- a/assets/icons/radix/letter-case-uppercase.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/letter-spacing.svg b/assets/icons/radix/letter-spacing.svg deleted file mode 100644 index 073023e0f4..0000000000 --- a/assets/icons/radix/letter-spacing.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lightning-bolt.svg b/assets/icons/radix/lightning-bolt.svg deleted file mode 100644 index 7c35df9cfe..0000000000 --- a/assets/icons/radix/lightning-bolt.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/line-height.svg b/assets/icons/radix/line-height.svg deleted file mode 100644 index 1c302d1ffc..0000000000 --- a/assets/icons/radix/line-height.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-1.svg b/assets/icons/radix/link-1.svg deleted file mode 100644 index d5682b113e..0000000000 --- a/assets/icons/radix/link-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-2.svg b/assets/icons/radix/link-2.svg deleted file mode 100644 index be8370606e..0000000000 --- a/assets/icons/radix/link-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-break-1.svg b/assets/icons/radix/link-break-1.svg deleted file mode 100644 index 05ae93e47a..0000000000 --- a/assets/icons/radix/link-break-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-break-2.svg b/assets/icons/radix/link-break-2.svg deleted file mode 100644 index 78f28f98e8..0000000000 --- a/assets/icons/radix/link-break-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-none-1.svg b/assets/icons/radix/link-none-1.svg deleted file mode 100644 index 6ea56a386f..0000000000 --- a/assets/icons/radix/link-none-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/link-none-2.svg b/assets/icons/radix/link-none-2.svg deleted file mode 100644 index 0b19d940d1..0000000000 --- a/assets/icons/radix/link-none-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/linkedin-logo.svg b/assets/icons/radix/linkedin-logo.svg deleted file mode 100644 index 0f0138bdf6..0000000000 --- a/assets/icons/radix/linkedin-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/list-bullet.svg b/assets/icons/radix/list-bullet.svg deleted file mode 100644 index 2630b95ef0..0000000000 --- a/assets/icons/radix/list-bullet.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lock-closed.svg b/assets/icons/radix/lock-closed.svg deleted file mode 100644 index 3871b5d5ad..0000000000 --- a/assets/icons/radix/lock-closed.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lock-open-1.svg b/assets/icons/radix/lock-open-1.svg deleted file mode 100644 index 8f6bfd5bbf..0000000000 --- a/assets/icons/radix/lock-open-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/lock-open-2.svg b/assets/icons/radix/lock-open-2.svg deleted file mode 100644 index ce69f67f29..0000000000 --- a/assets/icons/radix/lock-open-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/loop.svg b/assets/icons/radix/loop.svg deleted file mode 100644 index bfa90ed084..0000000000 --- a/assets/icons/radix/loop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/magnifying-glass.svg b/assets/icons/radix/magnifying-glass.svg deleted file mode 100644 index a3a89bfa50..0000000000 --- a/assets/icons/radix/magnifying-glass.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/margin.svg b/assets/icons/radix/margin.svg deleted file mode 100644 index 1a513b37d6..0000000000 --- a/assets/icons/radix/margin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mask-off.svg b/assets/icons/radix/mask-off.svg deleted file mode 100644 index 5f847668e8..0000000000 --- a/assets/icons/radix/mask-off.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mask-on.svg b/assets/icons/radix/mask-on.svg deleted file mode 100644 index 684c1b934d..0000000000 --- a/assets/icons/radix/mask-on.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/maximize.svg b/assets/icons/radix/maximize.svg deleted file mode 100644 index f37f6a2087..0000000000 --- a/assets/icons/radix/maximize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/radix/minimize.svg b/assets/icons/radix/minimize.svg deleted file mode 100644 index ec78f152e1..0000000000 --- a/assets/icons/radix/minimize.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/radix/minus-circled.svg b/assets/icons/radix/minus-circled.svg deleted file mode 100644 index 2c6df4cebf..0000000000 --- a/assets/icons/radix/minus-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/minus.svg b/assets/icons/radix/minus.svg deleted file mode 100644 index 2b39602979..0000000000 --- a/assets/icons/radix/minus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mix.svg b/assets/icons/radix/mix.svg deleted file mode 100644 index 9412a01843..0000000000 --- a/assets/icons/radix/mix.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mixer-horizontal.svg b/assets/icons/radix/mixer-horizontal.svg deleted file mode 100644 index f29ba25548..0000000000 --- a/assets/icons/radix/mixer-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mixer-vertical.svg b/assets/icons/radix/mixer-vertical.svg deleted file mode 100644 index dc85d3a9e7..0000000000 --- a/assets/icons/radix/mixer-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/mobile.svg b/assets/icons/radix/mobile.svg deleted file mode 100644 index b62b6506ff..0000000000 --- a/assets/icons/radix/mobile.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/modulz-logo.svg b/assets/icons/radix/modulz-logo.svg deleted file mode 100644 index 754b229db6..0000000000 --- a/assets/icons/radix/modulz-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/moon.svg b/assets/icons/radix/moon.svg deleted file mode 100644 index 1dac2ca212..0000000000 --- a/assets/icons/radix/moon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/move.svg b/assets/icons/radix/move.svg deleted file mode 100644 index 3d0a0e56c9..0000000000 --- a/assets/icons/radix/move.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/notion-logo.svg b/assets/icons/radix/notion-logo.svg deleted file mode 100644 index c2df152619..0000000000 --- a/assets/icons/radix/notion-logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/opacity.svg b/assets/icons/radix/opacity.svg deleted file mode 100644 index a2d01bff82..0000000000 --- a/assets/icons/radix/opacity.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/open-in-new-window.svg b/assets/icons/radix/open-in-new-window.svg deleted file mode 100644 index 22baf82cff..0000000000 --- a/assets/icons/radix/open-in-new-window.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/assets/icons/radix/outer-shadow.svg b/assets/icons/radix/outer-shadow.svg deleted file mode 100644 index b44e3d553c..0000000000 --- a/assets/icons/radix/outer-shadow.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - diff --git a/assets/icons/radix/overline.svg b/assets/icons/radix/overline.svg deleted file mode 100644 index 57262c76e6..0000000000 --- a/assets/icons/radix/overline.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/padding.svg b/assets/icons/radix/padding.svg deleted file mode 100644 index 483a25a27e..0000000000 --- a/assets/icons/radix/padding.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/paper-plane.svg b/assets/icons/radix/paper-plane.svg deleted file mode 100644 index 37ad070300..0000000000 --- a/assets/icons/radix/paper-plane.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pause.svg b/assets/icons/radix/pause.svg deleted file mode 100644 index b399fb2f5a..0000000000 --- a/assets/icons/radix/pause.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pencil-1.svg b/assets/icons/radix/pencil-1.svg deleted file mode 100644 index decf0122ef..0000000000 --- a/assets/icons/radix/pencil-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pencil-2.svg b/assets/icons/radix/pencil-2.svg deleted file mode 100644 index 2559a393a9..0000000000 --- a/assets/icons/radix/pencil-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/person.svg b/assets/icons/radix/person.svg deleted file mode 100644 index 051abcc703..0000000000 --- a/assets/icons/radix/person.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pie-chart.svg b/assets/icons/radix/pie-chart.svg deleted file mode 100644 index bb58e47274..0000000000 --- a/assets/icons/radix/pie-chart.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pilcrow.svg b/assets/icons/radix/pilcrow.svg deleted file mode 100644 index 6996765fd6..0000000000 --- a/assets/icons/radix/pilcrow.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-bottom.svg b/assets/icons/radix/pin-bottom.svg deleted file mode 100644 index ad0842054f..0000000000 --- a/assets/icons/radix/pin-bottom.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-left.svg b/assets/icons/radix/pin-left.svg deleted file mode 100644 index eb89b2912f..0000000000 --- a/assets/icons/radix/pin-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-right.svg b/assets/icons/radix/pin-right.svg deleted file mode 100644 index 89a98bae4e..0000000000 --- a/assets/icons/radix/pin-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/pin-top.svg b/assets/icons/radix/pin-top.svg deleted file mode 100644 index edfeb64d5d..0000000000 --- a/assets/icons/radix/pin-top.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/play.svg b/assets/icons/radix/play.svg deleted file mode 100644 index 92af9e1ae7..0000000000 --- a/assets/icons/radix/play.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/plus-circled.svg b/assets/icons/radix/plus-circled.svg deleted file mode 100644 index 808ddc4c2c..0000000000 --- a/assets/icons/radix/plus-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/plus.svg b/assets/icons/radix/plus.svg deleted file mode 100644 index 57ce90219b..0000000000 --- a/assets/icons/radix/plus.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/question-mark-circled.svg b/assets/icons/radix/question-mark-circled.svg deleted file mode 100644 index be99968787..0000000000 --- a/assets/icons/radix/question-mark-circled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/question-mark.svg b/assets/icons/radix/question-mark.svg deleted file mode 100644 index 577aae5349..0000000000 --- a/assets/icons/radix/question-mark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/radiobutton.svg b/assets/icons/radix/radiobutton.svg deleted file mode 100644 index f0c3a60aee..0000000000 --- a/assets/icons/radix/radiobutton.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/reader.svg b/assets/icons/radix/reader.svg deleted file mode 100644 index e893cfa685..0000000000 --- a/assets/icons/radix/reader.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/reload.svg b/assets/icons/radix/reload.svg deleted file mode 100644 index cf1dfb7fa2..0000000000 --- a/assets/icons/radix/reload.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/reset.svg b/assets/icons/radix/reset.svg deleted file mode 100644 index f21a508514..0000000000 --- a/assets/icons/radix/reset.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/resume.svg b/assets/icons/radix/resume.svg deleted file mode 100644 index 79cdec2374..0000000000 --- a/assets/icons/radix/resume.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/rocket.svg b/assets/icons/radix/rocket.svg deleted file mode 100644 index 2226aacb1a..0000000000 --- a/assets/icons/radix/rocket.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/rotate-counter-clockwise.svg b/assets/icons/radix/rotate-counter-clockwise.svg deleted file mode 100644 index c43c90b90b..0000000000 --- a/assets/icons/radix/rotate-counter-clockwise.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/row-spacing.svg b/assets/icons/radix/row-spacing.svg deleted file mode 100644 index e155bd5947..0000000000 --- a/assets/icons/radix/row-spacing.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/rows.svg b/assets/icons/radix/rows.svg deleted file mode 100644 index fb4ca0f9e3..0000000000 --- a/assets/icons/radix/rows.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/ruler-horizontal.svg b/assets/icons/radix/ruler-horizontal.svg deleted file mode 100644 index db6f1ef488..0000000000 --- a/assets/icons/radix/ruler-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/ruler-square.svg b/assets/icons/radix/ruler-square.svg deleted file mode 100644 index 7de70cc5dc..0000000000 --- a/assets/icons/radix/ruler-square.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/scissors.svg b/assets/icons/radix/scissors.svg deleted file mode 100644 index 2893b34712..0000000000 --- a/assets/icons/radix/scissors.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/section.svg b/assets/icons/radix/section.svg deleted file mode 100644 index 1e939e2b2f..0000000000 --- a/assets/icons/radix/section.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sewing-pin-filled.svg b/assets/icons/radix/sewing-pin-filled.svg deleted file mode 100644 index 97f6f1120d..0000000000 --- a/assets/icons/radix/sewing-pin-filled.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sewing-pin-solid.svg b/assets/icons/radix/sewing-pin-solid.svg deleted file mode 100644 index 97f6f1120d..0000000000 --- a/assets/icons/radix/sewing-pin-solid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sewing-pin.svg b/assets/icons/radix/sewing-pin.svg deleted file mode 100644 index 068dfd7bdf..0000000000 --- a/assets/icons/radix/sewing-pin.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/shadow-inner.svg b/assets/icons/radix/shadow-inner.svg deleted file mode 100644 index 4d073bf35f..0000000000 --- a/assets/icons/radix/shadow-inner.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/shadow-none.svg b/assets/icons/radix/shadow-none.svg deleted file mode 100644 index b02d3466ad..0000000000 --- a/assets/icons/radix/shadow-none.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/shadow-outer.svg b/assets/icons/radix/shadow-outer.svg deleted file mode 100644 index dc7ea84087..0000000000 --- a/assets/icons/radix/shadow-outer.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - diff --git a/assets/icons/radix/shadow.svg b/assets/icons/radix/shadow.svg deleted file mode 100644 index c991af6156..0000000000 --- a/assets/icons/radix/shadow.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - diff --git a/assets/icons/radix/share-1.svg b/assets/icons/radix/share-1.svg deleted file mode 100644 index 58328e4d1e..0000000000 --- a/assets/icons/radix/share-1.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/share-2.svg b/assets/icons/radix/share-2.svg deleted file mode 100644 index 1302ea5fbe..0000000000 --- a/assets/icons/radix/share-2.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/shuffle.svg b/assets/icons/radix/shuffle.svg deleted file mode 100644 index 8670e1a048..0000000000 --- a/assets/icons/radix/shuffle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/size.svg b/assets/icons/radix/size.svg deleted file mode 100644 index dece8c5182..0000000000 --- a/assets/icons/radix/size.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sketch-logo.svg b/assets/icons/radix/sketch-logo.svg deleted file mode 100644 index 6c54c4c825..0000000000 --- a/assets/icons/radix/sketch-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/slash.svg b/assets/icons/radix/slash.svg deleted file mode 100644 index aa7dac30c1..0000000000 --- a/assets/icons/radix/slash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/slider.svg b/assets/icons/radix/slider.svg deleted file mode 100644 index 66e0452bc0..0000000000 --- a/assets/icons/radix/slider.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-between-horizontally.svg b/assets/icons/radix/space-between-horizontally.svg deleted file mode 100644 index a71638d52b..0000000000 --- a/assets/icons/radix/space-between-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-between-vertically.svg b/assets/icons/radix/space-between-vertically.svg deleted file mode 100644 index bae247222f..0000000000 --- a/assets/icons/radix/space-between-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-evenly-horizontally.svg b/assets/icons/radix/space-evenly-horizontally.svg deleted file mode 100644 index 70169492e4..0000000000 --- a/assets/icons/radix/space-evenly-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/space-evenly-vertically.svg b/assets/icons/radix/space-evenly-vertically.svg deleted file mode 100644 index 469b4c05d4..0000000000 --- a/assets/icons/radix/space-evenly-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/speaker-moderate.svg b/assets/icons/radix/speaker-moderate.svg deleted file mode 100644 index 0f1d1b4210..0000000000 --- a/assets/icons/radix/speaker-moderate.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/speaker-quiet.svg b/assets/icons/radix/speaker-quiet.svg deleted file mode 100644 index eb68cefcee..0000000000 --- a/assets/icons/radix/speaker-quiet.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/square.svg b/assets/icons/radix/square.svg deleted file mode 100644 index 82843f51c3..0000000000 --- a/assets/icons/radix/square.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stack.svg b/assets/icons/radix/stack.svg deleted file mode 100644 index 92426ffb0d..0000000000 --- a/assets/icons/radix/stack.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/star-filled.svg b/assets/icons/radix/star-filled.svg deleted file mode 100644 index 2b17b7f579..0000000000 --- a/assets/icons/radix/star-filled.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/assets/icons/radix/star.svg b/assets/icons/radix/star.svg deleted file mode 100644 index 23f09ad7b2..0000000000 --- a/assets/icons/radix/star.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stitches-logo.svg b/assets/icons/radix/stitches-logo.svg deleted file mode 100644 index 319a1481f3..0000000000 --- a/assets/icons/radix/stitches-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stop.svg b/assets/icons/radix/stop.svg deleted file mode 100644 index 57aac59cab..0000000000 --- a/assets/icons/radix/stop.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stopwatch.svg b/assets/icons/radix/stopwatch.svg deleted file mode 100644 index ce5661e5cc..0000000000 --- a/assets/icons/radix/stopwatch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stretch-horizontally.svg b/assets/icons/radix/stretch-horizontally.svg deleted file mode 100644 index 37977363b3..0000000000 --- a/assets/icons/radix/stretch-horizontally.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/stretch-vertically.svg b/assets/icons/radix/stretch-vertically.svg deleted file mode 100644 index c4b1fe79ce..0000000000 --- a/assets/icons/radix/stretch-vertically.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/strikethrough.svg b/assets/icons/radix/strikethrough.svg deleted file mode 100644 index b814ef420a..0000000000 --- a/assets/icons/radix/strikethrough.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/sun.svg b/assets/icons/radix/sun.svg deleted file mode 100644 index 1807a51b4c..0000000000 --- a/assets/icons/radix/sun.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/switch.svg b/assets/icons/radix/switch.svg deleted file mode 100644 index 6dea528ce9..0000000000 --- a/assets/icons/radix/switch.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/symbol.svg b/assets/icons/radix/symbol.svg deleted file mode 100644 index b529b2b08b..0000000000 --- a/assets/icons/radix/symbol.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/table.svg b/assets/icons/radix/table.svg deleted file mode 100644 index 8ff059b847..0000000000 --- a/assets/icons/radix/table.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/target.svg b/assets/icons/radix/target.svg deleted file mode 100644 index d67989e01f..0000000000 --- a/assets/icons/radix/target.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-bottom.svg b/assets/icons/radix/text-align-bottom.svg deleted file mode 100644 index 862a5aeb88..0000000000 --- a/assets/icons/radix/text-align-bottom.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-center.svg b/assets/icons/radix/text-align-center.svg deleted file mode 100644 index 673cf8cd0a..0000000000 --- a/assets/icons/radix/text-align-center.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-justify.svg b/assets/icons/radix/text-align-justify.svg deleted file mode 100644 index df877f9513..0000000000 --- a/assets/icons/radix/text-align-justify.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-left.svg b/assets/icons/radix/text-align-left.svg deleted file mode 100644 index b7a64fbd43..0000000000 --- a/assets/icons/radix/text-align-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-middle.svg b/assets/icons/radix/text-align-middle.svg deleted file mode 100644 index e739d04efa..0000000000 --- a/assets/icons/radix/text-align-middle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-right.svg b/assets/icons/radix/text-align-right.svg deleted file mode 100644 index e7609908ff..0000000000 --- a/assets/icons/radix/text-align-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-align-top.svg b/assets/icons/radix/text-align-top.svg deleted file mode 100644 index 21660fe7d3..0000000000 --- a/assets/icons/radix/text-align-top.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text-none.svg b/assets/icons/radix/text-none.svg deleted file mode 100644 index 2a87f9372a..0000000000 --- a/assets/icons/radix/text-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/text.svg b/assets/icons/radix/text.svg deleted file mode 100644 index bd41d8ac19..0000000000 --- a/assets/icons/radix/text.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-down.svg b/assets/icons/radix/thick-arrow-down.svg deleted file mode 100644 index 32923bec58..0000000000 --- a/assets/icons/radix/thick-arrow-down.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-left.svg b/assets/icons/radix/thick-arrow-left.svg deleted file mode 100644 index 0cfd863903..0000000000 --- a/assets/icons/radix/thick-arrow-left.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-right.svg b/assets/icons/radix/thick-arrow-right.svg deleted file mode 100644 index a0cb605693..0000000000 --- a/assets/icons/radix/thick-arrow-right.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/thick-arrow-up.svg b/assets/icons/radix/thick-arrow-up.svg deleted file mode 100644 index 68687be28d..0000000000 --- a/assets/icons/radix/thick-arrow-up.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/timer.svg b/assets/icons/radix/timer.svg deleted file mode 100644 index 20c52dff95..0000000000 --- a/assets/icons/radix/timer.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/tokens.svg b/assets/icons/radix/tokens.svg deleted file mode 100644 index 2bbbc82030..0000000000 --- a/assets/icons/radix/tokens.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/track-next.svg b/assets/icons/radix/track-next.svg deleted file mode 100644 index 24fd40e36f..0000000000 --- a/assets/icons/radix/track-next.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/track-previous.svg b/assets/icons/radix/track-previous.svg deleted file mode 100644 index d99e7ab53f..0000000000 --- a/assets/icons/radix/track-previous.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/transform.svg b/assets/icons/radix/transform.svg deleted file mode 100644 index e913ccc9a7..0000000000 --- a/assets/icons/radix/transform.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/transparency-grid.svg b/assets/icons/radix/transparency-grid.svg deleted file mode 100644 index 6559ef8c2b..0000000000 --- a/assets/icons/radix/transparency-grid.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/assets/icons/radix/trash.svg b/assets/icons/radix/trash.svg deleted file mode 100644 index 18780e492c..0000000000 --- a/assets/icons/radix/trash.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-down.svg b/assets/icons/radix/triangle-down.svg deleted file mode 100644 index ebfd8f2a12..0000000000 --- a/assets/icons/radix/triangle-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-left.svg b/assets/icons/radix/triangle-left.svg deleted file mode 100644 index 0014139716..0000000000 --- a/assets/icons/radix/triangle-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-right.svg b/assets/icons/radix/triangle-right.svg deleted file mode 100644 index aed1393b9c..0000000000 --- a/assets/icons/radix/triangle-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/triangle-up.svg b/assets/icons/radix/triangle-up.svg deleted file mode 100644 index 5eb1b416d3..0000000000 --- a/assets/icons/radix/triangle-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/radix/twitter-logo.svg b/assets/icons/radix/twitter-logo.svg deleted file mode 100644 index 7dcf2f58eb..0000000000 --- a/assets/icons/radix/twitter-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/underline.svg b/assets/icons/radix/underline.svg deleted file mode 100644 index 3344685097..0000000000 --- a/assets/icons/radix/underline.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/update.svg b/assets/icons/radix/update.svg deleted file mode 100644 index b529b2b08b..0000000000 --- a/assets/icons/radix/update.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/upload.svg b/assets/icons/radix/upload.svg deleted file mode 100644 index a7f6bddb2e..0000000000 --- a/assets/icons/radix/upload.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/value-none.svg b/assets/icons/radix/value-none.svg deleted file mode 100644 index a86c08be1a..0000000000 --- a/assets/icons/radix/value-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/value.svg b/assets/icons/radix/value.svg deleted file mode 100644 index 59dd7d9373..0000000000 --- a/assets/icons/radix/value.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/vercel-logo.svg b/assets/icons/radix/vercel-logo.svg deleted file mode 100644 index 5466fd9f0e..0000000000 --- a/assets/icons/radix/vercel-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/video.svg b/assets/icons/radix/video.svg deleted file mode 100644 index e405396bef..0000000000 --- a/assets/icons/radix/video.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-grid.svg b/assets/icons/radix/view-grid.svg deleted file mode 100644 index 04825a870b..0000000000 --- a/assets/icons/radix/view-grid.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-horizontal.svg b/assets/icons/radix/view-horizontal.svg deleted file mode 100644 index 2ca7336b99..0000000000 --- a/assets/icons/radix/view-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-none.svg b/assets/icons/radix/view-none.svg deleted file mode 100644 index 71b08a46d2..0000000000 --- a/assets/icons/radix/view-none.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/view-vertical.svg b/assets/icons/radix/view-vertical.svg deleted file mode 100644 index 0c8f8164b4..0000000000 --- a/assets/icons/radix/view-vertical.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/width.svg b/assets/icons/radix/width.svg deleted file mode 100644 index 3ae2b56e3d..0000000000 --- a/assets/icons/radix/width.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/zoom-in.svg b/assets/icons/radix/zoom-in.svg deleted file mode 100644 index caac722ad0..0000000000 --- a/assets/icons/radix/zoom-in.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/radix/zoom-out.svg b/assets/icons/radix/zoom-out.svg deleted file mode 100644 index 62046a9e0f..0000000000 --- a/assets/icons/radix/zoom-out.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/assets/icons/robot_14.svg b/assets/icons/robot_14.svg deleted file mode 100644 index 7b6dc3f752..0000000000 --- a/assets/icons/robot_14.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg deleted file mode 100644 index 49e097b023..0000000000 --- a/assets/icons/screen.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/select-all.svg b/assets/icons/select-all.svg new file mode 100644 index 0000000000..45a10bba42 --- /dev/null +++ b/assets/icons/select-all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/radix/speaker-loud.svg b/assets/icons/speaker-loud.svg similarity index 100% rename from assets/icons/radix/speaker-loud.svg rename to assets/icons/speaker-loud.svg diff --git a/assets/icons/radix/speaker-off.svg b/assets/icons/speaker-off.svg similarity index 100% rename from assets/icons/radix/speaker-off.svg rename to assets/icons/speaker-off.svg diff --git a/assets/icons/speech_bubble_12.svg b/assets/icons/speech_bubble_12.svg deleted file mode 100644 index 736f39a984..0000000000 --- a/assets/icons/speech_bubble_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/split_12.svg b/assets/icons/split_12.svg deleted file mode 100644 index e4cf1921fa..0000000000 --- a/assets/icons/split_12.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/assets/icons/split_message_15.svg b/assets/icons/split_message.svg similarity index 100% rename from assets/icons/split_message_15.svg rename to assets/icons/split_message.svg diff --git a/assets/icons/stop_sharing.svg b/assets/icons/stop_sharing.svg deleted file mode 100644 index e9aa7eac5a..0000000000 --- a/assets/icons/stop_sharing.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/success.svg b/assets/icons/success.svg deleted file mode 100644 index 85450cdc43..0000000000 --- a/assets/icons/success.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/terminal_12.svg b/assets/icons/terminal_12.svg deleted file mode 100644 index 9d5a9447b5..0000000000 --- a/assets/icons/terminal_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/terminal_16.svg b/assets/icons/terminal_16.svg deleted file mode 100644 index 95da7ff4e1..0000000000 --- a/assets/icons/terminal_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/terminal_8.svg b/assets/icons/terminal_8.svg deleted file mode 100644 index b09495dcf9..0000000000 --- a/assets/icons/terminal_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/triangle_exclamation_12.svg b/assets/icons/triangle_exclamation_12.svg deleted file mode 100644 index f87d365bdf..0000000000 --- a/assets/icons/triangle_exclamation_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/triangle_exclamation_16.svg b/assets/icons/triangle_exclamation_16.svg deleted file mode 100644 index 2df386203a..0000000000 --- a/assets/icons/triangle_exclamation_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/triangle_exclamation_8.svg b/assets/icons/triangle_exclamation_8.svg deleted file mode 100644 index 96f11015b1..0000000000 --- a/assets/icons/triangle_exclamation_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/unlock_8.svg b/assets/icons/unlock_8.svg deleted file mode 100644 index 7a40f94345..0000000000 --- a/assets/icons/unlock_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_circle_12.svg b/assets/icons/user_circle_12.svg deleted file mode 100644 index 8631c36fd6..0000000000 --- a/assets/icons/user_circle_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_circle_8.svg b/assets/icons/user_circle_8.svg deleted file mode 100644 index 304001d546..0000000000 --- a/assets/icons/user_circle_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_group_12.svg b/assets/icons/user_group_12.svg deleted file mode 100644 index 5eae1d55b7..0000000000 --- a/assets/icons/user_group_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/user_group_16.svg b/assets/icons/user_group_16.svg deleted file mode 100644 index aa99277646..0000000000 --- a/assets/icons/user_group_16.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/user_group_8.svg b/assets/icons/user_group_8.svg deleted file mode 100644 index 69d08b0d3b..0000000000 --- a/assets/icons/user_group_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/user_plus_12.svg b/assets/icons/user_plus_12.svg deleted file mode 100644 index 535d04af45..0000000000 --- a/assets/icons/user_plus_12.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/user_plus_16.svg b/assets/icons/user_plus_16.svg deleted file mode 100644 index 150392f6e0..0000000000 --- a/assets/icons/user_plus_16.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/user_plus_8.svg b/assets/icons/user_plus_8.svg deleted file mode 100644 index 100b43af86..0000000000 --- a/assets/icons/user_plus_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/version_control_branch_12.svg b/assets/icons/version_control_branch_12.svg deleted file mode 100644 index 3571874a89..0000000000 --- a/assets/icons/version_control_branch_12.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/word_search_14.svg b/assets/icons/word_search.svg similarity index 100% rename from assets/icons/word_search_14.svg rename to assets/icons/word_search.svg diff --git a/assets/icons/word_search_12.svg b/assets/icons/word_search_12.svg deleted file mode 100644 index 4cf6401fd2..0000000000 --- a/assets/icons/word_search_12.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/assets/icons/x_mark_12.svg b/assets/icons/x_mark_12.svg deleted file mode 100644 index 1c95f979d0..0000000000 --- a/assets/icons/x_mark_12.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/x_mark_16.svg b/assets/icons/x_mark_16.svg deleted file mode 100644 index 21a7f1c210..0000000000 --- a/assets/icons/x_mark_16.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/x_mark_8.svg b/assets/icons/x_mark_8.svg deleted file mode 100644 index f724b1515e..0000000000 --- a/assets/icons/x_mark_8.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/zed_plus_copilot_32.svg b/assets/icons/zed_x_copilot.svg similarity index 100% rename from assets/icons/zed_plus_copilot_32.svg rename to assets/icons/zed_x_copilot.svg diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index fa62a74f3f..2211f9563d 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -231,7 +231,14 @@ } }, { - "context": "BufferSearchBar > Editor", + "context": "BufferSearchBar && in_replace", + "bindings": { + "enter": "search::ReplaceNext", + "cmd-enter": "search::ReplaceAll" + } + }, + { + "context": "BufferSearchBar && !in_replace > Editor", "bindings": { "up": "search::PreviousHistoryQuery", "down": "search::NextHistoryQuery" @@ -533,7 +540,7 @@ // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", "cmd-alt-i": "zed::DebugElements", - "ctrl-:": "editor::ToggleInlayHints", + "ctrl-:": "editor::ToggleInlayHints" } }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index b47907783e..44dbf2533f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -32,6 +32,8 @@ "right": "vim::Right", "$": "vim::EndOfLine", "^": "vim::FirstNonWhitespace", + "_": "vim::StartOfLineDownward", + "g _": "vim::EndOfLineDownward", "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", "{": "vim::StartOfParagraph", @@ -123,6 +125,8 @@ "g shift-t": "pane::ActivatePrevItem", "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToTypeDefinition", + "g s": "outline::Toggle", + "g shift-s": "project_symbols::Toggle", "g .": "editor::ToggleCodeActions", // zed specific "g shift-a": "editor::FindAllReferences", // zed specific "g *": [ @@ -326,7 +330,7 @@ } }, { - "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting", + "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { ".": "vim::Repeat", "c": [ @@ -389,7 +393,7 @@ } }, { - "context": "Editor && vim_operator == n", + "context": "Editor && VimCount", "bindings": { "0": [ "vim::Number", @@ -448,6 +452,8 @@ "shift-o": "vim::OtherEnd", "d": "vim::VisualDelete", "x": "vim::VisualDelete", + "shift-d": "vim::VisualDelete", + "shift-x": "vim::VisualDelete", "y": "vim::VisualYank", "p": "vim::Paste", "shift-p": [ @@ -497,7 +503,7 @@ "around": true } } - ], + ] } }, { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 6d1db5ada5..f9b34add9a 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -16,8 +16,8 @@ use workspace::{item::ItemHandle, StatusItemView, Workspace}; actions!(lsp_status, [ShowErrorMessage]); -const DOWNLOAD_ICON: &str = "icons/download_12.svg"; -const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg"; +const DOWNLOAD_ICON: &str = "icons/download.svg"; +const WARNING_ICON: &str = "icons/warning.svg"; pub enum Event { ShowError { lsp_name: Arc, error: String }, diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 4438f88108..d96e470d5c 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -27,6 +27,7 @@ futures.workspace = true indoc.workspace = true isahc.workspace = true ordered-float.workspace = true +parking_lot.workspace = true regex.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 2c2d7e774e..7d9b93b0a7 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,5 +1,6 @@ pub mod assistant; mod assistant_settings; +mod codegen; mod streaming_diff; use anyhow::{anyhow, Result}; @@ -26,7 +27,7 @@ use util::paths::CONVERSATIONS_DIR; const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; // Data types for chat completion requests -#[derive(Debug, Serialize)] +#[derive(Debug, Default, Serialize)] pub struct OpenAIRequest { model: String, messages: Vec, diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 9b384252fc..7fa66f26fb 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,9 +1,8 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - stream_completion, - streaming_diff::{Hunk, StreamingDiff}, - MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, - SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, + codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider}, + stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, + Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; @@ -13,10 +12,10 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, }; use fs::Fs; -use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use futures::StreamExt; use gpui::{ actions, elements::{ @@ -30,17 +29,14 @@ use gpui::{ ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::{ - language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _, - TransactionId, -}; +use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use search::BufferSearchBar; use settings::SettingsStore; use std::{ cell::{Cell, RefCell}, cmp, env, fmt::Write, - future, iter, + iter, ops::Range, path::{Path, PathBuf}, rc::Rc, @@ -266,23 +262,40 @@ impl AssistantPanel { } fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { + api_key + } else { + return; + }; + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let provider = Arc::new(OpenAICompletionProvider::new( + api_key, + cx.background().clone(), + )); let selection = editor.read(cx).selections.newest_anchor().clone(); - let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); - let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { - InlineAssistKind::Generate + let codegen_kind = if editor.read(cx).selections.newest::(cx).is_empty() { + CodegenKind::Generate { + position: selection.start, + } } else { - InlineAssistKind::Transform + CodegenKind::Transform { + range: selection.start..selection.end, + } }; + let codegen = cx.add_model(|cx| { + Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) + }); + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); let inline_assistant = cx.add_view(|cx| { let assistant = InlineAssistant::new( inline_assist_id, - assist_kind, measurements.clone(), self.include_conversation_in_next_inline_assist, self.inline_prompt_history.clone(), + codegen.clone(), cx, ); cx.focus_self(); @@ -321,48 +334,66 @@ impl AssistantPanel { self.pending_inline_assists.insert( inline_assist_id, PendingInlineAssist { - kind: assist_kind, editor: editor.downgrade(), - range, - highlighted_ranges: Default::default(), inline_assistant: Some((block_id, inline_assistant.clone())), - code_generation: Task::ready(None), - transaction_id: None, + codegen: codegen.clone(), _subscriptions: vec![ cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), cx.subscribe(editor, { let inline_assistant = inline_assistant.downgrade(); - move |this, editor, event, cx| { + move |_, editor, event, cx| { if let Some(inline_assistant) = inline_assistant.upgrade(cx) { - match event { - editor::Event::SelectionsChanged { local } => { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); - } + if let editor::Event::SelectionsChanged { local } = event { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); } - editor::Event::TransactionUndone { - transaction_id: tx_id, - } => { - if let Some(pending_assist) = - this.pending_inline_assists.get(&inline_assist_id) - { - if pending_assist.transaction_id == Some(*tx_id) { - // Notice we are supplying `undo: false` here. This - // is because there's no need to undo the transaction - // because the user just did so. - this.close_inline_assist( - inline_assist_id, - false, - cx, - ); - } - } - } - _ => {} } } } }), + cx.observe(&codegen, { + let editor = editor.downgrade(); + move |this, _, cx| { + if let Some(editor) = editor.upgrade(cx) { + this.update_highlights_for_editor(&editor, cx); + } + } + }), + cx.subscribe(&codegen, move |this, codegen, event, cx| match event { + codegen::Event::Undone => { + this.finish_inline_assist(inline_assist_id, false, cx) + } + codegen::Event::Finished => { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + let error = codegen + .read(cx) + .error() + .map(|error| format!("Inline assistant error: {}", error)); + if let Some(error) = error { + if pending_assist.inline_assistant.is_none() { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new(inline_assist_id, error), + cx, + ); + }) + } + + this.finish_inline_assist(inline_assist_id, false, cx); + } + } else { + this.finish_inline_assist(inline_assist_id, false, cx); + } + } + }), ], }, ); @@ -388,7 +419,7 @@ impl AssistantPanel { self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); } InlineAssistantEvent::Canceled => { - self.close_inline_assist(assist_id, true, cx); + self.finish_inline_assist(assist_id, true, cx); } InlineAssistantEvent::Dismissed => { self.hide_inline_assist(assist_id, cx); @@ -417,7 +448,7 @@ impl AssistantPanel { .get(&editor.downgrade()) .and_then(|assist_ids| assist_ids.last().copied()) { - panel.close_inline_assist(assist_id, true, cx); + panel.finish_inline_assist(assist_id, true, cx); true } else { false @@ -432,7 +463,7 @@ impl AssistantPanel { cx.propagate_action(); } - fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { + fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { self.hide_inline_assist(assist_id, cx); if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { @@ -450,13 +481,9 @@ impl AssistantPanel { self.update_highlights_for_editor(&editor, cx); if undo { - if let Some(transaction_id) = pending_assist.transaction_id { - editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - buffer.undo_transaction(transaction_id, cx) - }); - }); - } + pending_assist + .codegen + .update(cx, |codegen, cx| codegen.undo(cx)); } } } @@ -481,12 +508,6 @@ impl AssistantPanel { include_conversation: bool, cx: &mut ViewContext, ) { - let api_key = if let Some(api_key) = self.api_key.borrow().clone() { - api_key - } else { - return; - }; - let conversation = if include_conversation { self.active_editor() .map(|editor| editor.read(cx).conversation.clone()) @@ -514,56 +535,9 @@ impl AssistantPanel { self.inline_prompt_history.pop_front(); } - let range = pending_assist.range.clone(); let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let selected_text = snapshot - .text_for_range(range.start..range.end) - .collect::(); - - let selection_start = range.start.to_point(&snapshot); - let selection_end = range.end.to_point(&snapshot); - - let mut base_indent: Option = None; - let mut start_row = selection_start.row; - if snapshot.is_line_blank(start_row) { - if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { - start_row = prev_non_blank_row; - } - } - for row in start_row..=selection_end.row { - if snapshot.is_line_blank(row) { - continue; - } - - let line_indent = snapshot.indent_size_for_line(row); - if let Some(base_indent) = base_indent.as_mut() { - if line_indent.len < base_indent.len { - *base_indent = line_indent; - } - } else { - base_indent = Some(line_indent); - } - } - - let mut normalized_selected_text = selected_text.clone(); - if let Some(base_indent) = base_indent { - for row in selection_start.row..=selection_end.row { - let selection_row = row - selection_start.row; - let line_start = - normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indent_len = if row == selection_start.row { - base_indent.len.saturating_sub(selection_start.column) - } else { - let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indent.len) - }; - let indent_end = cmp::min( - line_start + indent_len as usize, - normalized_selected_text.len(), - ); - normalized_selected_text.replace(line_start..indent_end, ""); - } - } + let range = pending_assist.codegen.read(cx).range(); + let selected_text = snapshot.text_for_range(range.clone()).collect::(); let language = snapshot.language_at(range.start); let language_name = if let Some(language) = language.as_ref() { @@ -581,8 +555,8 @@ impl AssistantPanel { if let Some(language_name) = language_name { writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); } - match pending_assist.kind { - InlineAssistKind::Transform => { + match pending_assist.codegen.read(cx).kind() { + CodegenKind::Transform { .. } => { writeln!( prompt, "You're currently working inside an editor on this file:" @@ -608,7 +582,7 @@ impl AssistantPanel { } else { writeln!(prompt, "```").unwrap(); } - writeln!(prompt, "{normalized_selected_text}").unwrap(); + writeln!(prompt, "{selected_text}").unwrap(); writeln!(prompt, "```").unwrap(); writeln!(prompt).unwrap(); writeln!( @@ -622,7 +596,7 @@ impl AssistantPanel { ) .unwrap(); } - InlineAssistKind::Generate => { + CodegenKind::Generate { .. } => { writeln!( prompt, "You're currently working inside an editor on this file:" @@ -689,209 +663,9 @@ impl AssistantPanel { messages, stream: true, }; - let response = stream_completion(api_key, cx.background().clone(), request); - let editor = editor.downgrade(); - - pending_assist.code_generation = cx.spawn(|this, mut cx| { - async move { - let mut edit_start = range.start.to_offset(&snapshot); - - let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { - let chunks = strip_markdown_codeblock(response.await?.filter_map( - |message| async move { - match message { - Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)), - Err(error) => Some(Err(error)), - } - }, - )); - futures::pin_mut!(chunks); - let mut diff = StreamingDiff::new(selected_text.to_string()); - - let mut indent_len; - let indent_text; - if let Some(base_indent) = base_indent { - indent_len = base_indent.len; - indent_text = match base_indent.kind { - language::IndentKind::Space => " ", - language::IndentKind::Tab => "\t", - }; - } else { - indent_len = 0; - indent_text = ""; - }; - - let mut first_line_len = 0; - let mut first_line_non_whitespace_char_ix = None; - let mut first_line = true; - let mut new_text = String::new(); - - while let Some(chunk) = chunks.next().await { - let chunk = chunk?; - - let mut lines = chunk.split('\n'); - if let Some(mut line) = lines.next() { - if first_line { - if first_line_non_whitespace_char_ix.is_none() { - if let Some(mut char_ix) = - line.find(|ch: char| !ch.is_whitespace()) - { - line = &line[char_ix..]; - char_ix += first_line_len; - first_line_non_whitespace_char_ix = Some(char_ix); - let first_line_indent = char_ix - .saturating_sub(selection_start.column as usize) - as usize; - new_text.push_str(&indent_text.repeat(first_line_indent)); - indent_len = indent_len.saturating_sub(char_ix as u32); - } - } - first_line_len += line.len(); - } - - if first_line_non_whitespace_char_ix.is_some() { - new_text.push_str(line); - } - } - - for line in lines { - first_line = false; - new_text.push('\n'); - if !line.is_empty() { - new_text.push_str(&indent_text.repeat(indent_len as usize)); - } - new_text.push_str(line); - } - - let hunks = diff.push_new(&new_text); - hunks_tx.send(hunks).await?; - new_text.clear(); - } - hunks_tx.send(diff.finish()).await?; - - anyhow::Ok(()) - }); - - while let Some(hunks) = hunks_rx.next().await { - let editor = if let Some(editor) = editor.upgrade(&cx) { - editor - } else { - break; - }; - - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - break; - }; - - this.update(&mut cx, |this, cx| { - let pending_assist = if let Some(pending_assist) = - this.pending_inline_assists.get_mut(&inline_assist_id) - { - pending_assist - } else { - return; - }; - - pending_assist.highlighted_ranges.clear(); - editor.update(cx, |editor, cx| { - let transaction = editor.buffer().update(cx, |buffer, cx| { - // Avoid grouping assistant edits with user edits. - buffer.finalize_last_transaction(cx); - - buffer.start_transaction(cx); - buffer.edit( - hunks.into_iter().filter_map(|hunk| match hunk { - Hunk::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - Some((edit_start..edit_start, text)) - } - Hunk::Remove { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start = edit_end; - Some((edit_range, String::new())) - } - Hunk::Keep { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start += len; - pending_assist.highlighted_ranges.push(edit_range); - None - } - }), - None, - cx, - ); - - buffer.end_transaction(cx) - }); - - if let Some(transaction) = transaction { - if let Some(first_transaction) = pending_assist.transaction_id { - // Group all assistant edits into the first transaction. - editor.buffer().update(cx, |buffer, cx| { - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ) - }); - } else { - pending_assist.transaction_id = Some(transaction); - editor.buffer().update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx) - }); - } - } - }); - - this.update_highlights_for_editor(&editor, cx); - }); - } - - if let Err(error) = diff.await { - this.update(&mut cx, |this, cx| { - let pending_assist = if let Some(pending_assist) = - this.pending_inline_assists.get_mut(&inline_assist_id) - { - pending_assist - } else { - return; - }; - - if let Some((_, inline_assistant)) = - pending_assist.inline_assistant.as_ref() - { - inline_assistant.update(cx, |inline_assistant, cx| { - inline_assistant.set_error(error, cx); - }); - } else if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - inline_assist_id, - format!("Inline assistant error: {}", error), - ), - cx, - ); - }) - } - })?; - } else { - let _ = this.update(&mut cx, |this, cx| { - this.close_inline_assist(inline_assist_id, false, cx) - }); - } - - anyhow::Ok(()) - } - .log_err() - }); + pending_assist + .codegen + .update(cx, |codegen, cx| codegen.start(request, cx)); } fn update_highlights_for_editor( @@ -909,8 +683,9 @@ impl AssistantPanel { for inline_assist_id in inline_assist_ids { if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { - background_ranges.push(pending_assist.range.clone()); - foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned()); + let codegen = pending_assist.codegen.read(cx); + background_ranges.push(codegen.range()); + foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); } } @@ -929,7 +704,7 @@ impl AssistantPanel { } if foreground_ranges.is_empty() { - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } else { editor.highlight_text::( foreground_ranges, @@ -2601,7 +2376,7 @@ impl ConversationEditor { .with_children( if let MessageStatus::Error(error) = &message.status { Some( - Svg::new("icons/circle_x_mark_12.svg") + Svg::new("icons/error.svg") .with_color(style.error_icon.color) .constrained() .with_width(style.error_icon.width) @@ -2887,12 +2662,6 @@ enum InlineAssistantEvent { }, } -#[derive(Copy, Clone)] -enum InlineAssistKind { - Transform, - Generate, -} - struct InlineAssistant { id: usize, prompt_editor: ViewHandle, @@ -2900,11 +2669,11 @@ struct InlineAssistant { has_focus: bool, include_conversation: bool, measurements: Rc>, - error: Option, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, - _subscription: Subscription, + codegen: ModelHandle, + _subscriptions: Vec, } impl Entity for InlineAssistant { @@ -2933,9 +2702,9 @@ impl View for InlineAssistant { .element() .aligned(), ) - .with_children(if let Some(error) = self.error.as_ref() { + .with_children(if let Some(error) = self.codegen.read(cx).error() { Some( - Svg::new("icons/circle_x_mark_12.svg") + Svg::new("icons/error.svg") .with_color(theme.assistant.error_icon.color) .constrained() .with_width(theme.assistant.error_icon.width) @@ -3007,10 +2776,10 @@ impl View for InlineAssistant { impl InlineAssistant { fn new( id: usize, - kind: InlineAssistKind, measurements: Rc>, include_conversation: bool, prompt_history: VecDeque, + codegen: ModelHandle, cx: &mut ViewContext, ) -> Self { let prompt_editor = cx.add_view(|cx| { @@ -3018,14 +2787,17 @@ impl InlineAssistant { Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), cx, ); - let placeholder = match kind { - InlineAssistKind::Transform => "Enter transformation prompt…", - InlineAssistKind::Generate => "Enter generation prompt…", + let placeholder = match codegen.read(cx).kind() { + CodegenKind::Transform { .. } => "Enter transformation prompt…", + CodegenKind::Generate { .. } => "Enter generation prompt…", }; editor.set_placeholder_text(placeholder, cx); editor }); - let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events); + let subscriptions = vec![ + cx.observe(&codegen, Self::handle_codegen_changed), + cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), + ]; Self { id, prompt_editor, @@ -3033,11 +2805,11 @@ impl InlineAssistant { has_focus: false, include_conversation, measurements, - error: None, prompt_history, prompt_history_ix: None, pending_prompt: String::new(), - _subscription: subscription, + codegen, + _subscriptions: subscriptions, } } @@ -3053,6 +2825,32 @@ impl InlineAssistant { } } + fn handle_codegen_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + let is_read_only = !self.codegen.read(cx).idle(); + self.prompt_editor.update(cx, |editor, cx| { + let was_read_only = editor.read_only(); + if was_read_only != is_read_only { + if is_read_only { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + } else { + self.confirmed = false; + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + } + } + }); + cx.notify(); + } + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { cx.emit(InlineAssistantEvent::Canceled); } @@ -3076,7 +2874,6 @@ impl InlineAssistant { include_conversation: self.include_conversation, }); self.confirmed = true; - self.error = None; cx.notify(); } } @@ -3093,19 +2890,6 @@ impl InlineAssistant { cx.notify(); } - fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext) { - self.error = Some(error); - self.confirmed = false; - self.prompt_editor.update(cx, |editor, cx| { - editor.set_read_only(false); - editor.set_field_editor_style( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); - }); - cx.notify(); - } - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { if let Some(ix) = self.prompt_history_ix { if ix > 0 { @@ -3152,13 +2936,9 @@ struct BlockMeasurements { } struct PendingInlineAssist { - kind: InlineAssistKind, editor: WeakViewHandle, - range: Range, - highlighted_ranges: Vec>, inline_assistant: Option<(BlockId, ViewHandle)>, - code_generation: Task>, - transaction_id: Option, + codegen: ModelHandle, _subscriptions: Vec, } @@ -3184,65 +2964,10 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { } } -fn strip_markdown_codeblock( - stream: impl Stream>, -) -> impl Stream> { - let mut first_line = true; - let mut buffer = String::new(); - let mut starts_with_fenced_code_block = false; - stream.filter_map(move |chunk| { - let chunk = match chunk { - Ok(chunk) => chunk, - Err(err) => return future::ready(Some(Err(err))), - }; - buffer.push_str(&chunk); - - if first_line { - if buffer == "" || buffer == "`" || buffer == "``" { - return future::ready(None); - } else if buffer.starts_with("```") { - starts_with_fenced_code_block = true; - if let Some(newline_ix) = buffer.find('\n') { - buffer.replace_range(..newline_ix + 1, ""); - first_line = false; - } else { - return future::ready(None); - } - } - } - - let text = if starts_with_fenced_code_block { - buffer - .strip_suffix("\n```\n") - .or_else(|| buffer.strip_suffix("\n```")) - .or_else(|| buffer.strip_suffix("\n``")) - .or_else(|| buffer.strip_suffix("\n`")) - .or_else(|| buffer.strip_suffix('\n')) - .unwrap_or(&buffer) - } else { - &buffer - }; - - if text.contains('\n') { - first_line = false; - } - - let remainder = buffer.split_off(text.len()); - let result = if buffer.is_empty() { - None - } else { - Some(Ok(buffer.clone())) - }; - buffer = remainder; - future::ready(result) - }) -} - #[cfg(test)] mod tests { use super::*; use crate::MessageId; - use futures::stream; use gpui::AppContext; #[gpui::test] @@ -3611,62 +3336,6 @@ mod tests { ); } - #[gpui::test] - async fn test_strip_markdown_codeblock() { - assert_eq!( - strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "```js\nLorem ipsum dolor\n```" - ); - assert_eq!( - strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "``\nLorem ipsum dolor\n```" - ); - - fn chunks(text: &str, size: usize) -> impl Stream> { - stream::iter( - text.chars() - .collect::>() - .chunks(size) - .map(|chunk| Ok(chunk.iter().collect::())) - .collect::>(), - ) - } - } - fn messages( conversation: &ModelHandle, cx: &AppContext, diff --git a/crates/ai/src/codegen.rs b/crates/ai/src/codegen.rs new file mode 100644 index 0000000000..e7da46cdf9 --- /dev/null +++ b/crates/ai/src/codegen.rs @@ -0,0 +1,704 @@ +use crate::{ + stream_completion, + streaming_diff::{Hunk, StreamingDiff}, + OpenAIRequest, +}; +use anyhow::Result; +use editor::{ + multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; +use futures::{ + channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt, +}; +use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task}; +use language::{Rope, TransactionId}; +use std::{cmp, future, ops::Range, sync::Arc}; + +pub trait CompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>>; +} + +pub struct OpenAICompletionProvider { + api_key: String, + executor: Arc, +} + +impl OpenAICompletionProvider { + pub fn new(api_key: String, executor: Arc) -> Self { + Self { api_key, executor } + } +} + +impl CompletionProvider for OpenAICompletionProvider { + fn complete( + &self, + prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>> { + let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt); + async move { + let response = request.await?; + let stream = response + .filter_map(|response| async move { + match response { + Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }) + .boxed(); + Ok(stream) + } + .boxed() + } +} + +pub enum Event { + Finished, + Undone, +} + +#[derive(Clone)] +pub enum CodegenKind { + Transform { range: Range }, + Generate { position: Anchor }, +} + +pub struct Codegen { + provider: Arc, + buffer: ModelHandle, + snapshot: MultiBufferSnapshot, + kind: CodegenKind, + last_equal_ranges: Vec>, + transaction_id: Option, + error: Option, + generation: Task<()>, + idle: bool, + _subscription: gpui::Subscription, +} + +impl Entity for Codegen { + type Event = Event; +} + +impl Codegen { + pub fn new( + buffer: ModelHandle, + mut kind: CodegenKind, + provider: Arc, + cx: &mut ModelContext, + ) -> Self { + let snapshot = buffer.read(cx).snapshot(cx); + match &mut kind { + CodegenKind::Transform { range } => { + let mut point_range = range.to_point(&snapshot); + point_range.start.column = 0; + if point_range.end.column > 0 || point_range.start.row == point_range.end.row { + point_range.end.column = snapshot.line_len(point_range.end.row); + } + range.start = snapshot.anchor_before(point_range.start); + range.end = snapshot.anchor_after(point_range.end); + } + CodegenKind::Generate { position } => { + *position = position.bias_right(&snapshot); + } + } + + Self { + provider, + buffer: buffer.clone(), + snapshot, + kind, + last_equal_ranges: Default::default(), + transaction_id: Default::default(), + error: Default::default(), + idle: true, + generation: Task::ready(()), + _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), + } + } + + fn handle_buffer_event( + &mut self, + _buffer: ModelHandle, + event: &multi_buffer::Event, + cx: &mut ModelContext, + ) { + if let multi_buffer::Event::TransactionUndone { transaction_id } = event { + if self.transaction_id == Some(*transaction_id) { + self.transaction_id = None; + self.generation = Task::ready(()); + cx.emit(Event::Undone); + } + } + } + + pub fn range(&self) -> Range { + match &self.kind { + CodegenKind::Transform { range } => range.clone(), + CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position, + } + } + + pub fn kind(&self) -> &CodegenKind { + &self.kind + } + + pub fn last_equal_ranges(&self) -> &[Range] { + &self.last_equal_ranges + } + + pub fn idle(&self) -> bool { + self.idle + } + + pub fn error(&self) -> Option<&anyhow::Error> { + self.error.as_ref() + } + + pub fn start(&mut self, prompt: OpenAIRequest, cx: &mut ModelContext) { + let range = self.range(); + let snapshot = self.snapshot.clone(); + let selected_text = snapshot + .text_for_range(range.start..range.end) + .collect::(); + + let selection_start = range.start.to_point(&snapshot); + let suggested_line_indent = snapshot + .suggested_indents(selection_start.row..selection_start.row + 1, cx) + .into_values() + .next() + .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); + + let response = self.provider.complete(prompt); + self.generation = cx.spawn_weak(|this, mut cx| { + async move { + let generate = async { + let mut edit_start = range.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let chunks = strip_markdown_codeblock(response.await?); + futures::pin_mut!(chunks); + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let mut new_text = String::new(); + let mut base_indent = None; + let mut line_indent = None; + let mut first_line = true; + + while let Some(chunk) = chunks.next().await { + let chunk = chunk?; + + let mut lines = chunk.split('\n').peekable(); + while let Some(line) = lines.next() { + new_text.push_str(line); + if line_indent.is_none() { + if let Some(non_whitespace_ch_ix) = + new_text.find(|ch: char| !ch.is_whitespace()) + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); + + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); + } + + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); + } + } + + if line_indent.is_some() { + hunks_tx.send(diff.push_new(&new_text)).await?; + new_text.clear(); + } + + if lines.peek().is_some() { + hunks_tx.send(diff.push_new("\n")).await?; + line_indent = None; + first_line = false; + } + } + } + hunks_tx.send(diff.push_new(&new_text)).await?; + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; + + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + + let transaction = this.buffer.update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + this.last_equal_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = this.transaction_id { + // Group all assistant edits into the first transaction. + this.buffer.update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + this.transaction_id = Some(transaction); + this.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + + cx.notify(); + }); + } + + diff.await?; + anyhow::Ok(()) + }; + + let result = generate.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }); + } + } + }); + self.error.take(); + self.idle = false; + cx.notify(); + } + + pub fn undo(&mut self, cx: &mut ModelContext) { + if let Some(transaction_id) = self.transaction_id { + self.buffer + .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } + } +} + +fn strip_markdown_codeblock( + stream: impl Stream>, +) -> impl Stream> { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_fenced_code_block = false; + stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; + buffer.push_str(&chunk); + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return future::ready(None); + } else if buffer.starts_with("```") { + starts_with_fenced_code_block = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return future::ready(None); + } + } + } + + let text = if starts_with_fenced_code_block { + buffer + .strip_suffix("\n```\n") + .or_else(|| buffer.strip_suffix("\n```")) + .or_else(|| buffer.strip_suffix("\n``")) + .or_else(|| buffer.strip_suffix("\n`")) + .or_else(|| buffer.strip_suffix('\n')) + .unwrap_or(&buffer) + } else { + &buffer + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(Ok(buffer.clone())) + }; + buffer = remainder; + future::ready(result) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::stream; + use gpui::{executor::Deterministic, TestAppContext}; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use parking_lot::Mutex; + use rand::prelude::*; + use settings::SettingsStore; + + #[gpui::test(iterations = 10)] + async fn test_transform_autoindent( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + let x = 0; + for _ in 0..10 { + x += 1; + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let range = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Transform { range }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + " let mut x = 0;\n", + " while x < 10 {\n", + " x += 1;\n", + " }", + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_past_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + le + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 6)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + "t mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_before_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + deterministic: Arc, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = concat!( + "fn main() {\n", + " \n", + "}\n" // + ); + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 2)) + }); + let provider = Arc::new(TestCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx)); + + let mut new_text = concat!( + "let mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + deterministic.run_until_parked(); + } + provider.finish_completion(); + deterministic.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test] + async fn test_strip_markdown_codeblock() { + assert_eq!( + strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + + fn chunks(text: &str, size: usize) -> impl Stream> { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| Ok(chunk.iter().collect::())) + .collect::>(), + ) + } + } + + struct TestCompletionProvider { + last_completion_tx: Mutex>>, + } + + impl TestCompletionProvider { + fn new() -> Self { + Self { + last_completion_tx: Mutex::new(None), + } + } + + fn send_completion(&self, completion: impl Into) { + let mut tx = self.last_completion_tx.lock(); + tx.as_mut().unwrap().try_send(completion.into()).unwrap(); + } + + fn finish_completion(&self) { + self.last_completion_tx.lock().take().unwrap(); + } + } + + impl CompletionProvider for TestCompletionProvider { + fn complete( + &self, + _prompt: OpenAIRequest, + ) -> BoxFuture<'static, Result>>> { + let (tx, rx) = mpsc::channel(1); + *self.last_completion_tx.lock() = Some(tx); + async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed() + } + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + } +} diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 8397fa0745..e4a5c23534 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -50,7 +50,7 @@ impl View for UpdateNotification { .with_child( MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") + Svg::new("icons/x.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 4b29a08015..792c65b075 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.20.0" +version = "0.21.0" publish = false [[bin]] diff --git a/crates/collab/admin_api.conf b/crates/collab/admin_api.conf new file mode 100644 index 0000000000..5d3b0e65b7 --- /dev/null +++ b/crates/collab/admin_api.conf @@ -0,0 +1,4 @@ +db-uri = "postgres://postgres@localhost/zed" +server-port = 8081 +jwt-secret = "the-postgrest-jwt-secret-for-authorization" +log-level = "info" diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml index 79dd2b8851..d4a7a7033e 100644 --- a/crates/collab/k8s/manifest.template.yml +++ b/crates/collab/k8s/manifest.template.yml @@ -3,6 +3,7 @@ apiVersion: v1 kind: Namespace metadata: name: ${ZED_KUBE_NAMESPACE} + --- kind: Service apiVersion: v1 @@ -11,7 +12,7 @@ metadata: name: collab annotations: service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" - service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} spec: type: LoadBalancer selector: @@ -21,6 +22,26 @@ spec: protocol: TCP port: 443 targetPort: 8080 + +--- +kind: Service +apiVersion: v1 +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: pgadmin + annotations: + service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443" + service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID} +spec: + type: LoadBalancer + selector: + app: postgrest + ports: + - name: web + protocol: TCP + port: 443 + targetPort: 8080 + --- apiVersion: apps/v1 kind: Deployment @@ -117,3 +138,40 @@ spec: # FIXME - Switch to the more restrictive `PERFMON` capability. # This capability isn't yet available in a stable version of Debian. add: ["SYS_ADMIN"] + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: ${ZED_KUBE_NAMESPACE} + name: postgrest + +spec: + replicas: 1 + selector: + matchLabels: + app: postgrest + template: + metadata: + labels: + app: postgrest + spec: + containers: + - name: postgrest + image: "postgrest/postgrest" + ports: + - containerPort: 8080 + protocol: TCP + env: + - name: PGRST_SERVER_PORT + value: "8080" + - name: PGRST_DB_URI + valueFrom: + secretKeyRef: + name: database + key: url + - name: PGRST_JWT_SECRET + valueFrom: + secretKeyRef: + name: postgrest + key: jwt_secret diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 7191400f44..a84fcf328b 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -1,8 +1,7 @@ use crate::{ auth, - db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary}, - rpc::{self, ResultExt}, - AppState, Error, Result, + db::{User, UserId}, + rpc, AppState, Error, Result, }; use anyhow::anyhow; use axum::{ @@ -11,7 +10,7 @@ use axum::{ http::{self, Request, StatusCode}, middleware::{self, Next}, response::IntoResponse, - routing::{get, post, put}, + routing::{get, post}, Extension, Json, Router, }; use axum_extra::response::ErasedJson; @@ -23,18 +22,9 @@ use tracing::instrument; pub fn routes(rpc_server: Arc, state: Arc) -> Router { Router::new() .route("/user", get(get_authenticated_user)) - .route("/users", get(get_users).post(create_user)) - .route("/users/:id", put(update_user).delete(destroy_user)) .route("/users/:id/access_tokens", post(create_access_token)) - .route("/users_with_no_invites", get(get_users_with_no_invites)) - .route("/invite_codes/:code", get(get_user_for_invite_code)) .route("/panic", post(trace_panic)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) - .route("/signups", post(create_signup)) - .route("/signups_summary", get(get_waitlist_summary)) - .route("/user_invites", post(create_invite_from_code)) - .route("/unsent_invites", get(get_unsent_invites)) - .route("/sent_invites", post(record_sent_invites)) .layer( ServiceBuilder::new() .layer(Extension(state)) @@ -104,28 +94,6 @@ async fn get_authenticated_user( return Ok(Json(AuthenticatedUserResponse { user, metrics_id })); } -#[derive(Debug, Deserialize)] -struct GetUsersQueryParams { - query: Option, - page: Option, - limit: Option, -} - -async fn get_users( - Query(params): Query, - Extension(app): Extension>, -) -> Result>> { - let limit = params.limit.unwrap_or(100); - let users = if let Some(query) = params.query { - app.db.fuzzy_search_users(&query, limit).await? - } else { - app.db - .get_all_users(params.page.unwrap_or(0), limit) - .await? - }; - Ok(Json(users)) -} - #[derive(Deserialize, Debug)] struct CreateUserParams { github_user_id: i32, @@ -145,119 +113,6 @@ struct CreateUserResponse { metrics_id: String, } -async fn create_user( - Json(params): Json, - Extension(app): Extension>, - Extension(rpc_server): Extension>, -) -> Result>> { - let user = NewUserParams { - github_login: params.github_login, - github_user_id: params.github_user_id, - invite_count: params.invite_count, - }; - - // Creating a user via the normal signup process - let result = if let Some(email_confirmation_code) = params.email_confirmation_code { - if let Some(result) = app - .db - .create_user_from_invite( - &Invite { - email_address: params.email_address, - email_confirmation_code, - }, - user, - ) - .await? - { - result - } else { - return Ok(Json(None)); - } - } - // Creating a user as an admin - else if params.admin { - app.db - .create_user(¶ms.email_address, false, user) - .await? - } else { - Err(Error::Http( - StatusCode::UNPROCESSABLE_ENTITY, - "email confirmation code is required".into(), - ))? - }; - - if let Some(inviter_id) = result.inviting_user_id { - rpc_server - .invite_code_redeemed(inviter_id, result.user_id) - .await - .trace_err(); - } - - let user = app - .db - .get_user_by_id(result.user_id) - .await? - .ok_or_else(|| anyhow!("couldn't find the user we just created"))?; - - Ok(Json(Some(CreateUserResponse { - user, - metrics_id: result.metrics_id, - signup_device_id: result.signup_device_id, - }))) -} - -#[derive(Deserialize)] -struct UpdateUserParams { - admin: Option, - invite_count: Option, -} - -async fn update_user( - Path(user_id): Path, - Json(params): Json, - Extension(app): Extension>, - Extension(rpc_server): Extension>, -) -> Result<()> { - let user_id = UserId(user_id); - - if let Some(admin) = params.admin { - app.db.set_user_is_admin(user_id, admin).await?; - } - - if let Some(invite_count) = params.invite_count { - app.db - .set_invite_count_for_user(user_id, invite_count) - .await?; - rpc_server.invite_count_updated(user_id).await.trace_err(); - } - - Ok(()) -} - -async fn destroy_user( - Path(user_id): Path, - Extension(app): Extension>, -) -> Result<()> { - app.db.destroy_user(UserId(user_id)).await?; - Ok(()) -} - -#[derive(Debug, Deserialize)] -struct GetUsersWithNoInvites { - invited_by_another_user: bool, -} - -async fn get_users_with_no_invites( - Query(params): Query, - Extension(app): Extension>, -) -> Result>> { - Ok(Json( - app.db - .get_users_with_no_invites(params.invited_by_another_user) - .await?, - )) -} - #[derive(Debug, Deserialize)] struct Panic { version: String, @@ -327,69 +182,3 @@ async fn create_access_token( encrypted_access_token, })) } - -async fn get_user_for_invite_code( - Path(code): Path, - Extension(app): Extension>, -) -> Result> { - Ok(Json(app.db.get_user_for_invite_code(&code).await?)) -} - -async fn create_signup( - Json(params): Json, - Extension(app): Extension>, -) -> Result<()> { - app.db.create_signup(¶ms).await?; - Ok(()) -} - -async fn get_waitlist_summary( - Extension(app): Extension>, -) -> Result> { - Ok(Json(app.db.get_waitlist_summary().await?)) -} - -#[derive(Deserialize)] -pub struct CreateInviteFromCodeParams { - invite_code: String, - email_address: String, - device_id: Option, - #[serde(default)] - added_to_mailing_list: bool, -} - -async fn create_invite_from_code( - Json(params): Json, - Extension(app): Extension>, -) -> Result> { - Ok(Json( - app.db - .create_invite_from_code( - ¶ms.invite_code, - ¶ms.email_address, - params.device_id.as_deref(), - params.added_to_mailing_list, - ) - .await?, - )) -} - -#[derive(Deserialize)] -pub struct GetUnsentInvitesParams { - pub count: usize, -} - -async fn get_unsent_invites( - Query(params): Query, - Extension(app): Extension>, -) -> Result>> { - Ok(Json(app.db.get_unsent_invites(params.count).await?)) -} - -async fn record_sent_invites( - Json(params): Json>, - Extension(app): Extension>, -) -> Result<()> { - app.db.record_sent_invites(¶ms).await?; - Ok(()) -} diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 09a8f073b4..d132596438 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -7,5 +7,4 @@ pub mod contacts; pub mod projects; pub mod rooms; pub mod servers; -pub mod signups; pub mod users; diff --git a/crates/collab/src/db/queries/signups.rs b/crates/collab/src/db/queries/signups.rs deleted file mode 100644 index 8cb8d866fb..0000000000 --- a/crates/collab/src/db/queries/signups.rs +++ /dev/null @@ -1,349 +0,0 @@ -use super::*; -use hyper::StatusCode; - -impl Database { - pub async fn create_invite_from_code( - &self, - code: &str, - email_address: &str, - device_id: Option<&str>, - added_to_mailing_list: bool, - ) -> Result { - self.transaction(|tx| async move { - let existing_user = user::Entity::find() - .filter(user::Column::EmailAddress.eq(email_address)) - .one(&*tx) - .await?; - - if existing_user.is_some() { - Err(anyhow!("email address is already in use"))?; - } - - let inviting_user_with_invites = match user::Entity::find() - .filter( - user::Column::InviteCode - .eq(code) - .and(user::Column::InviteCount.gt(0)), - ) - .one(&*tx) - .await? - { - Some(inviting_user) => inviting_user, - None => { - return Err(Error::Http( - StatusCode::UNAUTHORIZED, - "unable to find an invite code with invites remaining".to_string(), - ))? - } - }; - user::Entity::update_many() - .filter( - user::Column::Id - .eq(inviting_user_with_invites.id) - .and(user::Column::InviteCount.gt(0)), - ) - .col_expr( - user::Column::InviteCount, - Expr::col(user::Column::InviteCount).sub(1), - ) - .exec(&*tx) - .await?; - - let signup = signup::Entity::insert(signup::ActiveModel { - email_address: ActiveValue::set(email_address.into()), - email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), - email_confirmation_sent: ActiveValue::set(false), - inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)), - platform_linux: ActiveValue::set(false), - platform_mac: ActiveValue::set(false), - platform_windows: ActiveValue::set(false), - platform_unknown: ActiveValue::set(true), - device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())), - added_to_mailing_list: ActiveValue::set(added_to_mailing_list), - ..Default::default() - }) - .on_conflict( - OnConflict::column(signup::Column::EmailAddress) - .update_column(signup::Column::InvitingUserId) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - Ok(Invite { - email_address: signup.email_address, - email_confirmation_code: signup.email_confirmation_code, - }) - }) - .await - } - - pub async fn create_user_from_invite( - &self, - invite: &Invite, - user: NewUserParams, - ) -> Result> { - self.transaction(|tx| async { - let tx = tx; - let signup = signup::Entity::find() - .filter( - signup::Column::EmailAddress - .eq(invite.email_address.as_str()) - .and( - signup::Column::EmailConfirmationCode - .eq(invite.email_confirmation_code.as_str()), - ), - ) - .one(&*tx) - .await? - .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?; - - if signup.user_id.is_some() { - return Ok(None); - } - - let user = user::Entity::insert(user::ActiveModel { - email_address: ActiveValue::set(Some(invite.email_address.clone())), - github_login: ActiveValue::set(user.github_login.clone()), - github_user_id: ActiveValue::set(Some(user.github_user_id)), - admin: ActiveValue::set(false), - invite_count: ActiveValue::set(user.invite_count), - invite_code: ActiveValue::set(Some(random_invite_code())), - metrics_id: ActiveValue::set(Uuid::new_v4()), - ..Default::default() - }) - .on_conflict( - OnConflict::column(user::Column::GithubLogin) - .update_columns([ - user::Column::EmailAddress, - user::Column::GithubUserId, - user::Column::Admin, - ]) - .to_owned(), - ) - .exec_with_returning(&*tx) - .await?; - - let mut signup = signup.into_active_model(); - signup.user_id = ActiveValue::set(Some(user.id)); - let signup = signup.update(&*tx).await?; - - if let Some(inviting_user_id) = signup.inviting_user_id { - let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id { - (inviting_user_id, user.id, true) - } else { - (user.id, inviting_user_id, false) - }; - - contact::Entity::insert(contact::ActiveModel { - user_id_a: ActiveValue::set(user_id_a), - user_id_b: ActiveValue::set(user_id_b), - a_to_b: ActiveValue::set(a_to_b), - should_notify: ActiveValue::set(true), - accepted: ActiveValue::set(true), - ..Default::default() - }) - .on_conflict(OnConflict::new().do_nothing().to_owned()) - .exec_without_returning(&*tx) - .await?; - } - - Ok(Some(NewUserResult { - user_id: user.id, - metrics_id: user.metrics_id.to_string(), - inviting_user_id: signup.inviting_user_id, - signup_device_id: signup.device_id, - })) - }) - .await - } - - pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> { - self.transaction(|tx| async move { - if count > 0 { - user::Entity::update_many() - .filter( - user::Column::Id - .eq(id) - .and(user::Column::InviteCode.is_null()), - ) - .set(user::ActiveModel { - invite_code: ActiveValue::set(Some(random_invite_code())), - ..Default::default() - }) - .exec(&*tx) - .await?; - } - - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - invite_count: ActiveValue::set(count), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_invite_code_for_user(&self, id: UserId) -> Result> { - self.transaction(|tx| async move { - match user::Entity::find_by_id(id).one(&*tx).await? { - Some(user) if user.invite_code.is_some() => { - Ok(Some((user.invite_code.unwrap(), user.invite_count))) - } - _ => Ok(None), - } - }) - .await - } - - pub async fn get_user_for_invite_code(&self, code: &str) -> Result { - self.transaction(|tx| async move { - user::Entity::find() - .filter(user::Column::InviteCode.eq(code)) - .one(&*tx) - .await? - .ok_or_else(|| { - Error::Http( - StatusCode::NOT_FOUND, - "that invite code does not exist".to_string(), - ) - }) - }) - .await - } - - pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> { - self.transaction(|tx| async move { - signup::Entity::insert(signup::ActiveModel { - email_address: ActiveValue::set(signup.email_address.clone()), - email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), - email_confirmation_sent: ActiveValue::set(false), - platform_mac: ActiveValue::set(signup.platform_mac), - platform_windows: ActiveValue::set(signup.platform_windows), - platform_linux: ActiveValue::set(signup.platform_linux), - platform_unknown: ActiveValue::set(false), - editor_features: ActiveValue::set(Some(signup.editor_features.clone())), - programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), - device_id: ActiveValue::set(signup.device_id.clone()), - added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list), - ..Default::default() - }) - .on_conflict( - OnConflict::column(signup::Column::EmailAddress) - .update_columns([ - signup::Column::PlatformMac, - signup::Column::PlatformWindows, - signup::Column::PlatformLinux, - signup::Column::EditorFeatures, - signup::Column::ProgrammingLanguages, - signup::Column::DeviceId, - signup::Column::AddedToMailingList, - ]) - .to_owned(), - ) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_signup(&self, email_address: &str) -> Result { - self.transaction(|tx| async move { - let signup = signup::Entity::find() - .filter(signup::Column::EmailAddress.eq(email_address)) - .one(&*tx) - .await? - .ok_or_else(|| { - anyhow!("signup with email address {} doesn't exist", email_address) - })?; - - Ok(signup) - }) - .await - } - - pub async fn get_waitlist_summary(&self) -> Result { - self.transaction(|tx| async move { - let query = " - SELECT - COUNT(*) as count, - COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count, - COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count, - COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count, - COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count - FROM ( - SELECT * - FROM signups - WHERE - NOT email_confirmation_sent - ) AS unsent - "; - Ok( - WaitlistSummary::find_by_statement(Statement::from_sql_and_values( - self.pool.get_database_backend(), - query.into(), - vec![], - )) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid result"))?, - ) - }) - .await - } - - pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> { - let emails = invites - .iter() - .map(|s| s.email_address.as_str()) - .collect::>(); - self.transaction(|tx| async { - let tx = tx; - signup::Entity::update_many() - .filter(signup::Column::EmailAddress.is_in(emails.iter().copied())) - .set(signup::ActiveModel { - email_confirmation_sent: ActiveValue::set(true), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - - pub async fn get_unsent_invites(&self, count: usize) -> Result> { - self.transaction(|tx| async move { - Ok(signup::Entity::find() - .select_only() - .column(signup::Column::EmailAddress) - .column(signup::Column::EmailConfirmationCode) - .filter( - signup::Column::EmailConfirmationSent.eq(false).and( - signup::Column::PlatformMac - .eq(true) - .or(signup::Column::PlatformUnknown.eq(true)), - ), - ) - .order_by_asc(signup::Column::CreatedAt) - .limit(count as u64) - .into_model() - .all(&*tx) - .await?) - }) - .await - } -} - -fn random_invite_code() -> String { - nanoid::nanoid!(16) -} - -fn random_email_confirmation_code() -> String { - nanoid::nanoid!(64) -} diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index 5cb1ef6ea3..db968ba895 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -123,27 +123,6 @@ impl Database { .await } - pub async fn get_users_with_no_invites( - &self, - invited_by_another_user: bool, - ) -> Result> { - self.transaction(|tx| async move { - Ok(user::Entity::find() - .filter( - user::Column::InviteCount - .eq(0) - .and(if invited_by_another_user { - user::Column::InviterId.is_not_null() - } else { - user::Column::InviterId.is_null() - }), - ) - .all(&*tx) - .await?) - }) - .await - } - pub async fn get_user_metrics_id(&self, id: UserId) -> Result { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryAs { @@ -163,21 +142,6 @@ impl Database { .await } - pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> { - self.transaction(|tx| async move { - user::Entity::update_many() - .filter(user::Column::Id.eq(id)) - .set(user::ActiveModel { - admin: ActiveValue::set(is_admin), - ..Default::default() - }) - .exec(&*tx) - .await?; - Ok(()) - }) - .await - } - pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { self.transaction(|tx| async move { user::Entity::update_many() diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index fc31ee7c4d..0e6a0529c4 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -575,308 +575,6 @@ async fn test_fuzzy_search_users() { } } -#[gpui::test] -async fn test_invite_codes() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - - let NewUserResult { user_id: user1, .. } = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - - // Initially, user 1 has no invite code - assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None); - - // Setting invite count to 0 when no code is assigned does not assign a new code - db.set_invite_count_for_user(user1, 0).await.unwrap(); - assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none()); - - // User 1 creates an invite code that can be used twice. - db.set_invite_count_for_user(user1, 2).await.unwrap(); - let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 2); - - // User 2 redeems the invite code and becomes a contact of user 1. - let user2_invite = db - .create_invite_from_code( - &invite_code, - "user2@example.com", - Some("user-2-device-id"), - true, - ) - .await - .unwrap(); - let NewUserResult { - user_id: user2, - inviting_user_id, - signup_device_id, - metrics_id, - } = db - .create_user_from_invite( - &user2_invite, - NewUserParams { - github_login: "user2".into(), - github_user_id: 2, - invite_count: 7, - }, - ) - .await - .unwrap() - .unwrap(); - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 1); - assert_eq!(inviting_user_id, Some(user1)); - assert_eq!(signup_device_id.unwrap(), "user-2-device-id"); - assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }] - ); - assert_eq!( - db.get_contacts(user2).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: false, - busy: false, - }] - ); - assert!(db.has_contact(user1, user2).await.unwrap()); - assert!(db.has_contact(user2, user1).await.unwrap()); - assert_eq!( - db.get_invite_code_for_user(user2).await.unwrap().unwrap().1, - 7 - ); - - // User 3 redeems the invite code and becomes a contact of user 1. - let user3_invite = db - .create_invite_from_code(&invite_code, "user3@example.com", None, true) - .await - .unwrap(); - let NewUserResult { - user_id: user3, - inviting_user_id, - signup_device_id, - .. - } = db - .create_user_from_invite( - &user3_invite, - NewUserParams { - github_login: "user-3".into(), - github_user_id: 3, - invite_count: 3, - }, - ) - .await - .unwrap() - .unwrap(); - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 0); - assert_eq!(inviting_user_id, Some(user1)); - assert!(signup_device_id.is_none()); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user3, - should_notify: true, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user3).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: false, - busy: false, - }] - ); - assert!(db.has_contact(user1, user3).await.unwrap()); - assert!(db.has_contact(user3, user1).await.unwrap()); - assert_eq!( - db.get_invite_code_for_user(user3).await.unwrap().unwrap().1, - 3 - ); - - // Trying to reedem the code for the third time results in an error. - db.create_invite_from_code( - &invite_code, - "user4@example.com", - Some("user-4-device-id"), - true, - ) - .await - .unwrap_err(); - - // Invite count can be updated after the code has been created. - db.set_invite_count_for_user(user1, 2).await.unwrap(); - let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0 - assert_eq!(invite_count, 2); - - // User 4 can now redeem the invite code and becomes a contact of user 1. - let user4_invite = db - .create_invite_from_code( - &invite_code, - "user4@example.com", - Some("user-4-device-id"), - true, - ) - .await - .unwrap(); - let user4 = db - .create_user_from_invite( - &user4_invite, - NewUserParams { - github_login: "user-4".into(), - github_user_id: 4, - invite_count: 5, - }, - ) - .await - .unwrap() - .unwrap() - .user_id; - - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 1); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user3, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user4, - should_notify: true, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user4).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: false, - busy: false, - }] - ); - assert!(db.has_contact(user1, user4).await.unwrap()); - assert!(db.has_contact(user4, user1).await.unwrap()); - assert_eq!( - db.get_invite_code_for_user(user4).await.unwrap().unwrap().1, - 5 - ); - - // An existing user cannot redeem invite codes. - db.create_invite_from_code( - &invite_code, - "user2@example.com", - Some("user-2-device-id"), - true, - ) - .await - .unwrap_err(); - let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap(); - assert_eq!(invite_count, 1); - - // A newer user can invite an existing one via a different email address - // than the one they used to sign up. - let user5 = db - .create_user( - "user5@example.com", - false, - NewUserParams { - github_login: "user5".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - db.set_invite_count_for_user(user5, 5).await.unwrap(); - let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap(); - let user5_invite_to_user1 = db - .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true) - .await - .unwrap(); - let user1_2 = db - .create_user_from_invite( - &user5_invite_to_user1, - NewUserParams { - github_login: "user1".into(), - github_user_id: 1, - invite_count: 5, - }, - ) - .await - .unwrap() - .unwrap() - .user_id; - assert_eq!(user1_2, user1); - assert_eq!( - db.get_contacts(user1).await.unwrap(), - [ - Contact::Accepted { - user_id: user2, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user3, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user4, - should_notify: true, - busy: false, - }, - Contact::Accepted { - user_id: user5, - should_notify: false, - busy: false, - } - ] - ); - assert_eq!( - db.get_contacts(user5).await.unwrap(), - [Contact::Accepted { - user_id: user1, - should_notify: true, - busy: false, - }] - ); - assert!(db.has_contact(user1, user5).await.unwrap()); - assert!(db.has_contact(user5, user1).await.unwrap()); -} - test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); async fn test_channels(db: &Arc) { @@ -1329,245 +1027,6 @@ async fn test_channel_renames(db: &Arc) { assert!(bad_name_rename.is_err()) } -#[gpui::test] -async fn test_multiple_signup_overwrite() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - - let email_address = "user_1@example.com".to_string(); - - let initial_signup_created_at_milliseconds = 0; - - let initial_signup = NewSignup { - email_address: email_address.clone(), - platform_mac: false, - platform_linux: true, - platform_windows: false, - editor_features: vec!["speed".into()], - programming_languages: vec!["rust".into(), "c".into()], - device_id: Some(format!("device_id")), - added_to_mailing_list: false, - created_at: Some( - DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(), - ), - }; - - db.create_signup(&initial_signup).await.unwrap(); - - let initial_signup_from_db = db.get_signup(&email_address).await.unwrap(); - - assert_eq!( - initial_signup_from_db.clone(), - signup::Model { - email_address: initial_signup.email_address, - platform_mac: initial_signup.platform_mac, - platform_linux: initial_signup.platform_linux, - platform_windows: initial_signup.platform_windows, - editor_features: Some(initial_signup.editor_features), - programming_languages: Some(initial_signup.programming_languages), - added_to_mailing_list: initial_signup.added_to_mailing_list, - ..initial_signup_from_db - } - ); - - let subsequent_signup = NewSignup { - email_address: email_address.clone(), - platform_mac: true, - platform_linux: false, - platform_windows: true, - editor_features: vec!["git integration".into(), "clean design".into()], - programming_languages: vec!["d".into(), "elm".into()], - device_id: Some(format!("different_device_id")), - added_to_mailing_list: true, - // subsequent signup happens next day - created_at: Some( - DateTime::from_timestamp_millis( - initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24), - ) - .unwrap(), - ), - }; - - db.create_signup(&subsequent_signup).await.unwrap(); - - let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap(); - - assert_eq!( - subsequent_signup_from_db.clone(), - signup::Model { - platform_mac: subsequent_signup.platform_mac, - platform_linux: subsequent_signup.platform_linux, - platform_windows: subsequent_signup.platform_windows, - editor_features: Some(subsequent_signup.editor_features), - programming_languages: Some(subsequent_signup.programming_languages), - device_id: subsequent_signup.device_id, - added_to_mailing_list: subsequent_signup.added_to_mailing_list, - // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line - created_at: initial_signup_from_db.created_at, - ..subsequent_signup_from_db - } - ); -} - -#[gpui::test] -async fn test_signups() { - let test_db = TestDb::postgres(build_background_executor()); - let db = test_db.db(); - - let usernames = (0..8).map(|i| format!("person-{i}")).collect::>(); - - let all_signups = usernames - .iter() - .enumerate() - .map(|(i, username)| NewSignup { - email_address: format!("{username}@example.com"), - platform_mac: true, - platform_linux: i % 2 == 0, - platform_windows: i % 4 == 0, - editor_features: vec!["speed".into()], - programming_languages: vec!["rust".into(), "c".into()], - device_id: Some(format!("device_id_{i}")), - added_to_mailing_list: i != 0, // One user failed to subscribe - created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive - }) - .collect::>(); - - // people sign up on the waitlist - for signup in &all_signups { - // users can sign up multiple times without issues - for _ in 0..2 { - db.create_signup(&signup).await.unwrap(); - } - } - - assert_eq!( - db.get_waitlist_summary().await.unwrap(), - WaitlistSummary { - count: 8, - mac_count: 8, - linux_count: 4, - windows_count: 2, - unknown_count: 0, - } - ); - - // retrieve the next batch of signup emails to send - let signups_batch1 = db.get_unsent_invites(3).await.unwrap(); - let addresses = signups_batch1 - .iter() - .map(|s| &s.email_address) - .collect::>(); - assert_eq!( - addresses, - &[ - all_signups[0].email_address.as_str(), - all_signups[1].email_address.as_str(), - all_signups[2].email_address.as_str() - ] - ); - assert_ne!( - signups_batch1[0].email_confirmation_code, - signups_batch1[1].email_confirmation_code - ); - - // the waitlist isn't updated until we record that the emails - // were successfully sent. - let signups_batch = db.get_unsent_invites(3).await.unwrap(); - assert_eq!(signups_batch, signups_batch1); - - // once the emails go out, we can retrieve the next batch - // of signups. - db.record_sent_invites(&signups_batch1).await.unwrap(); - let signups_batch2 = db.get_unsent_invites(3).await.unwrap(); - let addresses = signups_batch2 - .iter() - .map(|s| &s.email_address) - .collect::>(); - assert_eq!( - addresses, - &[ - all_signups[3].email_address.as_str(), - all_signups[4].email_address.as_str(), - all_signups[5].email_address.as_str() - ] - ); - - // the sent invites are excluded from the summary. - assert_eq!( - db.get_waitlist_summary().await.unwrap(), - WaitlistSummary { - count: 5, - mac_count: 5, - linux_count: 2, - windows_count: 1, - unknown_count: 0, - } - ); - - // user completes the signup process by providing their - // github account. - let NewUserResult { - user_id, - inviting_user_id, - signup_device_id, - .. - } = db - .create_user_from_invite( - &Invite { - ..signups_batch1[0].clone() - }, - NewUserParams { - github_login: usernames[0].clone(), - github_user_id: 0, - invite_count: 5, - }, - ) - .await - .unwrap() - .unwrap(); - let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); - assert!(inviting_user_id.is_none()); - assert_eq!(user.github_login, usernames[0]); - assert_eq!( - user.email_address, - Some(all_signups[0].email_address.clone()) - ); - assert_eq!(user.invite_count, 5); - assert_eq!(signup_device_id.unwrap(), "device_id_0"); - - // cannot redeem the same signup again. - assert!(db - .create_user_from_invite( - &Invite { - email_address: signups_batch1[0].email_address.clone(), - email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), - }, - NewUserParams { - github_login: "some-other-github_account".into(), - github_user_id: 1, - invite_count: 5, - }, - ) - .await - .unwrap() - .is_none()); - - // cannot redeem a signup with the wrong confirmation code. - db.create_user_from_invite( - &Invite { - email_address: signups_batch1[1].email_address.clone(), - email_confirmation_code: "the-wrong-code".to_string(), - }, - NewUserParams { - github_login: usernames[1].clone(), - github_user_id: 2, - invite_count: 5, - }, - ) - .await - .unwrap_err(); -} - fn build_background_executor() -> Arc { Deterministic::new(0).build_background() } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index e454fcbb9e..2d66b43b93 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -553,9 +553,8 @@ impl Server { this.app_state.db.set_user_connected_once(user_id, true).await?; } - let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4( + let (contacts, channels_for_user, channel_invites) = future::try_join3( this.app_state.db.get_contacts(user_id), - this.app_state.db.get_invite_code_for_user(user_id), this.app_state.db.get_channels_for_user(user_id), this.app_state.db.get_channel_invites_for_user(user_id) ).await?; @@ -568,13 +567,6 @@ impl Server { channels_for_user, channel_invites ))?; - - if let Some((code, count)) = invite_code { - this.peer.send(connection_id, proto::UpdateInviteInfo { - url: format!("{}{}", this.app_state.config.invite_link_prefix, code), - count: count as u32, - })?; - } } if let Some(incoming_call) = this.app_state.db.incoming_call_for_user(user_id).await? { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 8121b0ac91..a9f4a31eb7 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3146,6 +3146,7 @@ async fn test_local_settings( ) .await; let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; + deterministic.run_until_parked(); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8e0252ec60..c2a2b35134 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1249,7 +1249,7 @@ impl CollabPanel { .collab_panel .add_contact_button .style_for(is_selected, state), - "icons/plus_16.svg", + "icons/plus.svg", ) }) .with_cursor_style(CursorStyle::PointingHand) @@ -1668,7 +1668,7 @@ impl CollabPanel { cx.font_cache(), )) .with_child( - Svg::new("icons/radix/file.svg") + Svg::new("icons/file.svg") .with_color(theme.channel_hash.color) .constrained() .with_width(theme.channel_hash.width) diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index 539e041ae7..9f96aa4b60 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -211,7 +211,7 @@ impl PickerDelegate for ContactFinderDelegate { ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { Some("icons/check_8.svg") } - ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"), + ContactRequestStatus::RequestSent => Some("icons/x.svg"), ContactRequestStatus::RequestAccepted => None, }; let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index f0e09e139e..4537bf04e0 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -483,10 +483,10 @@ impl CollabTitlebarItem { let icon; let tooltip; if room.read(cx).is_screen_sharing() { - icon = "icons/radix/desktop.svg"; + icon = "icons/desktop.svg"; tooltip = "Stop Sharing Screen" } else { - icon = "icons/radix/desktop.svg"; + icon = "icons/desktop.svg"; tooltip = "Share Screen"; } @@ -533,10 +533,10 @@ impl CollabTitlebarItem { let tooltip; let is_muted = room.read(cx).is_muted(cx); if is_muted { - icon = "icons/radix/mic-mute.svg"; + icon = "icons/mic-mute.svg"; tooltip = "Unmute microphone"; } else { - icon = "icons/radix/mic.svg"; + icon = "icons/mic.svg"; tooltip = "Mute microphone"; } @@ -586,10 +586,10 @@ impl CollabTitlebarItem { let tooltip; let is_deafened = room.read(cx).is_deafened().unwrap_or(false); if is_deafened { - icon = "icons/radix/speaker-off.svg"; + icon = "icons/speaker-off.svg"; tooltip = "Unmute speakers"; } else { - icon = "icons/radix/speaker-loud.svg"; + icon = "icons/speaker-loud.svg"; tooltip = "Mute speakers"; } @@ -625,7 +625,7 @@ impl CollabTitlebarItem { .into_any() } fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - let icon = "icons/radix/exit.svg"; + let icon = "icons/exit.svg"; let tooltip = "Leave call"; let titlebar = &theme.titlebar; @@ -748,7 +748,7 @@ impl CollabTitlebarItem { dropdown .with_child( - Svg::new("icons/caret_down_8.svg") + Svg::new("icons/caret_down.svg") .with_color(user_menu_button_style.icon.color) .constrained() .with_width(user_menu_button_style.icon.width) @@ -1116,7 +1116,7 @@ impl CollabTitlebarItem { | client::Status::Reauthenticating { .. } | client::Status::Reconnecting { .. } | client::Status::ReconnectionError { .. } => Some( - Svg::new("icons/cloud_slash_12.svg") + Svg::new("icons/disconnected.svg") .with_color(theme.titlebar.offline_icon.color) .constrained() .with_width(theme.titlebar.offline_icon.width) diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 9aff3a3522..5943e016cb 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -53,7 +53,7 @@ where .with_child( MouseEventHandler::new::(user.id as usize, cx, |state, _| { let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") + Svg::new("icons/x.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index f73f854927..ce0f364806 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -78,15 +78,15 @@ impl View for CopilotButton { .with_child( Svg::new({ match status { - Status::Error(_) => "icons/copilot_error_16.svg", + Status::Error(_) => "icons/copilot_error.svg", Status::Authorized => { if enabled { - "icons/copilot_16.svg" + "icons/copilot.svg" } else { - "icons/copilot_disabled_16.svg" + "icons/copilot_disabled.svg" } } - _ => "icons/copilot_init_16.svg", + _ => "icons/copilot_init.svg", } }) .with_color(style.icon_color) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 0e5b714f09..ac45bcbb79 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -686,11 +686,9 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); let icon_width = cx.em_width * style.icon_width_factor; let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { - Svg::new("icons/circle_x_mark_12.svg") - .with_color(theme.error_diagnostic.message.text.color) + Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color) } else { - Svg::new("icons/triangle_exclamation_12.svg") - .with_color(theme.warning_diagnostic.message.text.color) + Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color) }; Flex::row() @@ -748,7 +746,7 @@ pub(crate) fn render_summary( let summary_spacing = theme.tab_summary_spacing; Flex::row() .with_child( - Svg::new("icons/circle_x_mark_12.svg") + Svg::new("icons/error.svg") .with_color(text_style.color) .constrained() .with_width(icon_width) @@ -767,7 +765,7 @@ pub(crate) fn render_summary( .aligned(), ) .with_child( - Svg::new("icons/triangle_exclamation_12.svg") + Svg::new("icons/warning.svg") .with_color(text_style.color) .constrained() .with_width(icon_width) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 89b4469d42..c3733018b6 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -32,7 +32,8 @@ impl DiagnosticIndicator { this.in_progress_checks.insert(*language_server_id); cx.notify(); } - project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { + project::Event::DiskBasedDiagnosticsFinished { language_server_id } + | project::Event::LanguageServerRemoved(language_server_id) => { this.summary = project.read(cx).diagnostic_summary(cx); this.in_progress_checks.remove(language_server_id); cx.notify(); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f306692b5e..d97db9695a 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,11 +5,11 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::{DocumentRange, InlayRange}, - Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; -use collections::{HashMap, HashSet}; +use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; use gpui::{ color::Color, @@ -43,7 +43,8 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type InlayHighlights = BTreeMap>; pub struct DisplayMap { buffer: ModelHandle, @@ -54,6 +55,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -89,6 +91,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -113,6 +116,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), + inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -215,37 +219,32 @@ impl DisplayMap { ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights.insert( - Some(type_id), - Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())), - ); + self.text_highlights + .insert(Some(type_id), Arc::new((style, ranges))); } pub fn highlight_inlays( &mut self, type_id: TypeId, - ranges: Vec, + highlights: Vec, style: HighlightStyle, ) { - self.text_highlights.insert( - Some(type_id), - Arc::new(( - style, - ranges.into_iter().map(DocumentRange::Inlay).collect(), - )), - ); + for highlight in highlights { + self.inlay_highlights + .entry(type_id) + .or_default() + .insert(highlight.inlay, (style, highlight)); + } } - pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> { + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) } - - pub fn clear_text_highlights( - &mut self, - type_id: TypeId, - ) -> Option)>> { - self.text_highlights.remove(&Some(type_id)) + pub fn clear_highlights(&mut self, type_id: TypeId) -> bool { + let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some(); + cleared |= self.inlay_highlights.remove(&type_id).is_none(); + cleared } pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { @@ -309,6 +308,14 @@ impl DisplayMap { } } +#[derive(Debug, Default)] +pub struct Highlights<'a> { + pub text_highlights: Option<&'a TextHighlights>, + pub inlay_highlights: Option<&'a InlayHighlights>, + pub inlay_highlight_style: Option, + pub suggestion_highlight_style: Option, +} + pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, pub fold_snapshot: fold_map::FoldSnapshot, @@ -317,6 +324,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -422,15 +430,6 @@ impl DisplaySnapshot { .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) } - pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint { - let inlay_point = self.inlay_snapshot.to_point(offset); - let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); - let tab_point = self.tab_snapshot.to_tab_point(fold_point); - let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); - let block_point = self.block_snapshot.to_block_point(wrap_point); - DisplayPoint(block_point) - } - fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); @@ -463,9 +462,7 @@ impl DisplaySnapshot { .chunks( display_row..self.max_point().row() + 1, false, - None, - None, - None, + Highlights::default(), ) .map(|h| h.text) } @@ -474,7 +471,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None) + .chunks(row..row + 1, false, Highlights::default()) .map(|h| h.text) .collect::>() .into_iter() @@ -482,19 +479,22 @@ impl DisplaySnapshot { }) } - pub fn chunks( - &self, + pub fn chunks<'a>( + &'a self, display_rows: Range, language_aware: bool, - hint_highlight_style: Option, + inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, - Some(&self.text_highlights), - hint_highlight_style, - suggestion_highlight_style, + Highlights { + text_highlights: Some(&self.text_highlights), + inlay_highlights: Some(&self.inlay_highlights), + inlay_highlight_style, + suggestion_highlight_style, + }, ) } @@ -752,12 +752,20 @@ impl DisplaySnapshot { } #[cfg(any(test, feature = "test-support"))] - pub fn highlight_ranges( + pub fn text_highlight_ranges( &self, - ) -> Option)>> { + ) -> Option>)>> { let type_id = TypeId::of::(); self.text_highlights.get(&Some(type_id)).cloned() } + + #[cfg(any(test, feature = "test-support"))] + pub fn inlay_highlights( + &self, + ) -> Option<&HashMap> { + let type_id = TypeId::of::(); + self.inlay_highlights.get(&type_id) + } } #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 741507004c..e54ac04d89 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,10 +1,10 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - TextHighlights, + Highlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{fonts::HighlightStyle, AnyElement, ViewContext}; +use gpui::{AnyElement, ViewContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -576,9 +576,7 @@ impl BlockSnapshot { self.chunks( 0..self.transforms.summary().output_rows, false, - None, - None, - None, + Highlights::default(), ) .map(|chunk| chunk.text) .collect() @@ -588,9 +586,7 @@ impl BlockSnapshot { &'a self, rows: Range, language_aware: bool, - text_highlights: Option<&'a TextHighlights>, - hint_highlight_style: Option, - suggestion_highlight_style: Option, + highlights: Highlights<'a>, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -622,9 +618,7 @@ impl BlockSnapshot { input_chunks: self.wrap_snapshot.chunks( input_start..input_end, language_aware, - text_highlights, - hint_highlight_style, - suggestion_highlight_style, + highlights, ), input_chunk: Default::default(), transforms: cursor, @@ -1501,9 +1495,7 @@ mod tests { .chunks( start_row as u32..blocks_snapshot.max_point().row + 1, false, - None, - None, - None, + Highlights::default(), ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index d5473027a6..8faa0c3ec2 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - TextHighlights, + Highlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, Highlights::default()) .map(|c| c.text) .collect() } @@ -651,9 +651,7 @@ impl FoldSnapshot { &'a self, range: Range, language_aware: bool, - text_highlights: Option<&'a TextHighlights>, - hint_highlight_style: Option, - suggestion_highlight_style: Option, + highlights: Highlights<'a>, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -674,9 +672,7 @@ impl FoldSnapshot { inlay_chunks: self.inlay_snapshot.chunks( inlay_start..inlay_end, language_aware, - text_highlights, - hint_highlight_style, - suggestion_highlight_style, + highlights, ), inlay_chunk: None, inlay_offset: inlay_start, @@ -687,8 +683,12 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks(start.to_offset(self)..self.len(), false, None, None, None) - .flat_map(|chunk| chunk.text.chars()) + self.chunks( + start.to_offset(self)..self.len(), + false, + Highlights::default(), + ) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1496,7 +1496,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, Highlights::default()) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 25b8d3aef6..124b32c234 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,5 +1,4 @@ use crate::{ - link_go_to_definition::DocumentRange, multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; @@ -11,12 +10,13 @@ use std::{ cmp, iter::Peekable, ops::{Add, AddAssign, Range, Sub, SubAssign}, + sync::Arc, vec, }; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, SumTree, TreeMap}; use text::{Patch, Rope}; -use super::TextHighlights; +use super::Highlights; pub struct InlayMap { snapshot: InlaySnapshot, @@ -214,10 +214,11 @@ pub struct InlayChunks<'a> { inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, - hint_highlight_style: Option, + inlay_highlight_style: Option, suggestion_highlight_style: Option, highlight_endpoints: Peekable>, active_highlights: BTreeMap, HighlightStyle>, + highlights: Highlights<'a>, snapshot: &'a InlaySnapshot, } @@ -293,8 +294,41 @@ impl<'a> Iterator for InlayChunks<'a> { prefix } Transform::Inlay(inlay) => { + let mut inlay_style_and_highlight = None; + if let Some(inlay_highlights) = self.highlights.inlay_highlights { + for (_, inlay_id_to_data) in inlay_highlights.iter() { + let style_and_highlight = inlay_id_to_data.get(&inlay.id); + if style_and_highlight.is_some() { + inlay_style_and_highlight = style_and_highlight; + break; + } + } + } + + let mut highlight_style = match inlay.id { + InlayId::Suggestion(_) => self.suggestion_highlight_style, + InlayId::Hint(_) => self.inlay_highlight_style, + }; + let next_inlay_highlight_endpoint; + let offset_in_inlay = self.output_offset - self.transforms.start().0; + if let Some((style, highlight)) = inlay_style_and_highlight { + let range = &highlight.range; + if offset_in_inlay.0 < range.start { + next_inlay_highlight_endpoint = range.start - offset_in_inlay.0; + } else if offset_in_inlay.0 >= range.end { + next_inlay_highlight_endpoint = usize::MAX; + } else { + next_inlay_highlight_endpoint = range.end - offset_in_inlay.0; + highlight_style + .get_or_insert_with(|| Default::default()) + .highlight(style.clone()); + } + } else { + next_inlay_highlight_endpoint = usize::MAX; + } + let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| { - let start = self.output_offset - self.transforms.start().0; + let start = offset_in_inlay; let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0) - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) @@ -302,21 +336,15 @@ impl<'a> Iterator for InlayChunks<'a> { let inlay_chunk = self .inlay_chunk .get_or_insert_with(|| inlay_chunks.next().unwrap()); - let (chunk, remainder) = inlay_chunk.split_at( - inlay_chunk - .len() - .min(next_highlight_endpoint.0 - self.output_offset.0), - ); + let (chunk, remainder) = + inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint)); *inlay_chunk = remainder; if inlay_chunk.is_empty() { self.inlay_chunk = None; } self.output_offset.0 += chunk.len(); - let mut highlight_style = match inlay.id { - InlayId::Suggestion(_) => self.suggestion_highlight_style, - InlayId::Hint(_) => self.hint_highlight_style, - }; + if !self.active_highlights.is_empty() { for active_highlight in self.active_highlights.values() { highlight_style @@ -625,18 +653,20 @@ impl InlayMap { .filter(|ch| *ch != '\r') .take(len) .collect::(); - log::info!( - "creating inlay at buffer offset {} with bias {:?} and text {:?}", - position, - bias, - text - ); let inlay_id = if i % 2 == 0 { InlayId::Hint(post_inc(next_inlay_id)) } else { InlayId::Suggestion(post_inc(next_inlay_id)) }; + log::info!( + "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}", + inlay_id, + position, + bias, + text + ); + to_insert.push(Inlay { id: inlay_id, position: snapshot.buffer.anchor_at(position, bias), @@ -992,77 +1022,24 @@ impl InlaySnapshot { &'a self, range: Range, language_aware: bool, - text_highlights: Option<&'a TextHighlights>, - hint_highlight_style: Option, - suggestion_highlight_style: Option, + highlights: Highlights<'a>, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = text_highlights { + if let Some(text_highlights) = highlights.text_highlights { if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); - let transform_start = - self.to_inlay_offset(transform_start.to_offset(&self.buffer)); - - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - - for (tag, text_highlights) in text_highlights.iter() { - let style = text_highlights.0; - let ranges = &text_highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = self - .document_to_inlay_range(probe) - .end - .cmp(&transform_start); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - let range = self.document_to_inlay_range(range); - if range.start.cmp(&transform_end).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.end, - is_start: false, - tag: *tag, - style, - }); - } - } - - cursor.next(&()); - } - highlight_endpoints.sort(); + self.apply_text_highlights( + &mut cursor, + &range, + text_highlights, + &mut highlight_endpoints, + ); cursor.seek(&range.start, Bias::Right, &()); } } - + highlight_endpoints.sort(); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); let buffer_chunks = self.buffer.chunks(buffer_range, language_aware); @@ -1074,29 +1051,76 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style, - suggestion_highlight_style, + inlay_highlight_style: highlights.inlay_highlight_style, + suggestion_highlight_style: highlights.suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), + highlights, snapshot: self, } } - fn document_to_inlay_range(&self, range: &DocumentRange) -> Range { - match range { - DocumentRange::Text(text_range) => { - self.to_inlay_offset(text_range.start.to_offset(&self.buffer)) - ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer)) - } - DocumentRange::Inlay(inlay_range) => { - inlay_range.highlight_start..inlay_range.highlight_end + fn apply_text_highlights( + &self, + cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>, + range: &Range, + text_highlights: &TreeMap, Arc<(HighlightStyle, Vec>)>>, + highlight_endpoints: &mut Vec, + ) { + while cursor.start().0 < range.end { + let transform_start = self + .buffer + .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + let transform_end = + { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); + } } + + cursor.next(&()); } } #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None, None, None) + self.chunks(Default::default()..self.len(), false, Highlights::default()) .map(|chunk| chunk.text) .collect() } @@ -1144,7 +1168,11 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { #[cfg(test)] mod tests { use super::*; - use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer}; + use crate::{ + display_map::{InlayHighlights, TextHighlights}, + link_go_to_definition::InlayHighlight, + InlayId, MultiBuffer, + }; use gpui::AppContext; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; @@ -1619,8 +1647,8 @@ mod tests { }) .collect::>(); let mut expected_text = Rope::from(buffer_snapshot.text()); - for (offset, inlay) in inlays.into_iter().rev() { - expected_text.replace(offset..offset, &inlay.text.to_string()); + for (offset, inlay) in inlays.iter().rev() { + expected_text.replace(*offset..*offset, &inlay.text.to_string()); } assert_eq!(inlay_snapshot.text(), expected_text.to_string()); @@ -1640,51 +1668,87 @@ mod tests { ); } - let mut highlights = TextHighlights::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) + let mut text_highlights = TextHighlights::default(); + let text_highlight_count = rng.gen_range(0_usize..10); + let mut text_highlight_ranges = (0..text_highlight_count) .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = if rng.gen_bool(0.5) { - highlight_ranges - .into_iter() - .map(|range| InlayRange { - inlay_position: buffer_snapshot.anchor_before(range.start), - highlight_start: inlay_snapshot.to_inlay_offset(range.start), - highlight_end: inlay_snapshot.to_inlay_offset(range.end), - }) - .map(DocumentRange::Inlay) - .collect::>() - } else { - highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start) - ..buffer_snapshot.anchor_after(range.end) - }) - .map(DocumentRange::Text) - .collect::>() - }; - highlights.insert( + text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting text ranges {text_highlight_ranges:?}"); + text_highlights.insert( Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), + Arc::new(( + HighlightStyle::default(), + text_highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .collect(), + )), ); + let mut inlay_highlights = InlayHighlights::default(); + if !inlays.is_empty() { + let inlay_highlight_count = rng.gen_range(0..inlays.len()); + let mut inlay_indices = BTreeSet::default(); + while inlay_indices.len() < inlay_highlight_count { + inlay_indices.insert(rng.gen_range(0..inlays.len())); + } + let new_highlights = inlay_indices + .into_iter() + .filter_map(|i| { + let (_, inlay) = &inlays[i]; + let inlay_text_len = inlay.text.len(); + match inlay_text_len { + 0 => None, + 1 => Some(InlayHighlight { + inlay: inlay.id, + inlay_position: inlay.position, + range: 0..1, + }), + n => { + let inlay_text = inlay.text.to_string(); + let mut highlight_end = rng.gen_range(1..n); + let mut highlight_start = rng.gen_range(0..highlight_end); + while !inlay_text.is_char_boundary(highlight_end) { + highlight_end += 1; + } + while !inlay_text.is_char_boundary(highlight_start) { + highlight_start -= 1; + } + Some(InlayHighlight { + inlay: inlay.id, + inlay_position: inlay.position, + range: highlight_start..highlight_end, + }) + } + } + }) + .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight))) + .collect(); + log::info!("highlighting inlay ranges {new_highlights:?}"); + inlay_highlights.insert(TypeId::of::<()>(), new_highlights); + } + for _ in 0..5 { let mut end = rng.gen_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); let mut start = rng.gen_range(0..=end); start = expected_text.clip_offset(start, Bias::Right); + let range = InlayOffset(start)..InlayOffset(end); + log::info!("calling inlay_snapshot.chunks({range:?})"); let actual_text = inlay_snapshot .chunks( - InlayOffset(start)..InlayOffset(end), + range, false, - Some(&highlights), - None, - None, + Highlights { + text_highlights: Some(&text_highlights), + inlay_highlights: Some(&inlay_highlights), + ..Highlights::default() + }, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 2cf0471b37..6b38ea2d24 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,9 +1,8 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - TextHighlights, + Highlights, }; use crate::MultiBufferSnapshot; -use gpui::fonts::HighlightStyle; use language::{Chunk, Point}; use std::{cmp, mem, num::NonZeroU32, ops::Range}; use sum_tree::Bias; @@ -68,9 +67,7 @@ impl TabMap { 'outer: for chunk in old_snapshot.fold_snapshot.chunks( fold_edit.old.end..old_end_row_successor_offset, false, - None, - None, - None, + Highlights::default(), ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -183,7 +180,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None) + .chunks(range.start..line_end, false, Highlights::default()) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -200,9 +197,7 @@ impl TabSnapshot { .chunks( TabPoint::new(range.end.row(), 0)..range.end, false, - None, - None, - None, + Highlights::default(), ) .flat_map(|chunk| chunk.text.chars()) { @@ -223,9 +218,7 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - text_highlights: Option<&'a TextHighlights>, - hint_highlight_style: Option, - suggestion_highlight_style: Option, + highlights: Highlights<'a>, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); @@ -245,9 +238,7 @@ impl TabSnapshot { fold_chunks: self.fold_snapshot.chunks( input_start..input_end, language_aware, - text_highlights, - hint_highlight_style, - suggestion_highlight_style, + highlights, ), input_column, column: expanded_char_column, @@ -270,9 +261,13 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + TabPoint::zero()..self.max_point(), + false, + Highlights::default(), + ) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -597,9 +592,7 @@ mod tests { .chunks( TabPoint::new(0, ix as u32)..tab_snapshot.max_point(), false, - None, - None, - None, + Highlights::default(), ) .map(|c| c.text) .collect::(), @@ -674,7 +667,8 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default()) + { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -743,7 +737,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, Highlights::default()) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f3600936f9..60337661c1 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,13 +1,11 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - TextHighlights, + Highlights, }; use crate::MultiBufferSnapshot; use gpui::{ - fonts::{FontId, HighlightStyle}, - text_layout::LineWrapper, - AppContext, Entity, ModelContext, ModelHandle, Task, + fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task, }; use language::{Chunk, Point}; use lazy_static::lazy_static; @@ -444,9 +442,7 @@ impl WrapSnapshot { let mut chunks = new_tab_snapshot.chunks( TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), false, - None, - None, - None, + Highlights::default(), ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -575,9 +571,7 @@ impl WrapSnapshot { &'a self, rows: Range, language_aware: bool, - text_highlights: Option<&'a TextHighlights>, - hint_highlight_style: Option, - suggestion_highlight_style: Option, + highlights: Highlights<'a>, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -594,9 +588,7 @@ impl WrapSnapshot { input_chunks: self.tab_snapshot.chunks( input_start..input_end, language_aware, - text_highlights, - hint_highlight_style, - suggestion_highlight_style, + highlights, ), input_chunk: Default::default(), output_position: output_start, @@ -1323,9 +1315,7 @@ mod tests { self.chunks( wrap_row..self.max_point().row() + 1, false, - None, - None, - None, + Highlights::default(), ) .map(|h| h.text) } @@ -1350,7 +1340,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None) + .chunks(start_row..end_row, true, Highlights::default()) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 50a382439a..b36cf9f71d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -66,7 +66,7 @@ use language::{ TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange, + hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState, }; use log::error; @@ -99,6 +99,7 @@ use std::{ time::{Duration, Instant}, }; pub use sum_tree::Bias; +use sum_tree::TreeMap; use text::Rope; use theme::{DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; @@ -548,7 +549,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; -type BackgroundHighlight = (fn(&Theme) -> Color, Vec); +type BackgroundHighlight = (fn(&Theme) -> Color, Vec>); +type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec); pub struct Editor { handle: WeakViewHandle, @@ -580,6 +582,7 @@ pub struct Editor { placeholder_text: Option>, highlighted_rows: Option>, background_highlights: BTreeMap, + inlay_background_highlights: TreeMap, InlayBackgroundHighlight>, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -1523,6 +1526,7 @@ impl Editor { placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), + inlay_background_highlights: Default::default(), nav_history: None, context_menu: None, mouse_context_menu: cx @@ -1734,6 +1738,10 @@ impl Editor { } } + pub fn read_only(&self) -> bool { + self.read_only + } + pub fn set_read_only(&mut self, read_only: bool) { self.read_only = read_only; } @@ -2285,14 +2293,18 @@ impl Editor { // bracket of any of this language's bracket pairs. let mut bracket_pair = None; let mut is_bracket_pair_start = false; - for (pair, enabled) in scope.brackets() { - if enabled && pair.close && pair.start.ends_with(text.as_ref()) { - bracket_pair = Some(pair.clone()); - is_bracket_pair_start = true; - break; - } else if pair.end.as_str() == text.as_ref() { - bracket_pair = Some(pair.clone()); - break; + if !text.is_empty() { + // `text` can be empty when an user is using IME (e.g. Chinese Wubi Simplified) + // and they are removing the character that triggered IME popup. + for (pair, enabled) in scope.brackets() { + if enabled && pair.close && pair.start.ends_with(text.as_ref()) { + bracket_pair = Some(pair.clone()); + is_bracket_pair_start = true; + break; + } else if pair.end.as_str() == text.as_ref() { + bracket_pair = Some(pair.clone()); + break; + } } } @@ -3815,7 +3827,7 @@ impl Editor { enum CodeActions {} Some( MouseEventHandler::new::(0, cx, |state, _| { - Svg::new("icons/bolt_8.svg").with_color( + Svg::new("icons/bolt.svg").with_color( style .code_actions .indicator @@ -5121,9 +5133,6 @@ impl Editor { self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); cx.emit(Event::Edited); - cx.emit(Event::TransactionUndone { - transaction_id: tx_id, - }); } } @@ -7065,16 +7074,8 @@ impl Editor { } else { this.update(&mut cx, |this, cx| { let buffer = this.buffer.read(cx).snapshot(cx); - let display_snapshot = this - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); let mut buffer_highlights = this - .document_highlights_for_position( - selection.head(), - &buffer, - &display_snapshot, - ) - .filter_map(|highlight| highlight.as_text_range()) + .document_highlights_for_position(selection.head(), &buffer) .filter(|highlight| { highlight.start.excerpt_id() == selection.head().excerpt_id() && highlight.end.excerpt_id() == selection.head().excerpt_id() @@ -7129,15 +7130,11 @@ impl Editor { let ranges = this .clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| { - ranges.into_iter().filter_map(|range| range.as_text_range()) - }) + .flat_map(|(_, ranges)| ranges.into_iter()) .chain( this.clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| { - ranges.into_iter().filter_map(|range| range.as_text_range()) - }), + .flat_map(|(_, ranges)| ranges.into_iter()), ) .collect(); @@ -7238,7 +7235,7 @@ impl Editor { Some(Autoscroll::fit()), cx, ); - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); self.show_local_selections = true; if moving_cursor { @@ -7815,29 +7812,20 @@ impl Editor { color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights.insert( - TypeId::of::(), - ( - color_fetcher, - ranges.into_iter().map(DocumentRange::Text).collect(), - ), - ); + self.background_highlights + .insert(TypeId::of::(), (color_fetcher, ranges)); cx.notify(); } pub fn highlight_inlay_background( &mut self, - ranges: Vec, + ranges: Vec, color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights.insert( - TypeId::of::(), - ( - color_fetcher, - ranges.into_iter().map(DocumentRange::Inlay).collect(), - ), - ); + // TODO: no actual highlights happen for inlays currently, find a way to do that + self.inlay_background_highlights + .insert(Some(TypeId::of::()), (color_fetcher, ranges)); cx.notify(); } @@ -7845,15 +7833,18 @@ impl Editor { &mut self, cx: &mut ViewContext, ) -> Option { - let highlights = self.background_highlights.remove(&TypeId::of::()); - if highlights.is_some() { + let text_highlights = self.background_highlights.remove(&TypeId::of::()); + let inlay_highlights = self + .inlay_background_highlights + .remove(&Some(TypeId::of::())); + if text_highlights.is_some() || inlay_highlights.is_some() { cx.notify(); } - highlights + text_highlights } #[cfg(feature = "test-support")] - pub fn all_background_highlights( + pub fn all_text_background_highlights( &mut self, cx: &mut ViewContext, ) -> Vec<(Range, Color)> { @@ -7869,8 +7860,7 @@ impl Editor { &'a self, position: Anchor, buffer: &'a MultiBufferSnapshot, - display_snapshot: &'a DisplaySnapshot, - ) -> impl 'a + Iterator { + ) -> impl 'a + Iterator> { let read_highlights = self .background_highlights .get(&TypeId::of::()) @@ -7879,16 +7869,14 @@ impl Editor { .background_highlights .get(&TypeId::of::()) .map(|h| &h.1); - let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer)); - let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer)); + let left_position = position.bias_left(buffer); + let right_position = position.bias_right(buffer); read_highlights .into_iter() .chain(write_highlights) .flat_map(move |ranges| { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = document_to_inlay_range(probe, display_snapshot) - .end - .cmp(&left_position); + let cmp = probe.end.cmp(&left_position, buffer); if cmp.is_ge() { Ordering::Greater } else { @@ -7899,12 +7887,9 @@ impl Editor { }; let right_position = right_position.clone(); - ranges[start_ix..].iter().take_while(move |range| { - document_to_inlay_range(range, display_snapshot) - .start - .cmp(&right_position) - .is_le() - }) + ranges[start_ix..] + .iter() + .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) }) } @@ -7914,15 +7899,13 @@ impl Editor { display_snapshot: &DisplaySnapshot, theme: &Theme, ) -> Vec<(Range, Color)> { - let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) - ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { - let cmp = document_to_inlay_range(probe, display_snapshot) + let cmp = probe .end - .cmp(&search_range.start); + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); if cmp.is_gt() { Ordering::Greater } else { @@ -7932,13 +7915,16 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - let range = document_to_inlay_range(range, display_snapshot); - if range.start.cmp(&search_range.end).is_ge() { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { break; } - let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); - let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + let start = range.start.to_display_point(&display_snapshot); + let end = range.end.to_display_point(&display_snapshot); results.push((start..end, color)) } } @@ -7951,17 +7937,15 @@ impl Editor { display_snapshot: &DisplaySnapshot, count: usize, ) -> Vec> { - let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) - ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { return vec![]; }; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = document_to_inlay_range(probe, display_snapshot) + let cmp = probe .end - .cmp(&search_range.start); + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); if cmp.is_gt() { Ordering::Greater } else { @@ -7984,22 +7968,20 @@ impl Editor { return Vec::new(); } for range in &ranges[start_ix..] { - let range = document_to_inlay_range(range, display_snapshot); - if range.start.cmp(&search_range.end).is_ge() { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { break; } - let end = display_snapshot - .inlay_offset_to_display_point(range.end, Bias::Right) - .to_point(display_snapshot); + let end = range.end.to_point(&display_snapshot.buffer_snapshot); if let Some(current_row) = &end_row { if end.row == current_row.row { continue; } } - let start = display_snapshot - .inlay_offset_to_display_point(range.start, Bias::Left) - .to_point(display_snapshot); - + let start = range.start.to_point(&display_snapshot.buffer_snapshot); if start_row.is_none() { assert_eq!(end_row, None); start_row = Some(start); @@ -8038,12 +8020,12 @@ impl Editor { pub fn highlight_inlays( &mut self, - ranges: Vec, + highlights: Vec, style: HighlightStyle, cx: &mut ViewContext, ) { self.display_map.update(cx, |map, _| { - map.highlight_inlays(TypeId::of::(), ranges, style) + map.highlight_inlays(TypeId::of::(), highlights, style) }); cx.notify(); } @@ -8051,15 +8033,15 @@ impl Editor { pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, - ) -> Option<(HighlightStyle, &'a [DocumentRange])> { + ) -> Option<(HighlightStyle, &'a [Range])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_text_highlights(&mut self, cx: &mut ViewContext) { - let text_highlights = self + pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + let cleared = self .display_map - .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if text_highlights.is_some() { + .update(cx, |map, _| map.clear_highlights(TypeId::of::())); + if cleared { cx.notify(); } } @@ -8276,7 +8258,6 @@ impl Editor { Some( ranges .iter() - .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) }) @@ -8491,19 +8472,6 @@ impl Editor { } } -fn document_to_inlay_range( - range: &DocumentRange, - snapshot: &DisplaySnapshot, -) -> Range { - match range { - DocumentRange::Text(text_range) => { - snapshot.anchor_to_inlay_offset(text_range.start) - ..snapshot.anchor_to_inlay_offset(text_range.end) - } - DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end, - } -} - fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, @@ -8605,9 +8573,6 @@ pub enum Event { local: bool, autoscroll: bool, }, - TransactionUndone { - transaction_id: TransactionId, - }, Closed, } @@ -8717,7 +8682,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); } false @@ -8786,12 +8751,11 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; - let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } fn unmark_text(&mut self, cx: &mut ViewContext) { - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); self.ime_transaction.take(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b7e34fda53..3390b70530 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1735,7 +1735,7 @@ impl EditorElement { enum JumpIcon {} MouseEventHandler::new::((*id).into(), cx, |state, _| { let style = style.jump_icon.style_for(state); - Svg::new("icons/arrow_up_right_8.svg") + Svg::new("icons/arrow_up_right.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3d5b1d2113..f460b18bce 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,6 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, - link_go_to_definition::{DocumentRange, InlayRange}, + link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; @@ -50,19 +50,18 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC pub struct InlayHover { pub excerpt: ExcerptId, - pub triggered_from: InlayOffset, - pub range: InlayRange, + pub range: InlayHighlight, pub tooltip: HoverBlock, } pub fn find_hovered_hint_part( label_parts: Vec, - hint_range: Range, + hint_start: InlayOffset, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; + if hovered_offset >= hint_start { + let mut hovered_character = (hovered_offset - hint_start).0; + let mut part_start = hint_start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { @@ -88,10 +87,8 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie }; if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..range.highlight_end) - .contains(&inlay_hover.triggered_from) - { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { // Hover triggered from same location as last time. Don't show again. return; } @@ -99,18 +96,6 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie hide_hover(editor, cx); } - let snapshot = editor.snapshot(cx); - // Don't request again if the location is the same as the previous request - if let Some(triggered_from) = editor.hover_state.triggered_from { - if inlay_hover.triggered_from - == snapshot - .display_snapshot - .anchor_to_inlay_offset(triggered_from) - { - return; - } - } - let task = cx.spawn(|this, mut cx| { async move { cx.background() @@ -122,7 +107,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let hover_popover = InfoPopover { project: project.clone(), - symbol_range: DocumentRange::Inlay(inlay_hover.range), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), blocks: vec![inlay_hover.tooltip], language: None, rendered_content: None, @@ -326,7 +311,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: DocumentRange::Text(range), + symbol_range: RangeInEditor::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -608,8 +593,8 @@ impl HoverState { self.info_popover .as_ref() .map(|info_popover| match &info_popover.symbol_range { - DocumentRange::Text(range) => &range.start, - DocumentRange::Inlay(range) => &range.inlay_position, + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -635,7 +620,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - symbol_range: DocumentRange, + symbol_range: RangeInEditor, pub blocks: Vec, language: Option>, rendered_content: Option, @@ -811,6 +796,7 @@ mod tests { inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, link_go_to_definition::update_inlay_link_and_hover_points, test::editor_lsp_test_context::EditorLspTestContext, + InlayId, }; use collections::BTreeSet; use gpui::fonts::Weight; @@ -1477,25 +1463,16 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let entire_inlay_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - - let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); assert_eq!( popover.symbol_range, - DocumentRange::Inlay(InlayRange { + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_new_type_label_start, - highlight_end: InlayOffset( - expected_new_type_label_start.0 + new_type_label.len() - ), + range: ": ".len()..": ".len() + new_type_label.len(), }), "Popover range should match the new type label part" ); @@ -1543,23 +1520,17 @@ mod tests { .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); let hover_state = &editor.hover_state; assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); let popover = hover_state.info_popover.as_ref().unwrap(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let entire_inlay_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - let expected_struct_label_start = - InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); assert_eq!( popover.symbol_range, - DocumentRange::Inlay(InlayRange { + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_struct_label_start, - highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), }), "Popover range should match the struct label part" ); diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 34898aea2e..8aa7a1e40e 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -43,7 +43,8 @@ pub struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: u64, - hints: Vec<(InlayId, InlayHint)>, + ordered_hints: Vec, + hints_by_id: HashMap, } #[derive(Debug, Clone, Copy)] @@ -316,7 +317,7 @@ impl InlayHintCache { self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); if !retain { - invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); + invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied()); } retain }); @@ -384,7 +385,7 @@ impl InlayHintCache { let shown_excerpt_hints_to_remove = shown_hints_to_remove.entry(*excerpt_id).or_default(); let excerpt_cached_hints = excerpt_cached_hints.read(); - let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable(); + let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id @@ -395,7 +396,8 @@ impl InlayHintCache { let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { - Some((cached_hint_id, cached_hint)) => { + Some(&cached_hint_id) => { + let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; if cached_hint_id == shown_hint_id { excerpt_cache.next(); return !new_kinds.contains(&cached_hint.kind); @@ -428,7 +430,8 @@ impl InlayHintCache { } }); - for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache { + for cached_hint_id in excerpt_cache { + let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; let cached_hint_kind = maybe_missed_cached_hint.kind; if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { to_insert.push(Inlay::hint( @@ -463,7 +466,7 @@ impl InlayHintCache { self.update_tasks.remove(&excerpt_to_remove); if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) { let cached_hints = cached_hints.read(); - to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); + to_remove.extend(cached_hints.ordered_hints.iter().copied()); } } if to_remove.is_empty() { @@ -489,10 +492,8 @@ impl InlayHintCache { self.hints .get(&excerpt_id)? .read() - .hints - .iter() - .find(|&(id, _)| id == &hint_id) - .map(|(_, hint)| hint) + .hints_by_id + .get(&hint_id) .cloned() } @@ -500,7 +501,13 @@ impl InlayHintCache { let mut hints = Vec::new(); for excerpt_hints in self.hints.values() { let excerpt_hints = excerpt_hints.read(); - hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned()); + hints.extend( + excerpt_hints + .ordered_hints + .iter() + .map(|id| &excerpt_hints.hints_by_id[id]) + .cloned(), + ); } hints } @@ -518,12 +525,7 @@ impl InlayHintCache { ) { if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard - .hints - .iter_mut() - .find(|(hint_id, _)| hint_id == &id) - .map(|(_, hint)| hint) - { + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { let hint_to_resolve = cached_hint.clone(); let server_id = *server_id; @@ -555,12 +557,7 @@ impl InlayHintCache { editor.inlay_hint_cache.hints.get(&excerpt_id) { let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard - .hints - .iter_mut() - .find(|(hint_id, _)| hint_id == &id) - .map(|(_, hint)| hint) - { + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { if cached_hint.resolve_state == ResolveState::Resolving { resolved_hint.resolve_state = ResolveState::Resolved; *cached_hint = resolved_hint; @@ -986,12 +983,17 @@ fn calculate_hint_updates( let missing_from_cache = match &cached_excerpt_hints { Some(cached_excerpt_hints) => { let cached_excerpt_hints = cached_excerpt_hints.read(); - match cached_excerpt_hints.hints.binary_search_by(|probe| { - probe.1.position.cmp(&new_hint.position, buffer_snapshot) - }) { + match cached_excerpt_hints + .ordered_hints + .binary_search_by(|probe| { + cached_excerpt_hints.hints_by_id[probe] + .position + .cmp(&new_hint.position, buffer_snapshot) + }) { Ok(ix) => { let mut missing_from_cache = true; - for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] { + for id in &cached_excerpt_hints.ordered_hints[ix..] { + let cached_hint = &cached_excerpt_hints.hints_by_id[id]; if new_hint .position .cmp(&cached_hint.position, buffer_snapshot) @@ -1000,7 +1002,7 @@ fn calculate_hint_updates( break; } if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + excerpt_hints_to_persist.insert(*id, cached_hint.kind); missing_from_cache = false; } } @@ -1031,12 +1033,12 @@ fn calculate_hint_updates( let cached_excerpt_hints = cached_excerpt_hints.read(); remove_from_cache.extend( cached_excerpt_hints - .hints + .ordered_hints .iter() - .filter(|(cached_inlay_id, _)| { + .filter(|cached_inlay_id| { !excerpt_hints_to_persist.contains_key(cached_inlay_id) }) - .map(|(cached_inlay_id, _)| *cached_inlay_id), + .copied(), ); } } @@ -1080,7 +1082,8 @@ fn apply_hint_update( version: query.cache_version, buffer_version: buffer_snapshot.version().clone(), buffer_id: query.buffer_id, - hints: Vec::new(), + ordered_hints: Vec::new(), + hints_by_id: HashMap::default(), })) }); let mut cached_excerpt_hints = cached_excerpt_hints.write(); @@ -1093,20 +1096,27 @@ fn apply_hint_update( let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); cached_excerpt_hints - .hints - .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); + .ordered_hints + .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); + cached_excerpt_hints + .hints_by_id + .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); let mut splice = InlaySplice { to_remove: new_update.remove_from_visible, to_insert: Vec::new(), }; for new_hint in new_update.add_to_cache { - let cached_hints = &mut cached_excerpt_hints.hints; - let insert_position = match cached_hints - .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) - { + let insert_position = match cached_excerpt_hints + .ordered_hints + .binary_search_by(|probe| { + cached_excerpt_hints.hints_by_id[probe] + .position + .cmp(&new_hint.position, &buffer_snapshot) + }) { Ok(i) => { let mut insert_position = Some(i); - for (_, cached_hint) in &cached_hints[i..] { + for id in &cached_excerpt_hints.ordered_hints[i..] { + let cached_hint = &cached_excerpt_hints.hints_by_id[id]; if new_hint .position .cmp(&cached_hint.position, &buffer_snapshot) @@ -1137,7 +1147,11 @@ fn apply_hint_update( .to_insert .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); } - cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + let new_id = InlayId::Hint(new_inlay_id); + cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); + cached_excerpt_hints + .ordered_hints + .insert(insert_position, new_id); cached_inlays_changed = true; } } @@ -1157,7 +1171,7 @@ fn apply_hint_update( outdated_excerpt_caches.insert(*excerpt_id); splice .to_remove - .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); + .extend(excerpt_hints.ordered_hints.iter().copied()); } } cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); @@ -3311,8 +3325,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" pub fn cached_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - for (_, inlay) in &excerpt_hints.read().hints { - labels.push(inlay.text()); + let excerpt_hints = excerpt_hints.read(); + for id in &excerpt_hints.ordered_hints { + labels.push(excerpt_hints.hints_by_id[id].text()); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d999872592..b31c9dcd1b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -16,7 +16,7 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, SelectionGoal, }; -use project::{FormatTrigger, Item as _, Project, ProjectPath}; +use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view}; use smallvec::SmallVec; use std::{ @@ -26,6 +26,7 @@ use std::{ iter, ops::Range, path::{Path, PathBuf}, + sync::Arc, }; use text::Selection; use util::{ @@ -978,7 +979,26 @@ impl SearchableItem for Editor { } self.change_selections(None, cx, |s| s.select_ranges(ranges)); } + fn replace( + &mut self, + identifier: &Self::Match, + query: &SearchQuery, + cx: &mut ViewContext, + ) { + let text = self.buffer.read(cx); + let text = text.snapshot(cx); + let text = text.text_for_range(identifier.clone()).collect::>(); + let text: Cow<_> = if text.len() == 1 { + text.first().cloned().unwrap().into() + } else { + let joined_chunks = text.join(""); + joined_chunks.into() + }; + if let Some(replacement) = query.replacement(&text) { + self.edit([(identifier.clone(), Arc::from(&*replacement))], cx); + } + } fn match_index_for_direction( &mut self, matches: &Vec>, @@ -1030,7 +1050,7 @@ impl SearchableItem for Editor { fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>> { let buffer = self.buffer().read(cx).snapshot(cx); diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 1f9a3aab73..7da0b88622 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,8 +1,8 @@ use crate::{ - display_map::{DisplaySnapshot, InlayOffset}, + display_map::DisplaySnapshot, element::PointForPosition, hover_popover::{self, InlayHover}, - Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, + Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; @@ -17,44 +17,19 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, - pub symbol_range: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, } -#[derive(Debug)] -pub enum GoToDefinitionTrigger { - Text(DisplayPoint), - InlayHint(InlayRange, lsp::Location, LanguageServerId), -} - -#[derive(Debug, Clone)] -pub enum GoToDefinitionLink { - Text(LocationLink), - InlayHint(lsp::Location, LanguageServerId), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct InlayRange { - pub inlay_position: Anchor, - pub highlight_start: InlayOffset, - pub highlight_end: InlayOffset, -} - -#[derive(Debug, Clone)] -pub enum TriggerPoint { - Text(Anchor), - InlayHint(InlayRange, lsp::Location, LanguageServerId), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DocumentRange { +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum RangeInEditor { Text(Range), - Inlay(InlayRange), + Inlay(InlayHighlight), } -impl DocumentRange { +impl RangeInEditor { pub fn as_text_range(&self) -> Option> { match self { Self::Text(range) => Some(range.clone()), @@ -64,28 +39,47 @@ impl DocumentRange { fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { - (DocumentRange::Text(range), TriggerPoint::Text(point)) => { + (Self::Text(range), TriggerPoint::Text(point)) => { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => { - range.highlight_start.cmp(&point.highlight_end).is_le() - && range.highlight_end.cmp(&point.highlight_end).is_ge() + (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => { + highlight.inlay == point.inlay + && highlight.range.contains(&point.range.start) + && highlight.range.contains(&point.range.end) } - (DocumentRange::Inlay(_), TriggerPoint::Text(_)) - | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, + (Self::Inlay(_), TriggerPoint::Text(_)) + | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, } } } -impl TriggerPoint { - fn anchor(&self) -> &Anchor { - match self { - TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(range, _, _) => &range.inlay_position, - } - } +#[derive(Debug)] +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(InlayHighlight, lsp::Location, LanguageServerId), +} +#[derive(Debug, Clone)] +pub enum GoToDefinitionLink { + Text(LocationLink), + InlayHint(lsp::Location, LanguageServerId), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InlayHighlight { + pub inlay: InlayId, + pub inlay_position: Anchor, + pub range: Range, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(InlayHighlight, lsp::Location, LanguageServerId), +} + +impl TriggerPoint { pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { TriggerPoint::Text(_) => { @@ -98,6 +92,13 @@ impl TriggerPoint { TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type, } } + + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position, + } + } } pub fn update_go_to_definition_link( @@ -135,11 +136,7 @@ pub fn update_go_to_definition_link( } } (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => { - if range_a - .inlay_position - .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot) - .is_eq() - { + if range_a == range_b { return; } } @@ -173,10 +170,6 @@ pub fn update_inlay_link_and_hover_points( shift_held: bool, cx: &mut ViewContext<'_, '_, Editor>, ) { - let hint_start_offset = - snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = - snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) } else { @@ -224,15 +217,14 @@ pub fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { - let mut actual_hint_start = hint_start_offset; - let mut actual_hint_end = hint_end_offset; + let mut extra_shift_left = 0; + let mut extra_shift_right = 0; if cached_hint.padding_left { - actual_hint_start.0 += 1; - actual_hint_end.0 += 1; + extra_shift_left += 1; + extra_shift_right += 1; } if cached_hint.padding_right { - actual_hint_start.0 += 1; - actual_hint_end.0 += 1; + extra_shift_right += 1; } match cached_hint.label { project::InlayHintLabel::String(_) => { @@ -253,11 +245,11 @@ pub fn update_inlay_link_and_hover_points( } } }, - triggered_from: hovered_offset, - range: InlayRange { + range: InlayHighlight { + inlay: hovered_hint.id, inlay_position: hovered_hint.position, - highlight_start: actual_hint_start, - highlight_end: actual_hint_end, + range: extra_shift_left + ..hovered_hint.text.len() + extra_shift_right, }, }, cx, @@ -266,13 +258,24 @@ pub fn update_inlay_link_and_hover_points( } } project::InlayHintLabel::LabelParts(label_parts) => { + let hint_start = + snapshot.anchor_to_inlay_offset(hovered_hint.position); if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, - actual_hint_start..actual_hint_end, + hint_start, hovered_offset, ) { + let highlight_start = + (part_range.start - hint_start).0 + extra_shift_left; + let highlight_end = + (part_range.end - hint_start).0 + extra_shift_right; + let highlight = InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: highlight_start..highlight_end, + }; if let Some(tooltip) = hovered_hint_part.tooltip { hover_popover::hover_at_inlay( editor, @@ -292,12 +295,7 @@ pub fn update_inlay_link_and_hover_points( kind: content.kind, }, }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, + range: highlight.clone(), }, cx, ); @@ -310,11 +308,7 @@ pub fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, Some(GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, + highlight, location, language_server_id, )), @@ -425,7 +419,7 @@ pub fn show_link_definition( let end = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - DocumentRange::Text(start..end) + RangeInEditor::Text(start..end) }) }), definition_result @@ -435,8 +429,8 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some(( - Some(DocumentRange::Inlay(*trigger_source)), + TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some(( + Some(RangeInEditor::Inlay(highlight.clone())), vec![GoToDefinitionLink::InlayHint( lsp_location.clone(), *server_id, @@ -446,7 +440,7 @@ pub fn show_link_definition( this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_text_highlights::(cx); + this.clear_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -498,26 +492,26 @@ pub fn show_link_definition( // If no symbol range returned from language server, use the surrounding word. let (offset_range, _) = snapshot.surrounding_word(*trigger_anchor); - DocumentRange::Text( + RangeInEditor::Text( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_coordinates, _, _) => { - DocumentRange::Inlay(*inlay_coordinates) + TriggerPoint::InlayHint(highlight, _, _) => { + RangeInEditor::Inlay(highlight.clone()) } }); match highlight_range { - DocumentRange::Text(text_range) => this + RangeInEditor::Text(text_range) => this .highlight_text::( vec![text_range], style, cx, ), - DocumentRange::Inlay(inlay_coordinates) => this + RangeInEditor::Inlay(highlight) => this .highlight_inlays::( - vec![inlay_coordinates], + vec![highlight], style, cx, ), @@ -547,7 +541,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( @@ -1199,30 +1193,19 @@ mod tests { cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); - let actual_ranges = snapshot - .highlight_ranges::() - .map(|ranges| ranges.as_ref().clone().1) - .unwrap_or_default() + let actual_highlights = snapshot + .inlay_highlights::() .into_iter() - .map(|range| match range { - DocumentRange::Text(range) => { - panic!("Unexpected regular text selection range {range:?}") - } - DocumentRange::Inlay(inlay_range) => inlay_range, - }) + .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight)) .collect::>(); let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let expected_highlight_start = snapshot.display_point_to_inlay_offset( - inlay_range.start.to_display_point(&snapshot), - Bias::Left, - ); - let expected_ranges = vec![InlayRange { + let expected_highlight = InlayHighlight { + inlay: InlayId::Hint(0), inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), - highlight_start: expected_highlight_start, - highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()), - }]; - assert_set_eq!(actual_ranges, expected_ranges); + range: 0..hint_label.len(), + }; + assert_set_eq!(actual_highlights, vec![&expected_highlight]); }); // Unpress cmd causes highlight to go away @@ -1242,17 +1225,9 @@ mod tests { cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); let actual_ranges = snapshot - .highlight_ranges::() + .text_highlight_ranges::() .map(|ranges| ranges.as_ref().clone().1) - .unwrap_or_default() - .into_iter() - .map(|range| match range { - DocumentRange::Text(range) => { - panic!("Unexpected regular text selection range {range:?}") - } - DocumentRange::Inlay(inlay_range) => inlay_range, - }) - .collect::>(); + .unwrap_or_default(); assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); }); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 74283fd778..c5d17dfd2e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -70,6 +70,9 @@ pub enum Event { Edited { sigleton_buffer_edited: bool, }, + TransactionUndone { + transaction_id: TransactionId, + }, Reloaded, DiffBaseChanged, LanguageChanged, @@ -771,30 +774,36 @@ impl MultiBuffer { } pub fn undo(&mut self, cx: &mut ModelContext) -> Option { + let mut transaction_id = None; if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.undo(cx)); - } + transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); + } else { + while let Some(transaction) = self.history.pop_undo() { + let mut undone = false; + for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + undone |= buffer.update(cx, |buffer, cx| { + let undo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_undo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.undo_to_transaction(undo_to, cx) + }); + } + } - while let Some(transaction) = self.history.pop_undo() { - let mut undone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { - undone |= buffer.update(cx, |buffer, cx| { - let undo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_undo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.undo_to_transaction(undo_to, cx) - }); + if undone { + transaction_id = Some(transaction.id); + break; } } - - if undone { - return Some(transaction.id); - } } - None + if let Some(transaction_id) = transaction_id { + cx.emit(Event::TransactionUndone { transaction_id }); + } + + transaction_id } pub fn redo(&mut self, cx: &mut ModelContext) -> Option { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 033525395e..0bae32f1f7 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -225,7 +225,6 @@ impl<'a> EditorTestContext<'a> { .map(|h| h.1.clone()) .unwrap_or_default() .into_iter() - .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); @@ -237,11 +236,10 @@ impl<'a> EditorTestContext<'a> { let expected_ranges = self.ranges(marked_text); let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); let actual_ranges: Vec> = snapshot - .highlight_ranges::() + .text_highlight_ranges::() .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() - .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); assert_set_eq!(actual_ranges, expected_ranges); diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index a717223f6d..b3a06b471e 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -13,7 +13,7 @@ use gpui::{ use isahc::Request; use language::Buffer; use postage::prelude::Stream; -use project::Project; +use project::{search::SearchQuery, Project}; use regex::Regex; use serde::Serialize; use smallvec::SmallVec; @@ -276,7 +276,7 @@ impl Item for FeedbackEditor { ) -> AnyElement { Flex::row() .with_child( - Svg::new("icons/feedback_16.svg") + Svg::new("icons/feedback.svg") .with_color(style.label.text.color) .constrained() .with_width(style.type_icon_width) @@ -418,10 +418,13 @@ impl SearchableItem for FeedbackEditor { self.editor .update(cx, |e, cx| e.select_matches(matches, cx)) } - + fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.replace(matches, query, cx)); + } fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task> { self.editor diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 705feb351d..95b7ccb559 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -67,6 +67,7 @@ dhat = "0.3" env_logger.workspace = true png = "0.16" simplelog = "0.9" +util = { path = "../util", features = ["test-support"] } [target.'cfg(target_os = "macos")'.dependencies] media = { path = "../media" } diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index 8cb7d30dfe..89dd7bfc7d 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -75,7 +75,6 @@ impl KeymapMatcher { keystroke: Keystroke, mut dispatch_path: Vec<(usize, KeymapContext)>, ) -> MatchResult { - let mut any_pending = false; // Collect matched bindings into an ordered list using the position in the matching binding first, // and then the order the binding matched in the view tree second. // The key is the reverse position of the binding in the bindings list so that later bindings @@ -84,7 +83,8 @@ impl KeymapMatcher { let no_action_id = (NoAction {}).id(); let first_keystroke = self.pending_keystrokes.is_empty(); - self.pending_keystrokes.push(keystroke.clone()); + let mut pending_key = None; + let mut previous_keystrokes = self.pending_keystrokes.clone(); self.contexts.clear(); self.contexts @@ -106,24 +106,32 @@ impl KeymapMatcher { } for binding in self.keymap.bindings().iter().rev() { - match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..]) - { - BindingMatchResult::Complete(action) => { - if action.id() != no_action_id { - matched_bindings.push((*view_id, action)); + for possibility in keystroke.match_possibilities() { + previous_keystrokes.push(possibility.clone()); + match binding.match_keys_and_context(&previous_keystrokes, &self.contexts[i..]) + { + BindingMatchResult::Complete(action) => { + if action.id() != no_action_id { + matched_bindings.push((*view_id, action)); + } } + BindingMatchResult::Partial => { + if pending_key == None || pending_key == Some(possibility.clone()) { + self.pending_views + .insert(*view_id, self.contexts[i].clone()); + pending_key = Some(possibility) + } + } + _ => {} } - BindingMatchResult::Partial => { - self.pending_views - .insert(*view_id, self.contexts[i].clone()); - any_pending = true; - } - _ => {} + previous_keystrokes.pop(); } } } - if !any_pending { + if pending_key.is_some() { + self.pending_keystrokes.push(pending_key.unwrap()); + } else { self.clear_pending(); } @@ -131,7 +139,7 @@ impl KeymapMatcher { // Collect the sorted matched bindings into the final vec for ease of use // Matched bindings are in order by precedence MatchResult::Matches(matched_bindings) - } else if any_pending { + } else if !self.pending_keystrokes.is_empty() { MatchResult::Pending } else { MatchResult::None @@ -340,6 +348,7 @@ mod tests { shift: false, cmd: false, function: false, + ime_key: None, } ); @@ -352,6 +361,7 @@ mod tests { shift: true, cmd: false, function: false, + ime_key: None, } ); @@ -364,6 +374,7 @@ mod tests { shift: true, cmd: true, function: false, + ime_key: None, } ); @@ -466,7 +477,7 @@ mod tests { #[derive(Clone, Deserialize, PartialEq, Eq, Debug)] pub struct A(pub String); impl_actions!(test, [A]); - actions!(test, [B, Ab]); + actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]); #[derive(Clone, Debug, Eq, PartialEq)] struct ActionArg { @@ -477,6 +488,10 @@ mod tests { Binding::new("a", A("x".to_string()), Some("a")), Binding::new("b", B, Some("a")), Binding::new("a b", Ab, Some("a || b")), + Binding::new("$", Dollar, Some("a")), + Binding::new("\"", Quote, Some("a")), + Binding::new("alt-s", Ess, Some("a")), + Binding::new("ctrl-`", Backtick, Some("a")), ]); let mut context_a = KeymapContext::default(); @@ -543,6 +558,30 @@ mod tests { MatchResult::Matches(vec![(1, Box::new(Ab))]) ); + // handle Czech $ (option + 4 key) + assert_eq!( + matcher.push_keystroke(Keystroke::parse("alt-ç->$")?, vec![(1, context_a.clone())]), + MatchResult::Matches(vec![(1, Box::new(Dollar))]) + ); + + // handle Brazillian quote (quote key then space key) + assert_eq!( + matcher.push_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]), + MatchResult::Matches(vec![(1, Box::new(Quote))]) + ); + + // handle ctrl+` on a brazillian keyboard + assert_eq!( + matcher.push_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]), + MatchResult::Matches(vec![(1, Box::new(Backtick))]) + ); + + // handle alt-s on a US keyboard + assert_eq!( + matcher.push_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]), + MatchResult::Matches(vec![(1, Box::new(Ess))]) + ); + Ok(()) } } diff --git a/crates/gpui/src/keymap_matcher/keymap.rs b/crates/gpui/src/keymap_matcher/keymap.rs index 7cb95cab3a..6abbf1016f 100644 --- a/crates/gpui/src/keymap_matcher/keymap.rs +++ b/crates/gpui/src/keymap_matcher/keymap.rs @@ -162,7 +162,8 @@ mod tests { shift: false, cmd: false, function: false, - key: "q".to_string() + key: "q".to_string(), + ime_key: None, }], "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap" ); @@ -179,7 +180,8 @@ mod tests { shift: false, cmd: false, function: false, - key: "w".to_string() + key: "w".to_string(), + ime_key: None, }, &Keystroke { ctrl: true, @@ -187,7 +189,8 @@ mod tests { shift: false, cmd: false, function: false, - key: "w".to_string() + key: "w".to_string(), + ime_key: None, } ], "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap" @@ -339,7 +342,8 @@ mod tests { shift: false, cmd: false, function: false, - key: expected_key.to_string() + key: expected_key.to_string(), + ime_key: None, }], "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}" ); diff --git a/crates/gpui/src/keymap_matcher/keystroke.rs b/crates/gpui/src/keymap_matcher/keystroke.rs index 164dea8aba..bc0caddac9 100644 --- a/crates/gpui/src/keymap_matcher/keystroke.rs +++ b/crates/gpui/src/keymap_matcher/keystroke.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use anyhow::anyhow; use serde::Deserialize; +use smallvec::SmallVec; #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { @@ -10,10 +11,47 @@ pub struct Keystroke { pub shift: bool, pub cmd: bool, pub function: bool, + /// key is the character printed on the key that was pressed + /// e.g. for option-s, key is "s" pub key: String, + /// ime_key is the character inserted by the IME engine when that key was pressed. + /// e.g. for option-s, ime_key is "ß" + pub ime_key: Option, } impl Keystroke { + // When matching a key we cannot know whether the user intended to type + // the ime_key or the key. On some non-US keyboards keys we use in our + // bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), + // and on some keyboards the IME handler converts a sequence of keys into a + // specific character (for example `"` is typed as `" space` on a brazillian keyboard). + pub fn match_possibilities(&self) -> SmallVec<[Keystroke; 2]> { + let mut possibilities = SmallVec::new(); + match self.ime_key.as_ref() { + None => possibilities.push(self.clone()), + Some(ime_key) => { + possibilities.push(Keystroke { + ctrl: self.ctrl, + alt: false, + shift: false, + cmd: false, + function: false, + key: ime_key.to_string(), + ime_key: None, + }); + possibilities.push(Keystroke { + ime_key: None, + ..self.clone() + }); + } + } + possibilities + } + + /// key syntax is: + /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] + /// ime_key is only used for generating test events, + /// when matching a key with an ime_key set will be matched without it. pub fn parse(source: &str) -> anyhow::Result { let mut ctrl = false; let mut alt = false; @@ -21,6 +59,7 @@ impl Keystroke { let mut cmd = false; let mut function = false; let mut key = None; + let mut ime_key = None; let mut components = source.split('-').peekable(); while let Some(component) = components.next() { @@ -31,10 +70,14 @@ impl Keystroke { "cmd" => cmd = true, "fn" => function = true, _ => { - if let Some(component) = components.peek() { - if component.is_empty() && source.ends_with('-') { + if let Some(next) = components.peek() { + if next.is_empty() && source.ends_with('-') { key = Some(String::from("-")); break; + } else if next.len() > 1 && next.starts_with('>') { + key = Some(String::from(component)); + ime_key = Some(String::from(&next[1..])); + components.next(); } else { return Err(anyhow!("Invalid keystroke `{}`", source)); } @@ -54,6 +97,7 @@ impl Keystroke { cmd, function, key, + ime_key, }) } diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 7710834f53..c19d7abe4f 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -327,6 +327,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { cmd, function, key, + ime_key: None, } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index ba8c7a6963..ad8275f0ac 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -285,6 +285,7 @@ enum ImeState { None, } +#[derive(Debug)] struct InsertText { replacement_range: Option>, text: String, @@ -1006,40 +1007,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: .flatten() .is_some(); if !is_composing { - // if the IME has changed the key, we'll first emit an event with the character - // generated by the IME system; then fallback to the keystroke if that is not - // handled. - // cases that we have working: - // - " on a brazillian layout by typing - // - ctrl-` on a brazillian layout by typing - // - $ on a czech QWERTY layout by typing - // - 4 on a czech QWERTY layout by typing - // - ctrl-4 on a czech QWERTY layout by typing (or ) - if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) { - let event_with_ime_text = KeyDownEvent { - is_held: false, - keystroke: Keystroke { - // we match ctrl because some use-cases need it. - // we don't match alt because it's often used to generate the optional character - // we don't match shift because we're not here with letters (usually) - // we don't match cmd/fn because they don't seem to use IME - ctrl: event.keystroke.ctrl, - alt: false, - shift: false, - cmd: false, - function: false, - key: ime_text.clone().unwrap(), - }, - }; - handled = callback(Event::KeyDown(event_with_ime_text)); - } - if !handled { - // empty key happens when you type a deadkey in input composition. - // (e.g. on a brazillian keyboard typing quote is a deadkey) - if !event.keystroke.key.is_empty() { - handled = callback(Event::KeyDown(event)); - } - } + handled = callback(Event::KeyDown(event)); } if !handled { @@ -1197,6 +1165,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { shift: false, function: false, key: ".".into(), + ime_key: None, }; let event = Event::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), @@ -1479,6 +1448,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS replacement_range, text: text.to_string(), }); + if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key { + pending_key_down.0.keystroke.ime_key = Some(text.to_string()); + } window_state.borrow_mut().pending_key_down = Some(pending_key_down); } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2193b5c07e..07bea434e0 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -13,7 +13,7 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet}; use futures::{ - channel::oneshot, + channel::{mpsc, oneshot}, future::{BoxFuture, Shared}, FutureExt, TryFutureExt as _, }; @@ -48,9 +48,6 @@ use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; -#[cfg(any(test, feature = "test-support"))] -use futures::channel::mpsc; - pub use buffer::Operation; pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; @@ -64,6 +61,27 @@ pub fn init(cx: &mut AppContext) { language_settings::init(cx); } +#[derive(Clone, Default)] +struct LspBinaryStatusSender { + txs: Arc, LanguageServerBinaryStatus)>>>>, +} + +impl LspBinaryStatusSender { + fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc, LanguageServerBinaryStatus)> { + let (tx, rx) = mpsc::unbounded(); + self.txs.lock().push(tx); + rx + } + + fn send(&self, language: Arc, status: LanguageServerBinaryStatus) { + let mut txs = self.txs.lock(); + txs.retain(|tx| { + tx.unbounded_send((language.clone(), status.clone())) + .is_ok() + }); + } +} + thread_local! { static PARSER: RefCell = RefCell::new(Parser::new()); } @@ -594,14 +612,13 @@ struct AvailableLanguage { pub struct LanguageRegistry { state: RwLock, language_server_download_dir: Option>, - lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, - lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, login_shell_env_loaded: Shared>, #[allow(clippy::type_complexity)] lsp_binary_paths: Mutex< HashMap>>>>, >, executor: Option>, + lsp_binary_status_tx: LspBinaryStatusSender, } struct LanguageRegistryState { @@ -624,7 +641,6 @@ pub struct PendingLanguageServer { impl LanguageRegistry { pub fn new(login_shell_env_loaded: Task<()>) -> Self { - let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); Self { state: RwLock::new(LanguageRegistryState { next_language_server_id: 0, @@ -638,11 +654,10 @@ impl LanguageRegistry { reload_count: 0, }), language_server_download_dir: None, - lsp_binary_statuses_tx, - lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), lsp_binary_paths: Default::default(), executor: None, + lsp_binary_status_tx: Default::default(), } } @@ -918,8 +933,8 @@ impl LanguageRegistry { let container_dir: Arc = Arc::from(download_dir.join(adapter.name.0.as_ref())); let root_path = root_path.clone(); let adapter = adapter.clone(); - let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); + let lsp_binary_statuses = self.lsp_binary_status_tx.clone(); let task = { let container_dir = container_dir.clone(); @@ -976,8 +991,8 @@ impl LanguageRegistry { pub fn language_server_binary_statuses( &self, - ) -> async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)> { - self.lsp_binary_statuses_rx.clone() + ) -> mpsc::UnboundedReceiver<(Arc, LanguageServerBinaryStatus)> { + self.lsp_binary_status_tx.subscribe() } pub fn delete_server_container( @@ -1054,7 +1069,7 @@ async fn get_binary( language: Arc, delegate: Arc, container_dir: Arc, - statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + statuses: LspBinaryStatusSender, mut cx: AsyncAppContext, ) -> Result { if !container_dir.exists() { @@ -1081,19 +1096,15 @@ async fn get_binary( .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) .await { - statuses - .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) - .await?; + statuses.send(language.clone(), LanguageServerBinaryStatus::Cached); return Ok(binary); } else { - statuses - .broadcast(( - language.clone(), - LanguageServerBinaryStatus::Failed { - error: format!("{:?}", error), - }, - )) - .await?; + statuses.send( + language.clone(), + LanguageServerBinaryStatus::Failed { + error: format!("{:?}", error), + }, + ); } } @@ -1105,27 +1116,21 @@ async fn fetch_latest_binary( language: Arc, delegate: &dyn LspAdapterDelegate, container_dir: &Path, - lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + lsp_binary_statuses_tx: LspBinaryStatusSender, ) -> Result { let container_dir: Arc = container_dir.into(); - lsp_binary_statuses_tx - .broadcast(( - language.clone(), - LanguageServerBinaryStatus::CheckingForUpdate, - )) - .await?; + lsp_binary_statuses_tx.send( + language.clone(), + LanguageServerBinaryStatus::CheckingForUpdate, + ); let version_info = adapter.fetch_latest_server_version(delegate).await?; - lsp_binary_statuses_tx - .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) - .await?; + lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading); let binary = adapter .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate) .await?; - lsp_binary_statuses_tx - .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) - .await?; + lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded); Ok(binary) } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index a918e3d151..587e6ed25a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Buffer, LanguageServerId, LanguageServerName}; use lsp::IoKind; -use project::{Project, Worktree}; +use project::{search::SearchQuery, Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; use workspace::{ @@ -524,12 +524,24 @@ impl SearchableItem for LspLogView { fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> gpui::Task> { self.editor.update(cx, |e, cx| e.find_matches(query, cx)) } + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Since LSP Log is read-only, it doesn't make sense to support replace operation. + } + fn supported_options() -> workspace::searchable::SearchOptions { + workspace::searchable::SearchOptions { + case: true, + word: true, + regex: true, + // LSP log is read-only. + replacement: false, + } + } fn active_match_index( &mut self, matches: Vec, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0690cc9188..b4e698e08a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -912,7 +912,6 @@ impl Project { self.user_store.clone() } - #[cfg(any(test, feature = "test-support"))] pub fn opened_buffers(&self, cx: &AppContext) -> Vec> { self.opened_buffers .values() diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 6c53d2e934..bf81158701 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -7,6 +7,7 @@ use language::{char_kind, BufferSnapshot}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; use std::{ + borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, path::{Path, PathBuf}, @@ -35,6 +36,7 @@ impl SearchInputs { pub enum SearchQuery { Text { search: Arc>, + replacement: Option, whole_word: bool, case_sensitive: bool, inner: SearchInputs, @@ -42,7 +44,7 @@ pub enum SearchQuery { Regex { regex: Regex, - + replacement: Option, multiline: bool, whole_word: bool, case_sensitive: bool, @@ -95,6 +97,7 @@ impl SearchQuery { }; Self::Text { search: Arc::new(search), + replacement: None, whole_word, case_sensitive, inner, @@ -130,6 +133,7 @@ impl SearchQuery { }; Ok(Self::Regex { regex, + replacement: None, multiline, whole_word, case_sensitive, @@ -156,7 +160,21 @@ impl SearchQuery { )) } } - + pub fn with_replacement(mut self, new_replacement: Option) -> Self { + match self { + Self::Text { + ref mut replacement, + .. + } + | Self::Regex { + ref mut replacement, + .. + } => { + *replacement = new_replacement; + self + } + } + } pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { proto::SearchProject { project_id, @@ -214,7 +232,20 @@ impl SearchQuery { } } } - + pub fn replacement<'a>(&self, text: &'a str) -> Option> { + match self { + SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from), + SearchQuery::Regex { + regex, replacement, .. + } => { + if let Some(replacement) = replacement { + Some(regex.replace(text, replacement)) + } else { + None + } + } + } + } pub async fn search( &self, buffer: &BufferSnapshot, diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index b3d9784f1f..8a40325203 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -94,7 +94,7 @@ impl View for QuickActionBar { bar.add_child(render_quick_action_bar_button( 2, - "icons/radix/magic-wand.svg", + "icons/magic-wand.svg", false, ("Inline Assist".into(), Some(Box::new(InlineAssist))), cx, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 78729df936..bf92c2b72e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -2,19 +2,16 @@ use crate::{ history::SearchHistory, mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, - SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, + SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, + ToggleWholeWord, }; use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{ - actions, - elements::*, - impl_actions, - platform::{CursorStyle, MouseButton}, - Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle, - WindowContext, + actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription, + Task, View, ViewContext, ViewHandle, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -54,6 +51,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::previous_history_query); cx.add_action(BufferSearchBar::cycle_mode); cx.add_action(BufferSearchBar::cycle_mode_on_pane); + cx.add_action(BufferSearchBar::replace_all); + cx.add_action(BufferSearchBar::replace_next); + cx.add_action(BufferSearchBar::replace_all_on_pane); + cx.add_action(BufferSearchBar::replace_next_on_pane); + cx.add_action(BufferSearchBar::toggle_replace); + cx.add_action(BufferSearchBar::toggle_replace_on_a_pane); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); } @@ -73,9 +76,11 @@ fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContex pub struct BufferSearchBar { query_editor: ViewHandle, + replacement_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, + active_search: Option>, searchable_items_with_matches: HashMap, Vec>>, pending_search: Option>, @@ -85,6 +90,7 @@ pub struct BufferSearchBar { dismissed: bool, search_history: SearchHistory, current_mode: SearchMode, + replace_is_active: bool, } impl Entity for BufferSearchBar { @@ -96,6 +102,21 @@ impl View for BufferSearchBar { "BufferSearchBar" } + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + cx: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + let in_replace = self + .replacement_editor + .read_with(cx, |_, cx| cx.is_self_focused()) + .unwrap_or(false); + if in_replace { + keymap.add_identifier("in_replace"); + } + } + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { cx.focus(&self.query_editor); @@ -156,6 +177,9 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); + self.replacement_editor.update(cx, |editor, cx| { + editor.set_placeholder_text("Replace with...", cx); + }); let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; @@ -212,7 +236,6 @@ impl View for BufferSearchBar { cx, ) }; - let query_column = Flex::row() .with_child( Svg::for_style(theme.search.editor_icon.clone().icon) @@ -243,7 +266,57 @@ impl View for BufferSearchBar { .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let should_show_replace_input = self.replace_is_active && supported_options.replacement; + let replacement = should_show_replace_input.then(|| { + Flex::row() + .with_child( + Svg::for_style(theme.search.replace_icon.clone().icon) + .contained() + .with_style(theme.search.replace_icon.clone().container), + ) + .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true)) + .align_children_center() + .flex(1., true) + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false) + }); + let replace_all = should_show_replace_input.then(|| { + super::replace_action( + ReplaceAll, + "Replace all", + "icons/replace_all.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let replace_next = should_show_replace_input.then(|| { + super::replace_action( + ReplaceNext, + "Replace next", + "icons/replace_next.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let switches_column = supported_options.replacement.then(|| { + Flex::row() + .align_children_center() + .with_child(super::toggle_replace_button( + self.replace_is_active, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .contained() + .with_style(theme.search.option_button_group) + }); let mode_column = Flex::row() .with_child(search_button_for_mode( SearchMode::Text, @@ -261,7 +334,10 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height); let nav_column = Flex::row() - .with_child(self.render_action_button("all", cx)) + .align_children_center() + .with_children(replace_next) + .with_children(replace_all) + .with_child(self.render_action_button("icons/select-all.svg", cx)) .with_child(Flex::row().with_children(match_count)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) @@ -271,6 +347,8 @@ impl View for BufferSearchBar { Flex::row() .with_child(query_column) + .with_children(switches_column) + .with_children(replacement) .with_child(mode_column) .with_child(nav_column) .contained() @@ -345,9 +423,18 @@ impl BufferSearchBar { }); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - + let replacement_editor = cx.add_view(|cx| { + Editor::auto_height( + 2, + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ) + }); + // cx.subscribe(&replacement_editor, Self::on_query_editor_event) + // .detach(); Self { query_editor, + replacement_editor, active_searchable_item: None, active_searchable_item_subscription: None, active_match_index: None, @@ -359,6 +446,8 @@ impl BufferSearchBar { dismissed: true, search_history: SearchHistory::default(), current_mode: SearchMode::default(), + active_search: None, + replace_is_active: false, } } @@ -441,7 +530,9 @@ impl BufferSearchBar { pub fn query(&self, cx: &WindowContext) -> String { self.query_editor.read(cx).text(cx) } - + pub fn replacement(&self, cx: &WindowContext) -> String { + self.replacement_editor.read(cx).text(cx) + } pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { self.active_searchable_item .as_ref() @@ -477,37 +568,16 @@ impl BufferSearchBar { ) -> AnyElement { let tooltip = "Select All Matches"; let tooltip_style = theme::current(cx).tooltip.clone(); - let action_type_id = 0_usize; - let has_matches = self.active_match_index.is_some(); - let cursor_style = if has_matches { - CursorStyle::PointingHand - } else { - CursorStyle::default() - }; - enum ActionButton {} - MouseEventHandler::new::(action_type_id, cx, |state, cx| { - let theme = theme::current(cx); - let style = theme - .search - .action_button - .in_state(has_matches) - .style_for(state); - Label::new(icon, style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - this.select_all_matches(&SelectAllMatches, cx) - }) - .with_cursor_style(cursor_style) - .with_tooltip::( - action_type_id, - tooltip.to_string(), - Some(Box::new(SelectAllMatches)), - tooltip_style, - cx, - ) + + let theme = theme::current(cx); + let style = theme.search.action_button.clone(); + + gpui::elements::Component::element(SafeStylable::with_style( + theme::components::action_button::Button::action(SelectAllMatches) + .with_tooltip(tooltip, tooltip_style) + .with_contents(theme::components::svg::Svg::new(icon)), + style, + )) .into_any() } @@ -688,6 +758,7 @@ impl BufferSearchBar { let (done_tx, done_rx) = oneshot::channel(); let query = self.query(cx); self.pending_search.take(); + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); @@ -695,7 +766,7 @@ impl BufferSearchBar { let _ = done_tx.send(()); cx.notify(); } else { - let query = if self.current_mode == SearchMode::Regex { + let query: Arc<_> = if self.current_mode == SearchMode::Regex { match SearchQuery::regex( query, self.search_options.contains(SearchOptions::WHOLE_WORD), @@ -703,7 +774,8 @@ impl BufferSearchBar { Vec::new(), Vec::new(), ) { - Ok(query) => query, + Ok(query) => query + .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())), Err(_) => { self.query_contains_error = true; cx.notify(); @@ -718,8 +790,10 @@ impl BufferSearchBar { Vec::new(), Vec::new(), ) - }; - + .with_replacement(Some(self.replacement(cx)).filter(|s| !s.is_empty())) + } + .into(); + self.active_search = Some(query.clone()); let query_text = query.as_str().to_string(); let matches = active_searchable_item.find_matches(query, cx); @@ -810,6 +884,83 @@ impl BufferSearchBar { cx.propagate_action(); } } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { + if let Some(_) = &self.active_searchable_item { + self.replace_is_active = !self.replace_is_active; + cx.notify(); + } + } + fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| { + if let Some(_) = &bar.active_searchable_item { + should_propagate = false; + bar.replace_is_active = !bar.replace_is_active; + cx.notify(); + } + }); + } + if should_propagate { + cx.propagate_action(); + } + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query.as_ref().clone().with_replacement( + Some(self.replacement(cx)).filter(|rep| !rep.is_empty()), + ); + searchable_item.replace(&matches[active_index], &query, cx); + } + + self.focus_editor(&FocusEditor, cx); + } + } + } + } + } + fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query.as_ref().clone().with_replacement( + Some(self.replacement(cx)).filter(|rep| !rep.is_empty()), + ); + for m in matches { + searchable_item.replace(m, &query, cx); + } + + self.focus_editor(&FocusEditor, cx); + } + } + } + } + } + fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_next(action, cx)); + return; + } + cx.propagate_action(); + } + fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_all(action, cx)); + return; + } + cx.propagate_action(); + } } #[cfg(test)] @@ -861,7 +1012,7 @@ mod tests { .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( - editor.all_background_highlights(cx), + editor.all_text_background_highlights(cx), &[ ( DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), @@ -882,7 +1033,7 @@ mod tests { editor.next_notification(cx).await; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_background_highlights(cx), + editor.all_text_background_highlights(cx), &[( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), Color::red(), @@ -898,7 +1049,7 @@ mod tests { .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( - editor.all_background_highlights(cx), + editor.all_text_background_highlights(cx), &[ ( DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), @@ -939,7 +1090,7 @@ mod tests { editor.next_notification(cx).await; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_background_highlights(cx), + editor.all_text_background_highlights(cx), &[ ( DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), @@ -1150,7 +1301,7 @@ mod tests { .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( - editor.all_background_highlights(cx), + editor.all_text_background_highlights(cx), &[( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), Color::red(), @@ -1177,7 +1328,7 @@ mod tests { editor.next_notification(cx).await; editor.update(cx, |editor, cx| { assert_eq!( - editor.all_background_highlights(cx), + editor.all_text_background_highlights(cx), &[( DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), Color::red(), @@ -1539,4 +1690,109 @@ mod tests { assert_eq!(search_bar.search_options, SearchOptions::NONE); }); } + #[gpui::test] + async fn test_replace_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("expression", None, cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. + editor.set_text("expr$1", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex or regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + + // Search for word boundaries and replace just a single one. + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("banana", cx); + }); + search_bar.replace_next(&ReplaceNext, cx) + }); + // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Let's turn on regex mode. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("\\[([^\\]]+)\\]", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("${1}number", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Now with a whole-word twist. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("things", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + // The only word affected by this edit should be `algorithms`, even though there's a bunch + // of words in this text that would match this regex if not for WHOLE_WORD. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching things + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6ca4928803..240928d4e0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -511,7 +511,7 @@ impl Item for ProjectSearchView { ) -> AnyElement { Flex::row() .with_child( - Svg::new("icons/magnifying_glass_12.svg") + Svg::new("icons/magnifying_glass.svg") .with_color(tab_theme.label.text.color) .constrained() .with_width(tab_theme.type_icon_width) @@ -701,8 +701,9 @@ impl ProjectSearchView { })); return; } + } else { + semantic_state.maintain_rate_limit = None; } - semantic_state.maintain_rate_limit = None; } } @@ -1439,7 +1440,7 @@ impl View for ProjectSearchBar { let search = _search.read(cx); let filter_button = render_option_button_icon( search.filters_enabled, - "icons/filter_12.svg", + "icons/filter.svg", 0, "Toggle filters", Box::new(ToggleFilters), @@ -1470,14 +1471,14 @@ impl View for ProjectSearchBar { }; let case_sensitive = is_semantic_disabled.then(|| { render_option_button_icon( - "icons/case_insensitive_12.svg", + "icons/case_insensitive.svg", SearchOptions::CASE_SENSITIVE, cx, ) }); let whole_word = is_semantic_disabled.then(|| { - render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) + render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) }); let search_button_for_mode = |mode, side, cx: &mut ViewContext| { @@ -1724,7 +1725,7 @@ pub mod tests { assert_eq!( search_view .results_editor - .update(cx, |editor, cx| editor.all_background_highlights(cx)), + .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), &[ ( DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 47f7f485c4..dabbc720ce 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -27,6 +29,7 @@ actions!( CycleMode, ToggleWholeWord, ToggleCaseSensitive, + ToggleReplace, SelectNextMatch, SelectPrevMatch, SelectAllMatches, @@ -34,7 +37,9 @@ actions!( PreviousHistoryQuery, ActivateTextMode, ActivateSemanticMode, - ActivateRegexMode + ActivateRegexMode, + ReplaceAll, + ReplaceNext ] ); @@ -58,8 +63,8 @@ impl SearchOptions { pub fn icon(&self) -> &'static str { match *self { - SearchOptions::WHOLE_WORD => "icons/word_search_12.svg", - SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg", + SearchOptions::WHOLE_WORD => "icons/word_search.svg", + SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg", _ => panic!("{:?} is not a named SearchOption", self), } } @@ -98,3 +103,32 @@ impl SearchOptions { .into_any() } } + +fn toggle_replace_button( + active: bool, + tooltip_style: TooltipStyle, + button_style: ToggleIconButtonStyle, +) -> AnyElement { + Button::dynamic_action(Box::new(ToggleReplace)) + .with_tooltip("Toggle Replace", tooltip_style) + .with_contents(theme::components::svg::Svg::new("icons/replace.svg")) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() +} + +fn replace_action( + action: impl Action, + name: &'static str, + icon_path: &'static str, + tooltip_style: TooltipStyle, + button_style: IconButtonStyle, +) -> AnyElement { + Button::dynamic_action(Box::new(action)) + .with_tooltip(name, tooltip_style) + .with_contents(theme::components::svg::Svg::new(icon_path)) + .with_style(button_style) + .element() + .into_any() +} diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 72a36efd50..45b02722ac 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -23,6 +23,7 @@ settings = { path = "../settings" } anyhow.workspace = true postage.workspace = true futures.workspace = true +ordered-float.workspace = true smol.workspace = true rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } isahc.workspace = true diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index c53a3e1ba9..3e35284027 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -7,12 +7,13 @@ use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::channel::oneshot; use gpui::executor; +use ordered_float::OrderedFloat; use project::{search::PathMatcher, Fs}; use rpc::proto::Timestamp; use rusqlite::params; use rusqlite::types::Value; use std::{ - cmp::Ordering, + cmp::Reverse, future::Future, ops::Range, path::{Path, PathBuf}, @@ -190,6 +191,10 @@ impl VectorDatabase { )", [], )?; + db.execute( + "CREATE INDEX spans_digest ON spans (digest)", + [], + )?; log::trace!("vector database initialized with updated schema."); Ok(()) @@ -274,6 +279,39 @@ impl VectorDatabase { }) } + pub fn embeddings_for_digests( + &self, + digests: Vec, + ) -> impl Future>> { + self.transact(move |db| { + let mut query = db.prepare( + " + SELECT digest, embedding + FROM spans + WHERE digest IN rarray(?) + ", + )?; + let mut embeddings_by_digest = HashMap::default(); + let digests = Rc::new( + digests + .into_iter() + .map(|p| Value::Blob(p.0.to_vec())) + .collect::>(), + ); + let rows = query.query_map(params![digests], |row| { + Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?)) + })?; + + for row in rows { + if let Ok(row) = row { + embeddings_by_digest.insert(row.0, row.1); + } + } + + Ok(embeddings_by_digest) + }) + } + pub fn embeddings_for_files( &self, worktree_id_file_paths: HashMap>>, @@ -370,16 +408,16 @@ impl VectorDatabase { query_embedding: &Embedding, limit: usize, file_ids: &[i64], - ) -> impl Future>> { + ) -> impl Future)>>> { let query_embedding = query_embedding.clone(); let file_ids = file_ids.to_vec(); self.transact(move |db| { - let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1); + let mut results = Vec::<(i64, OrderedFloat)>::with_capacity(limit + 1); Self::for_each_span(db, &file_ids, |id, embedding| { let similarity = embedding.similarity(&query_embedding); - let ix = match results.binary_search_by(|(_, s)| { - similarity.partial_cmp(&s).unwrap_or(Ordering::Equal) - }) { + let ix = match results + .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s)) + { Ok(ix) => ix, Err(ix) => ix, }; diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 42d90f0fdb..b0124bf7df 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -7,6 +7,7 @@ use isahc::http::StatusCode; use isahc::prelude::Configurable; use isahc::{AsyncBody, Response}; use lazy_static::lazy_static; +use ordered_float::OrderedFloat; use parking_lot::Mutex; use parse_duration::parse; use postage::watch; @@ -35,7 +36,7 @@ impl From> for Embedding { } impl Embedding { - pub fn similarity(&self, other: &Self) -> f32 { + pub fn similarity(&self, other: &Self) -> OrderedFloat { let len = self.0.len(); assert_eq!(len, other.0.len()); @@ -58,7 +59,7 @@ impl Embedding { 1, ); } - result + OrderedFloat(result) } } @@ -379,13 +380,13 @@ mod tests { ); } - fn round_to_decimals(n: f32, decimal_places: i32) -> f32 { + fn round_to_decimals(n: OrderedFloat, decimal_places: i32) -> f32 { let factor = (10.0 as f32).powi(decimal_places); (n * factor).round() / factor } - fn reference_dot(a: &[f32], b: &[f32]) -> f32 { - a.iter().zip(b.iter()).map(|(a, b)| a * b).sum() + fn reference_dot(a: &[f32], b: &[f32]) -> OrderedFloat { + OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()) } } } diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index b6fc000e1d..9f5a339b23 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -7,6 +7,7 @@ use rusqlite::{ }; use sha1::{Digest, Sha1}; use std::{ + borrow::Cow, cmp::{self, Reverse}, collections::HashSet, ops::Range, @@ -16,7 +17,7 @@ use std::{ use tree_sitter::{Parser, QueryCursor}; #[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub struct SpanDigest([u8; 20]); +pub struct SpanDigest(pub [u8; 20]); impl FromSql for SpanDigest { fn column_result(value: ValueRef) -> FromSqlResult { @@ -94,12 +95,15 @@ impl CodeContextRetriever { fn parse_entire_file( &self, - relative_path: &Path, + relative_path: Option<&Path>, language_name: Arc, content: &str, ) -> Result> { let document_span = ENTIRE_FILE_TEMPLATE - .replace("", relative_path.to_string_lossy().as_ref()) + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) .replace("", language_name.as_ref()) .replace("", &content); let digest = SpanDigest::from(document_span.as_str()); @@ -114,9 +118,16 @@ impl CodeContextRetriever { }]) } - fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result> { + fn parse_markdown_file( + &self, + relative_path: Option<&Path>, + content: &str, + ) -> Result> { let document_span = MARKDOWN_CONTEXT_TEMPLATE - .replace("", relative_path.to_string_lossy().as_ref()) + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) .replace("", &content); let digest = SpanDigest::from(document_span.as_str()); let (document_span, token_count) = self.embedding_provider.truncate(&document_span); @@ -188,7 +199,7 @@ impl CodeContextRetriever { pub fn parse_file_with_template( &mut self, - relative_path: &Path, + relative_path: Option<&Path>, content: &str, language: Arc, ) -> Result> { @@ -196,14 +207,17 @@ impl CodeContextRetriever { if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) { return self.parse_entire_file(relative_path, language_name, &content); - } else if language_name.as_ref() == "Markdown" { + } else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) { return self.parse_markdown_file(relative_path, &content); } let mut spans = self.parse_file(content, language)?; for span in &mut spans { let document_content = CODE_CONTEXT_TEMPLATE - .replace("", relative_path.to_string_lossy().as_ref()) + .replace( + "", + &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()), + ) .replace("", language_name.as_ref()) .replace("item", &span.content); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 115bf5d7a8..06c7aa53fa 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -16,14 +16,16 @@ use embedding_queue::{EmbeddingQueue, FileToEmbed}; use futures::{future, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use language::{Anchor, Bias, Buffer, Language, LanguageRegistry}; +use ordered_float::OrderedFloat; use parking_lot::Mutex; -use parsing::{CodeContextRetriever, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES}; +use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES}; use postage::watch; use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId}; use smol::channel; use std::{ - cmp::Ordering, + cmp::Reverse, future::Future, + mem, ops::Range, path::{Path, PathBuf}, sync::{Arc, Weak}, @@ -37,7 +39,7 @@ use util::{ }; use workspace::WorkspaceCreated; -const SEMANTIC_INDEX_VERSION: usize = 10; +const SEMANTIC_INDEX_VERSION: usize = 11; const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60); const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250); @@ -262,9 +264,11 @@ pub struct PendingFile { job_handle: JobHandle, } +#[derive(Clone)] pub struct SearchResult { pub buffer: ModelHandle, pub range: Range, + pub similarity: OrderedFloat, } impl SemanticIndex { @@ -402,7 +406,7 @@ impl SemanticIndex { if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() { if let Some(mut spans) = retriever - .parse_file_with_template(&pending_file.relative_path, &content, language) + .parse_file_with_template(Some(&pending_file.relative_path), &content, language) .log_err() { log::trace!( @@ -422,7 +426,7 @@ impl SemanticIndex { path: pending_file.relative_path, mtime: pending_file.modified_time, job_handle: pending_file.job_handle, - spans: spans, + spans, }); } } @@ -687,39 +691,71 @@ impl SemanticIndex { pub fn search_project( &mut self, project: ModelHandle, - phrase: String, + query: String, limit: usize, includes: Vec, excludes: Vec, cx: &mut ModelContext, ) -> Task>> { + if query.is_empty() { + return Task::ready(Ok(Vec::new())); + } + let index = self.index_project(project.clone(), cx); let embedding_provider = self.embedding_provider.clone(); + + cx.spawn(|this, mut cx| async move { + let query = embedding_provider + .embed_batch(vec![query]) + .await? + .pop() + .ok_or_else(|| anyhow!("could not embed query"))?; + index.await?; + + let search_start = Instant::now(); + let modified_buffer_results = this.update(&mut cx, |this, cx| { + this.search_modified_buffers(&project, query.clone(), limit, &excludes, cx) + }); + let file_results = this.update(&mut cx, |this, cx| { + this.search_files(project, query, limit, includes, excludes, cx) + }); + let (modified_buffer_results, file_results) = + futures::join!(modified_buffer_results, file_results); + + // Weave together the results from modified buffers and files. + let mut results = Vec::new(); + let mut modified_buffers = HashSet::default(); + for result in modified_buffer_results.log_err().unwrap_or_default() { + modified_buffers.insert(result.buffer.clone()); + results.push(result); + } + for result in file_results.log_err().unwrap_or_default() { + if !modified_buffers.contains(&result.buffer) { + results.push(result); + } + } + results.sort_by_key(|result| Reverse(result.similarity)); + results.truncate(limit); + log::trace!("Semantic search took {:?}", search_start.elapsed()); + Ok(results) + }) + } + + pub fn search_files( + &mut self, + project: ModelHandle, + query: Embedding, + limit: usize, + includes: Vec, + excludes: Vec, + cx: &mut ModelContext, + ) -> Task>> { let db_path = self.db.path().clone(); let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { - index.await?; - - let t0 = Instant::now(); let database = VectorDatabase::new(fs.clone(), db_path.clone(), cx.background()).await?; - if phrase.len() == 0 { - return Ok(Vec::new()); - } - - let phrase_embedding = embedding_provider - .embed_batch(vec![phrase]) - .await? - .into_iter() - .next() - .unwrap(); - - log::trace!( - "Embedding search phrase took: {:?} milliseconds", - t0.elapsed().as_millis() - ); - let worktree_db_ids = this.read_with(&cx, |this, _| { let project_state = this .projects @@ -738,6 +774,7 @@ impl SemanticIndex { .collect::>(); anyhow::Ok(worktree_db_ids) })?; + let file_ids = database .retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes) .await?; @@ -756,26 +793,26 @@ impl SemanticIndex { let limit = limit.clone(); let fs = fs.clone(); let db_path = db_path.clone(); - let phrase_embedding = phrase_embedding.clone(); + let query = query.clone(); if let Some(db) = VectorDatabase::new(fs, db_path.clone(), cx.background()) .await .log_err() { batch_results.push(async move { - db.top_k_search(&phrase_embedding, limit, batch.as_slice()) - .await + db.top_k_search(&query, limit, batch.as_slice()).await }); } } + let batch_results = futures::future::join_all(batch_results).await; let mut results = Vec::new(); for batch_result in batch_results { if batch_result.is_ok() { for (id, similarity) in batch_result.unwrap() { - let ix = match results.binary_search_by(|(_, s)| { - similarity.partial_cmp(&s).unwrap_or(Ordering::Equal) - }) { + let ix = match results + .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s)) + { Ok(ix) => ix, Err(ix) => ix, }; @@ -785,7 +822,11 @@ impl SemanticIndex { } } - let ids = results.into_iter().map(|(id, _)| id).collect::>(); + let ids = results.iter().map(|(id, _)| *id).collect::>(); + let scores = results + .into_iter() + .map(|(_, score)| score) + .collect::>(); let spans = database.spans_for_ids(ids.as_slice()).await?; let mut tasks = Vec::new(); @@ -810,24 +851,106 @@ impl SemanticIndex { let buffers = futures::future::join_all(tasks).await; - log::trace!( - "Semantic Searching took: {:?} milliseconds in total", - t0.elapsed().as_millis() - ); - Ok(buffers .into_iter() .zip(ranges) - .filter_map(|(buffer, range)| { + .zip(scores) + .filter_map(|((buffer, range), similarity)| { let buffer = buffer.log_err()?; let range = buffer.read_with(&cx, |buffer, _| { let start = buffer.clip_offset(range.start, Bias::Left); let end = buffer.clip_offset(range.end, Bias::Right); buffer.anchor_before(start)..buffer.anchor_after(end) }); - Some(SearchResult { buffer, range }) + Some(SearchResult { + buffer, + range, + similarity, + }) }) - .collect::>()) + .collect()) + }) + } + + fn search_modified_buffers( + &self, + project: &ModelHandle, + query: Embedding, + limit: usize, + excludes: &[PathMatcher], + cx: &mut ModelContext, + ) -> Task>> { + let modified_buffers = project + .read(cx) + .opened_buffers(cx) + .into_iter() + .filter_map(|buffer_handle| { + let buffer = buffer_handle.read(cx); + let snapshot = buffer.snapshot(); + let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| { + excludes.iter().any(|matcher| matcher.is_match(&path)) + }); + if buffer.is_dirty() && !excluded { + Some((buffer_handle, snapshot)) + } else { + None + } + }) + .collect::>(); + + let embedding_provider = self.embedding_provider.clone(); + let fs = self.fs.clone(); + let db_path = self.db.path().clone(); + let background = cx.background().clone(); + cx.background().spawn(async move { + let db = VectorDatabase::new(fs, db_path.clone(), background).await?; + let mut results = Vec::::new(); + + let mut retriever = CodeContextRetriever::new(embedding_provider.clone()); + for (buffer, snapshot) in modified_buffers { + let language = snapshot + .language_at(0) + .cloned() + .unwrap_or_else(|| language::PLAIN_TEXT.clone()); + let mut spans = retriever + .parse_file_with_template(None, &snapshot.text(), language) + .log_err() + .unwrap_or_default(); + if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db) + .await + .log_err() + .is_some() + { + for span in spans { + let similarity = span.embedding.unwrap().similarity(&query); + let ix = match results + .binary_search_by_key(&Reverse(similarity), |result| { + Reverse(result.similarity) + }) { + Ok(ix) => ix, + Err(ix) => ix, + }; + + let range = { + let start = snapshot.clip_offset(span.range.start, Bias::Left); + let end = snapshot.clip_offset(span.range.end, Bias::Right); + snapshot.anchor_before(start)..snapshot.anchor_after(end) + }; + + results.insert( + ix, + SearchResult { + buffer: buffer.clone(), + range, + similarity, + }, + ); + results.truncate(limit); + } + } + } + + Ok(results) }) } @@ -1011,6 +1134,63 @@ impl SemanticIndex { Ok(()) }) } + + async fn embed_spans( + spans: &mut [Span], + embedding_provider: &dyn EmbeddingProvider, + db: &VectorDatabase, + ) -> Result<()> { + let mut batch = Vec::new(); + let mut batch_tokens = 0; + let mut embeddings = Vec::new(); + + let digests = spans + .iter() + .map(|span| span.digest.clone()) + .collect::>(); + let embeddings_for_digests = db + .embeddings_for_digests(digests) + .await + .log_err() + .unwrap_or_default(); + + for span in &*spans { + if embeddings_for_digests.contains_key(&span.digest) { + continue; + }; + + if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() { + let batch_embeddings = embedding_provider + .embed_batch(mem::take(&mut batch)) + .await?; + embeddings.extend(batch_embeddings); + batch_tokens = 0; + } + + batch_tokens += span.token_count; + batch.push(span.content.clone()); + } + + if !batch.is_empty() { + let batch_embeddings = embedding_provider + .embed_batch(mem::take(&mut batch)) + .await?; + + embeddings.extend(batch_embeddings); + } + + let mut embeddings = embeddings.into_iter(); + for span in spans { + let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) { + Some(embedding.clone()) + } else { + embeddings.next() + }; + let embedding = embedding.ok_or_else(|| anyhow!("failed to embed spans"))?; + span.embedding = Some(embedding); + } + Ok(()) + } } impl Entity for SemanticIndex { diff --git a/crates/storybook/src/collab_panel.rs b/crates/storybook/src/collab_panel.rs index 7bf08febe3..87fd536391 100644 --- a/crates/storybook/src/collab_panel.rs +++ b/crates/storybook/src/collab_panel.rs @@ -132,9 +132,9 @@ impl CollabPanelElement { div().flex().h_full().gap_1().items_center().child( svg() .path(if expanded { - "icons/radix/caret-down.svg" + "icons/caret_down.svg" } else { - "icons/radix/caret-up.svg" + "icons/caret_up.svg" }) .w_3p5() .h_3p5() diff --git a/crates/storybook/src/workspace.rs b/crates/storybook/src/workspace.rs index 3e7d467f80..1bcbc79b32 100644 --- a/crates/storybook/src/workspace.rs +++ b/crates/storybook/src/workspace.rs @@ -222,7 +222,7 @@ impl TitleBar { .fill(theme.lowest.base.pressed.background) .child( svg() - .path("icons/radix/speaker-loud.svg") + .path("icons/speaker-loud.svg") .size_3p5() .fill(theme.lowest.base.default.foreground), ), @@ -242,7 +242,7 @@ impl TitleBar { .fill(theme.lowest.base.pressed.background) .child( svg() - .path("icons/radix/desktop.svg") + .path("icons/desktop.svg") .size_3p5() .fill(theme.lowest.base.default.foreground), ), @@ -274,7 +274,7 @@ impl TitleBar { ) .child( svg() - .path("icons/caret_down_8.svg") + .path("icons/caret_down.svg") .w_2() .h_2() .fill(theme.lowest.variant.default.foreground), diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 9d19625971..b933fd48e9 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -333,6 +333,7 @@ mod test { cmd: false, function: false, key: "🖖🏻".to_string(), //2 char string + ime_key: None, }; assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 9fb3939e1f..39d2f14f08 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -70,7 +70,7 @@ impl TerminalPanel { Flex::row() .with_child(Pane::render_tab_bar_button( 0, - "icons/plus_12.svg", + "icons/plus.svg", false, Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))), cx, @@ -90,9 +90,9 @@ impl TerminalPanel { .with_child(Pane::render_tab_bar_button( 1, if pane.is_zoomed() { - "icons/minimize_8.svg" + "icons/minimize.svg" } else { - "icons/maximize_8.svg" + "icons/maximize.svg" }, pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a12f9d3c3c..b79f655f81 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -18,7 +18,7 @@ use gpui::{ ViewHandle, WeakViewHandle, }; use language::Bias; -use project::{LocalWorktree, Project}; +use project::{search::SearchQuery, LocalWorktree, Project}; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; use smol::Timer; @@ -26,6 +26,7 @@ use std::{ borrow::Cow, ops::RangeInclusive, path::{Path, PathBuf}, + sync::Arc, time::Duration, }; use terminal::{ @@ -380,10 +381,10 @@ impl TerminalView { pub fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>> { - let searcher = regex_search_for_query(query); + let searcher = regex_search_for_query(&query); if let Some(searcher) = searcher { self.terminal @@ -486,7 +487,7 @@ fn possible_open_targets( .collect() } -pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { +pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let query = query.as_str(); let searcher = RegexSearch::new(&query); searcher.ok() @@ -798,6 +799,7 @@ impl SearchableItem for TerminalView { case: false, word: false, regex: false, + replacement: false, } } @@ -851,10 +853,10 @@ impl SearchableItem for TerminalView { /// Get all of the matches for this query, should be done on the background fn find_matches( &mut self, - query: project::search::SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task> { - if let Some(searcher) = regex_search_for_query(query) { + if let Some(searcher) = regex_search_for_query(&query) { self.terminal() .update(cx, |term, cx| term.find_matches(searcher, cx)) } else { @@ -898,6 +900,9 @@ impl SearchableItem for TerminalView { res } + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Replacement is not supported in terminal view, so this is a no-op. + } } ///Get's the working directory for the given workspace, respecting the user's settings. diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cd983322db..cc90d96420 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,9 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{ + action_button::ButtonStyle, disclosure::DisclosureStyle, IconButtonStyle, ToggleIconButtonStyle, +}; use gpui::{ color::Color, elements::{Border, ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -439,9 +441,7 @@ pub struct Search { pub include_exclude_editor: FindEditor, pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, - pub option_button: Toggleable>, pub option_button_component: ToggleIconButtonStyle, - pub action_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, pub major_results_status: TextStyle, @@ -453,6 +453,10 @@ pub struct Search { pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, + pub replace_icon: IconStyle, + // Used for filters and replace + pub option_button: Toggleable>, + pub action_button: IconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index da5c7d46ed..ae6a2808cf 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -34,7 +34,9 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) { editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { + vim.clear_operator(cx); vim.workspace_state.recording = false; + vim.workspace_state.recorded_actions.clear(); if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { vim.active_editor = None; diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 9141a02ab3..fb567fab6a 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,6 +1,6 @@ -use crate::{state::Mode, Vim}; +use crate::{normal::repeat, state::Mode, Vim}; use editor::{scroll::autoscroll::Autoscroll, Bias}; -use gpui::{actions, AppContext, ViewContext}; +use gpui::{actions, Action, AppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; @@ -10,24 +10,41 @@ pub fn init(cx: &mut AppContext) { cx.add_action(normal_before); } -fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.stop_recording(); - vim.update_active_editor(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) +fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext) { + let should_repeat = Vim::update(cx, |vim, cx| { + let count = vim.take_count(cx).unwrap_or(1); + vim.stop_recording_immediately(action.boxed_clone()); + if count <= 1 || vim.workspace_state.replaying { + vim.update_active_editor(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_cursors_with(|map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); }); }); - }); - vim.switch_mode(Mode::Normal, false, cx); - }) + vim.switch_mode(Mode::Normal, false, cx); + false + } else { + true + } + }); + + if should_repeat { + repeat::repeat(cx, true) + } } #[cfg(test)] mod test { - use crate::{state::Mode, test::VimTestContext}; + use std::sync::Arc; + + use gpui::executor::Deterministic; + + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; #[gpui::test] async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) { @@ -40,4 +57,78 @@ mod test { assert_eq!(cx.mode(), Mode::Normal); cx.assert_editor_state("Tesˇt"); } + + #[gpui::test] + async fn test_insert_with_counts( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["5", "i", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("----ˇ-hello\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["5", "a", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("h----ˇ-ello\n").await; + + cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("---ˇ-h-----ello\n").await; + + cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("----h-----ello--ˇ-\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\noi\noi\noˇi\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("oi\noi\noˇi\nhello\n").await; + } + + #[gpui::test] + async fn test_insert_with_repeat( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["3", "i", "-", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("--ˇ-hello\n").await; + cx.simulate_shared_keystrokes(["."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("----ˇ--hello\n").await; + cx.simulate_shared_keystrokes(["2", "."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("-----ˇ---hello\n").await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"]) + .await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\nkk\nkˇk\n").await; + cx.simulate_shared_keystrokes(["."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await; + cx.simulate_shared_keystrokes(["1", "."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await; + } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index c232ff9849..3e65e6d504 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -40,6 +40,8 @@ pub enum Motion { FindForward { before: bool, char: char }, FindBackward { after: bool, char: char }, NextLineStart, + StartOfLineDownward, + EndOfLineDownward, } #[derive(Clone, Deserialize, PartialEq)] @@ -117,6 +119,8 @@ actions!( EndOfDocument, Matching, NextLineStart, + StartOfLineDownward, + EndOfLineDownward, ] ); impl_actions!( @@ -207,6 +211,12 @@ pub fn init(cx: &mut AppContext) { cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) }, ); cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx)); + cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| { + motion(Motion::StartOfLineDownward, cx) + }); + cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| { + motion(Motion::EndOfLineDownward, cx) + }); cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| { repeat_motion(action.backwards, cx) }) @@ -219,11 +229,11 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| vim.pop_operator(cx)); } - let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx)); + let count = Vim::update(cx, |vim, cx| vim.take_count(cx)); let operator = Vim::read(cx).active_operator(); match Vim::read(cx).state().mode { - Mode::Normal => normal_motion(motion, operator, times, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx), + Mode::Normal => normal_motion(motion, operator, count, cx), + Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx), Mode::Insert => { // Shouldn't execute a motion in insert mode. Ignoring } @@ -272,6 +282,7 @@ impl Motion { | EndOfDocument | CurrentLine | NextLineStart + | StartOfLineDownward | StartOfParagraph | EndOfParagraph => true, EndOfLine { .. } @@ -282,6 +293,7 @@ impl Motion { | Backspace | Right | StartOfLine { .. } + | EndOfLineDownward | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -305,6 +317,8 @@ impl Motion { | StartOfLine { .. } | StartOfParagraph | EndOfParagraph + | StartOfLineDownward + | EndOfLineDownward | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace { .. } @@ -322,6 +336,7 @@ impl Motion { | EndOfDocument | CurrentLine | EndOfLine { .. } + | EndOfLineDownward | NextWordEnd { .. } | Matching | FindForward { .. } @@ -330,6 +345,7 @@ impl Motion { | Backspace | Right | StartOfLine { .. } + | StartOfLineDownward | StartOfParagraph | EndOfParagraph | NextWordStart { .. } @@ -396,7 +412,7 @@ impl Motion { map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), - CurrentLine => (end_of_line(map, false, point), SelectionGoal::None), + CurrentLine => (next_line_end(map, point, times), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( end_of_document(map, point, maybe_times), @@ -412,6 +428,8 @@ impl Motion { SelectionGoal::None, ), NextLineStart => (next_line_start(map, point, times), SelectionGoal::None), + StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), + EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None), }; (new_point != point || infallible).then_some((new_point, goal)) @@ -849,6 +867,13 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> first_non_whitespace(map, false, correct_line) } +fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { + if times > 1 { + point = down(map, point, SelectionGoal::None, times - 1).0; + } + end_of_line(map, false, point) +} + #[cfg(test)] mod test { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index d920abee90..c8d12f8ee3 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,7 +2,7 @@ mod case; mod change; mod delete; mod paste; -mod repeat; +pub(crate) mod repeat; mod scroll; mod search; pub mod substitute; @@ -68,21 +68,21 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let times = vim.pop_number_operator(cx); + let times = vim.take_count(cx); delete_motion(vim, Motion::Left, times, cx); }) }); cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let times = vim.pop_number_operator(cx); + let times = vim.take_count(cx); delete_motion(vim, Motion::Right, times, cx); }) }); cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { Vim::update(cx, |vim, cx| { vim.start_recording(cx); - let times = vim.pop_number_operator(cx); + let times = vim.take_count(cx); change_motion( vim, Motion::EndOfLine { @@ -96,7 +96,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let times = vim.pop_number_operator(cx); + let times = vim.take_count(cx); delete_motion( vim, Motion::EndOfLine { @@ -110,7 +110,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let mut times = vim.pop_number_operator(cx).unwrap_or(1); + let mut times = vim.take_count(cx).unwrap_or(1); if vim.state().mode.is_visual() { times = 1; } else if times > 1 { @@ -356,7 +356,7 @@ mod test { use crate::{ state::Mode::{self}, - test::{ExemptionFeatures, NeovimBackedTestContext}, + test::NeovimBackedTestContext, }; #[gpui::test] @@ -762,20 +762,22 @@ mod test { #[gpui::test] async fn test_dd(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]); - cx.assert("ˇ").await; - cx.assert("The ˇquick").await; - cx.assert_all(indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}) - .await; - cx.assert_exempted( + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible("ˇ", ["d", "d"]).await; + cx.assert_neovim_compatible("The ˇquick", ["d", "d"]).await; + for marked_text in cx.each_marked_position(indoc! {" + The qˇuick + brown ˇfox + jumps ˇover"}) + { + cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await; + } + cx.assert_neovim_compatible( indoc! {" The quick ˇ brown fox"}, - ExemptionFeatures::DeletionOnEmptyLine, + ["d", "d"], ) .await; } diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 12fd8dbd2b..22d09f8359 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -8,7 +8,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim}; pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.record_current_action(cx); - let count = vim.pop_number_operator(cx).unwrap_or(1) as u32; + let count = vim.take_count(cx).unwrap_or(1) as u32; vim.update_active_editor(cx, |editor, cx| { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 836ce1492b..e9f3001392 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -121,7 +121,7 @@ fn expand_changed_word_selection( mod test { use indoc::indoc; - use crate::test::{ExemptionFeatures, NeovimBackedTestContext}; + use crate::test::NeovimBackedTestContext; #[gpui::test] async fn test_change_h(cx: &mut gpui::TestAppContext) { @@ -239,150 +239,178 @@ mod test { #[gpui::test] async fn test_change_0(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.assert_neovim_compatible( + indoc! {" The qˇuick - brown fox"}) - .await; - cx.assert(indoc! {" + brown fox"}, + ["c", "0"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick ˇ - brown fox"}) - .await; + brown fox"}, + ["c", "0"], + ) + .await; } #[gpui::test] async fn test_change_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.assert_neovim_compatible( + indoc! {" The quick brown ˇfox - jumps over"}) - .await; - cx.assert(indoc! {" + jumps over"}, + ["c", "k"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brown fox - jumps ˇover"}) - .await; - cx.assert_exempted( + jumps ˇover"}, + ["c", "k"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "k"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" ˇ brown fox jumps over"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "k"], ) .await; } #[gpui::test] async fn test_change_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brown ˇfox - jumps over"}) - .await; - cx.assert_exempted( + jumps over"}, + ["c", "j"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps ˇover"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "j"], ) .await; - cx.assert(indoc! {" + cx.assert_neovim_compatible( + indoc! {" The qˇuick brown fox - jumps over"}) - .await; - cx.assert_exempted( + jumps over"}, + ["c", "j"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox ˇ"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "j"], ) .await; } #[gpui::test] async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["c", "shift-g"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["c", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert_exempted( + the lazy"}, + ["c", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over the lˇazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "shift-g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over ˇ"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "shift-g"], ) .await; } #[gpui::test] async fn test_change_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["c", "g", "g"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["c", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brown fox jumps over - the lˇazy"}) - .await; - cx.assert_exempted( + the lˇazy"}, + ["c", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "g", "g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" ˇ brown fox jumps over the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["c", "g", "g"], ) .await; } @@ -427,27 +455,17 @@ mod test { async fn test_repeated_cb(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; - cx.add_initial_state_exemptions( - indoc! {" - ˇThe quick brown - - fox jumps-over - the lazy dog - "}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, - ); - for count in 1..=5 { - cx.assert_binding_matches_all( - ["c", &count.to_string(), "b"], - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await; + for marked_text in cx.each_marked_position(indoc! {" + ˇThe quˇickˇ browˇn + ˇ + ˇfox ˇjumpsˇ-ˇoˇver + ˇthe lazy dog + "}) + { + cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"]) + .await; + } } } diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index ae85acaab5..848e9f725d 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -278,37 +278,41 @@ mod test { #[gpui::test] async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["d", "shift-g"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["d", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert_exempted( + the lazy"}, + ["d", "shift-g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over the lˇazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["d", "shift-g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" The quick brown fox jumps over ˇ"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["d", "shift-g"], ) .await; } @@ -318,34 +322,40 @@ mod test { let mut cx = NeovimBackedTestContext::new(cx) .await .binding(["d", "g", "g"]); - cx.assert(indoc! {" + cx.assert_neovim_compatible( + indoc! {" The quick brownˇ fox jumps over - the lazy"}) - .await; - cx.assert(indoc! {" + the lazy"}, + ["d", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( + indoc! {" The quick brown fox jumps over - the lˇazy"}) - .await; - cx.assert_exempted( + the lˇazy"}, + ["d", "g", "g"], + ) + .await; + cx.assert_neovim_compatible( indoc! {" The qˇuick brown fox jumps over the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["d", "g", "g"], ) .await; - cx.assert_exempted( + cx.assert_neovim_compatible( indoc! {" ˇ brown fox jumps over the lazy"}, - ExemptionFeatures::OperatorAbortsOnFailedMotion, + ["d", "g", "g"], ) .await; } @@ -387,4 +397,40 @@ mod test { assert_eq!(cx.active_operator(), None); assert_eq!(cx.mode(), Mode::Normal); } + + #[gpui::test] + async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["d", "2", "d"]).await; + cx.assert_shared_state(indoc! {" + the ˇlazy dog"}) + .await; + + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["2", "d", "d"]).await; + cx.assert_shared_state(indoc! {" + the ˇlazy dog"}) + .await; + + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the moon, + a star, and + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await; + cx.assert_shared_state(indoc! {" + the ˇlazy dog"}) + .await; + } } diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 1a7c789aad..df9e9a32ad 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -1,10 +1,11 @@ use crate::{ + insert::NormalBefore, motion::Motion, state::{Mode, RecordedSelection, ReplayableAction}, visual::visual_motion, Vim, }; -use gpui::{actions, Action, AppContext}; +use gpui::{actions, Action, AppContext, WindowContext}; use workspace::Workspace; actions!(vim, [Repeat, EndRepeat,]); @@ -17,138 +18,187 @@ fn should_replay(action: &Box) -> bool { true } +fn repeatable_insert(action: &ReplayableAction) -> Option> { + match action { + ReplayableAction::Action(action) => { + if super::InsertBefore.id() == action.id() + || super::InsertAfter.id() == action.id() + || super::InsertFirstNonWhitespace.id() == action.id() + || super::InsertEndOfLine.id() == action.id() + { + Some(super::InsertBefore.boxed_clone()) + } else if super::InsertLineAbove.id() == action.id() + || super::InsertLineBelow.id() == action.id() + { + Some(super::InsertLineBelow.boxed_clone()) + } else { + None + } + } + ReplayableAction::Insertion { .. } => None, + } +} + pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| { Vim::update(cx, |vim, cx| { vim.workspace_state.replaying = false; - vim.update_active_editor(cx, |editor, _| { - editor.show_local_selections = true; - }); vim.switch_mode(Mode::Normal, false, cx) }); }); - cx.add_action(|_: &mut Workspace, _: &Repeat, cx| { - let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| { - let actions = vim.workspace_state.recorded_actions.clone(); - let Some(editor) = vim.active_editor.clone() else { - return None; - }; - let count = vim.pop_number_operator(cx); + cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false)); +} - vim.workspace_state.replaying = true; - - let selection = vim.workspace_state.recorded_selection.clone(); - match selection { - RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::Visual, false, cx) - } - RecordedSelection::VisualLine { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualLine, false, cx) - } - RecordedSelection::VisualBlock { .. } => { - vim.workspace_state.recorded_count = None; - vim.switch_mode(Mode::VisualBlock, false, cx) - } - RecordedSelection::None => { - if let Some(count) = count { - vim.workspace_state.recorded_count = Some(count); - } - } - } - - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, _| { - editor.show_local_selections = false; - }) - } else { - return None; - } - - Some((actions, editor, selection)) - }) else { - return; - }; - - match selection { - RecordedSelection::SingleLine { cols } => { - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx) - } - } - RecordedSelection::Visual { rows, cols } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - visual_motion( - Motion::StartOfLine { - display_lines: false, - }, - None, - cx, - ); - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx) - } - } - RecordedSelection::VisualBlock { rows, cols } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - if cols > 1 { - visual_motion(Motion::Right, Some(cols as usize - 1), cx); - } - } - RecordedSelection::VisualLine { rows } => { - visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - cx, - ); - } - RecordedSelection::None => {} +pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { + let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| { + let actions = vim.workspace_state.recorded_actions.clone(); + if actions.is_empty() { + return None; } - let window = cx.window(); - cx.app_context() - .spawn(move |mut cx| async move { - for action in actions { - match action { - ReplayableAction::Action(action) => { - if should_replay(&action) { - window - .dispatch_action(editor.id(), action.as_ref(), &mut cx) - .ok_or_else(|| anyhow::anyhow!("window was closed")) - } else { - Ok(()) - } - } - ReplayableAction::Insertion { - text, - utf16_range_to_replace, - } => editor.update(&mut cx, |editor, cx| { - editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) - }), - }? + let Some(editor) = vim.active_editor.clone() else { + return None; + }; + let count = vim.take_count(cx); + + let selection = vim.workspace_state.recorded_selection.clone(); + match selection { + RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { + vim.workspace_state.recorded_count = None; + vim.switch_mode(Mode::Visual, false, cx) + } + RecordedSelection::VisualLine { .. } => { + vim.workspace_state.recorded_count = None; + vim.switch_mode(Mode::VisualLine, false, cx) + } + RecordedSelection::VisualBlock { .. } => { + vim.workspace_state.recorded_count = None; + vim.switch_mode(Mode::VisualBlock, false, cx) + } + RecordedSelection::None => { + if let Some(count) = count { + vim.workspace_state.recorded_count = Some(count); } - window - .dispatch_action(editor.id(), &EndRepeat, &mut cx) - .ok_or_else(|| anyhow::anyhow!("window was closed")) - }) - .detach_and_log_err(cx); - }); + } + } + + Some((actions, editor, selection)) + }) else { + return; + }; + + match selection { + RecordedSelection::SingleLine { cols } => { + if cols > 1 { + visual_motion(Motion::Right, Some(cols as usize - 1), cx) + } + } + RecordedSelection::Visual { rows, cols } => { + visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + visual_motion( + Motion::StartOfLine { + display_lines: false, + }, + None, + cx, + ); + if cols > 1 { + visual_motion(Motion::Right, Some(cols as usize - 1), cx) + } + } + RecordedSelection::VisualBlock { rows, cols } => { + visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + if cols > 1 { + visual_motion(Motion::Right, Some(cols as usize - 1), cx); + } + } + RecordedSelection::VisualLine { rows } => { + visual_motion( + Motion::Down { + display_lines: false, + }, + Some(rows as usize), + cx, + ); + } + RecordedSelection::None => {} + } + + // insert internally uses repeat to handle counts + // vim doesn't treat 3a1 as though you literally repeated a1 + // 3 times, instead it inserts the content thrice at the insert position. + if let Some(to_repeat) = repeatable_insert(&actions[0]) { + if let Some(ReplayableAction::Action(action)) = actions.last() { + if action.id() == NormalBefore.id() { + actions.pop(); + } + } + + let mut new_actions = actions.clone(); + actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); + + let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1); + + // if we came from insert mode we're just doing repititions 2 onwards. + if from_insert_mode { + count -= 1; + new_actions[0] = actions[0].clone(); + } + + for _ in 1..count { + new_actions.append(actions.clone().as_mut()); + } + new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); + actions = new_actions; + } + + Vim::update(cx, |vim, _| vim.workspace_state.replaying = true); + let window = cx.window(); + cx.app_context() + .spawn(move |mut cx| async move { + editor.update(&mut cx, |editor, _| { + editor.show_local_selections = false; + })?; + for action in actions { + match action { + ReplayableAction::Action(action) => { + if should_replay(&action) { + window + .dispatch_action(editor.id(), action.as_ref(), &mut cx) + .ok_or_else(|| anyhow::anyhow!("window was closed")) + } else { + Ok(()) + } + } + ReplayableAction::Insertion { + text, + utf16_range_to_replace, + } => editor.update(&mut cx, |editor, cx| { + editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) + }), + }? + } + editor.update(&mut cx, |editor, _| { + editor.show_local_selections = true; + })?; + window + .dispatch_action(editor.id(), &EndRepeat, &mut cx) + .ok_or_else(|| anyhow::anyhow!("window was closed")) + }) + .detach_and_log_err(cx); } #[cfg(test)] @@ -203,7 +253,7 @@ mod test { deterministic.run_until_parked(); cx.simulate_shared_keystrokes(["."]).await; deterministic.run_until_parked(); - cx.set_shared_state("THE QUICK ˇbrown fox").await; + cx.assert_shared_state("THE QUICK ˇbrown fox").await; } #[gpui::test] @@ -424,4 +474,55 @@ mod test { }) .await; } + + #[gpui::test] + async fn test_repeat_motion_counts( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "ˇthe quick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await; + cx.assert_shared_state(indoc! { + "ˇ brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["j", "."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state(indoc! { + " brown + ˇ over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["j", "2", "."]).await; + deterministic.run_until_parked(); + cx.assert_shared_state(indoc! { + " brown + over + ˇe lazy dog" + }) + .await; + } + + #[gpui::test] + async fn test_record_interrupted( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("ˇhello\n", Mode::Normal); + cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]); + deterministic.run_until_parked(); + cx.assert_state("ˇjhello\n", Mode::Normal); + } } diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 1b3dcee6ad..877fff328b 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) { fn scroll(cx: &mut ViewContext, by: fn(c: Option) -> ScrollAmount) { Vim::update(cx, |vim, cx| { - let amount = by(vim.pop_number_operator(cx).map(|c| c as f32)); + let amount = by(vim.take_count(cx).map(|c| c as f32)); vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx)); }) } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 4ca0c42909..c9c04007d1 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -52,7 +52,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { search_bar.update(cx, |search_bar, cx| { @@ -119,7 +119,7 @@ pub fn move_to_internal( ) { Vim::update(cx, |vim, cx| { let pane = workspace.active_pane().clone(); - let count = vim.pop_number_operator(cx).unwrap_or(1); + let count = vim.take_count(cx).unwrap_or(1); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { let search = search_bar.update(cx, |search_bar, cx| { @@ -227,7 +227,7 @@ mod test { deterministic.run_until_parked(); cx.update_editor(|editor, cx| { - let highlights = editor.all_background_highlights(cx); + let highlights = editor.all_text_background_highlights(cx); assert_eq!(3, highlights.len()); assert_eq!( DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index d0dbb9e306..bb6e1abf92 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -11,7 +11,7 @@ pub(crate) fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { vim.start_recording(cx); - let count = vim.pop_number_operator(cx); + let count = vim.take_count(cx); substitute(vim, count, vim.state().mode == Mode::VisualLine, cx); }) }); @@ -22,7 +22,7 @@ pub(crate) fn init(cx: &mut AppContext) { if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) { vim.switch_mode(Mode::VisualLine, false, cx) } - let count = vim.pop_number_operator(cx); + let count = vim.take_count(cx); substitute(vim, count, true, cx) }) }); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 7359178f0e..8fd4049767 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -33,7 +33,6 @@ impl Default for Mode { #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] pub enum Operator { - Number(usize), Change, Delete, Yank, @@ -47,6 +46,12 @@ pub enum Operator { pub struct EditorState { pub mode: Mode, pub last_mode: Mode, + + /// pre_count is the number before an operator is specified (3 in 3d2d) + pub pre_count: Option, + /// post_count is the number after an operator is specified (2 in 3d2d) + pub post_count: Option, + pub operator_stack: Vec, } @@ -158,6 +163,10 @@ impl EditorState { } } + pub fn active_operator(&self) -> Option { + self.operator_stack.last().copied() + } + pub fn keymap_context_layer(&self) -> KeymapContext { let mut context = KeymapContext::default(); context.add_identifier("VimEnabled"); @@ -174,7 +183,13 @@ impl EditorState { context.add_identifier("VimControl"); } - let active_operator = self.operator_stack.last(); + if self.active_operator().is_none() && self.pre_count.is_some() + || self.active_operator().is_some() && self.post_count.is_some() + { + context.add_identifier("VimCount"); + } + + let active_operator = self.active_operator(); if let Some(active_operator) = active_operator { for context_flag in active_operator.context_flags().into_iter() { @@ -194,7 +209,6 @@ impl EditorState { impl Operator { pub fn id(&self) -> &'static str { match self { - Operator::Number(_) => "n", Operator::Object { around: false } => "i", Operator::Object { around: true } => "a", Operator::Change => "c", diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 9aa9fffc0a..559e065694 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -190,7 +190,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { search_bar.next_notification(&cx).await; cx.update_editor(|editor, cx| { - let highlights = editor.all_background_highlights(cx); + let highlights = editor.all_text_background_highlights(cx); assert_eq!(3, highlights.len()); assert_eq!( DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), @@ -574,3 +574,47 @@ async fn test_folds(cx: &mut gpui::TestAppContext) { "}) .await; } + +#[gpui::test] +async fn test_clear_counts(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + The quick brown + fox juˇmps over + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"]) + .await; + cx.assert_shared_state(indoc! {" + The quick brown + fox juˇ over + the lazy dog"}) + .await; +} + +#[gpui::test] +async fn test_zero(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["0"]).await; + cx.assert_shared_state(indoc! {" + ˇThe quick brown + fox jumps over + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["1", "0", "l"]).await; + cx.assert_shared_state(indoc! {" + The quick ˇbrown + fox jumps over + the lazy dog"}) + .await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index b433a6bfc0..e58f805a02 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -13,20 +13,13 @@ use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; -pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[ - ExemptionFeatures::DeletionOnEmptyLine, - ExemptionFeatures::OperatorAbortsOnFailedMotion, -]; +pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[]; /// Enum representing features we have tests for but which don't work, yet. Used /// to add exemptions and automatically #[derive(PartialEq, Eq)] pub enum ExemptionFeatures { // MOTIONS - // Deletions on empty lines miss some newlines - DeletionOnEmptyLine, - // When a motion fails, it should should not apply linewise operations - OperatorAbortsOnFailedMotion, // When an operator completes at the end of the file, an extra newline is left OperatorLastNewlineRemains, // Deleting a word on an empty line doesn't remove the newline @@ -68,6 +61,8 @@ pub struct NeovimBackedTestContext<'a> { last_set_state: Option, recent_keystrokes: Vec, + + is_dirty: bool, } impl<'a> NeovimBackedTestContext<'a> { @@ -81,6 +76,7 @@ impl<'a> NeovimBackedTestContext<'a> { last_set_state: None, recent_keystrokes: Default::default(), + is_dirty: false, } } @@ -128,6 +124,7 @@ impl<'a> NeovimBackedTestContext<'a> { self.last_set_state = Some(marked_text.to_string()); self.recent_keystrokes = Vec::new(); self.neovim.set_state(marked_text).await; + self.is_dirty = true; context_handle } @@ -153,6 +150,7 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn assert_shared_state(&mut self, marked_text: &str) { + self.is_dirty = false; let marked_text = marked_text.replace("•", " "); let neovim = self.neovim_state().await; let editor = self.editor_state(); @@ -258,6 +256,7 @@ impl<'a> NeovimBackedTestContext<'a> { } pub async fn assert_state_matches(&mut self) { + self.is_dirty = false; let neovim = self.neovim_state().await; let editor = self.editor_state(); let initial_state = self @@ -383,6 +382,17 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> { } } +// a common mistake in tests is to call set_shared_state when +// you mean asswert_shared_state. This notices that and lets +// you know. +impl<'a> Drop for NeovimBackedTestContext<'a> { + fn drop(&mut self) { + if self.is_dirty { + panic!("Test context was dropped after set_shared_state before assert_shared_state") + } + } +} + #[cfg(test)] mod test { use gpui::TestAppContext; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 03a74d46ce..7911717765 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -15,8 +15,8 @@ use anyhow::Result; use collections::{CommandPaletteFilter, HashMap}; use editor::{movement, Editor, EditorMode, Event}; use gpui::{ - actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, - Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action, + AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use language::{CursorShape, Point, Selection, SelectionGoal}; pub use mode_indicator::ModeIndicator; @@ -40,9 +40,12 @@ pub struct SwitchMode(pub Mode); pub struct PushOperator(pub Operator); #[derive(Clone, Deserialize, PartialEq)] -struct Number(u8); +struct Number(usize); -actions!(vim, [Tab, Enter]); +actions!( + vim, + [Tab, Enter, Object, InnerObject, FindForward, FindBackward] +); impl_actions!(vim, [Number, SwitchMode, PushOperator]); #[derive(Copy, Clone, Debug)] @@ -70,7 +73,7 @@ pub fn init(cx: &mut AppContext) { }, ); cx.add_action(|_: &mut Workspace, n: &Number, cx: _| { - Vim::update(cx, |vim, cx| vim.push_number(n, cx)); + Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx)); }); cx.add_action(|_: &mut Workspace, _: &Tab, cx| { @@ -225,23 +228,12 @@ impl Vim { let editor = self.active_editor.clone()?.upgrade(cx)?; Some(editor.update(cx, update)) } - // ~, shift-j, x, shift-x, p - // shift-c, shift-d, shift-i, i, a, o, shift-o, s - // c, d - // r - // TODO: shift-j? - // pub fn start_recording(&mut self, cx: &mut WindowContext) { if !self.workspace_state.replaying { self.workspace_state.recording = true; self.workspace_state.recorded_actions = Default::default(); - self.workspace_state.recorded_count = - if let Some(Operator::Number(number)) = self.active_operator() { - Some(number) - } else { - None - }; + self.workspace_state.recorded_count = None; let selections = self .active_editor @@ -286,6 +278,16 @@ impl Vim { } } + pub fn stop_recording_immediately(&mut self, action: Box) { + if self.workspace_state.recording { + self.workspace_state + .recorded_actions + .push(ReplayableAction::Action(action.boxed_clone())); + self.workspace_state.recording = false; + self.workspace_state.stop_recording_after_next_action = false; + } + } + pub fn record_current_action(&mut self, cx: &mut WindowContext) { self.start_recording(cx); self.stop_recording(); @@ -300,6 +302,9 @@ impl Vim { state.mode = mode; state.operator_stack.clear(); }); + if mode != Mode::Insert { + self.take_count(cx); + } cx.emit_global(VimEvent::ModeChanged { mode }); @@ -352,6 +357,39 @@ impl Vim { }); } + fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) { + if self.active_operator().is_some() { + self.update_state(|state| { + state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number) + }) + } else { + self.update_state(|state| { + state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number) + }) + } + // update the keymap so that 0 works + self.sync_vim_settings(cx) + } + + fn take_count(&mut self, cx: &mut WindowContext) -> Option { + if self.workspace_state.replaying { + return self.workspace_state.recorded_count; + } + + let count = if self.state().post_count == None && self.state().pre_count == None { + return None; + } else { + Some(self.update_state(|state| { + state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1) + })) + }; + if self.workspace_state.recording { + self.workspace_state.recorded_count = count; + } + self.sync_vim_settings(cx); + count + } + fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) { if matches!( operator, @@ -363,15 +401,6 @@ impl Vim { self.sync_vim_settings(cx); } - fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) { - if let Some(Operator::Number(current_number)) = self.active_operator() { - self.pop_operator(cx); - self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx); - } else { - self.push_operator(Operator::Number(*number as usize), cx); - } - } - fn maybe_pop_operator(&mut self) -> Option { self.update_state(|state| state.operator_stack.pop()) } @@ -382,22 +411,8 @@ impl Vim { self.sync_vim_settings(cx); popped_operator } - - fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option { - if self.workspace_state.replaying { - if let Some(number) = self.workspace_state.recorded_count { - return Some(number); - } - } - - if let Some(Operator::Number(number)) = self.active_operator() { - self.pop_operator(cx); - return Some(number); - } - None - } - fn clear_operator(&mut self, cx: &mut WindowContext) { + self.take_count(cx); self.update_state(|state| state.operator_stack.clear()); self.sync_vim_settings(cx); } diff --git a/crates/vim/test_data/test_clear_counts.json b/crates/vim/test_data/test_clear_counts.json new file mode 100644 index 0000000000..6ef6b36017 --- /dev/null +++ b/crates/vim/test_data/test_clear_counts.json @@ -0,0 +1,7 @@ +{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} +{"Key":"4"} +{"Key":"escape"} +{"Key":"3"} +{"Key":"d"} +{"Key":"l"} +{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_with_counts.json b/crates/vim/test_data/test_delete_with_counts.json new file mode 100644 index 0000000000..de19c5d29d --- /dev/null +++ b/crates/vim/test_data/test_delete_with_counts.json @@ -0,0 +1,16 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"d"} +{"Key":"2"} +{"Key":"d"} +{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"2"} +{"Key":"d"} +{"Key":"d"} +{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}} +{"Key":"2"} +{"Key":"d"} +{"Key":"2"} +{"Key":"d"} +{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dot_repeat.json b/crates/vim/test_data/test_dot_repeat.json index f1a1a3c138..331ef52ecb 100644 --- a/crates/vim/test_data/test_dot_repeat.json +++ b/crates/vim/test_data/test_dot_repeat.json @@ -35,4 +35,4 @@ {"Key":"."} {"Put":{"state":"THE QUIˇck brown fox"}} {"Key":"."} -{"Put":{"state":"THE QUICK ˇbrown fox"}} +{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_with_counts.json b/crates/vim/test_data/test_insert_with_counts.json new file mode 100644 index 0000000000..470888cf6e --- /dev/null +++ b/crates/vim/test_data/test_insert_with_counts.json @@ -0,0 +1,36 @@ +{"Put":{"state":"ˇhello\n"}} +{"Key":"5"} +{"Key":"i"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"5"} +{"Key":"a"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}} +{"Key":"4"} +{"Key":"shift-i"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}} +{"Key":"3"} +{"Key":"shift-a"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"o"} +{"Key":"o"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"shift-o"} +{"Key":"o"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_with_repeat.json b/crates/vim/test_data/test_insert_with_repeat.json new file mode 100644 index 0000000000..ac6637633c --- /dev/null +++ b/crates/vim/test_data/test_insert_with_repeat.json @@ -0,0 +1,23 @@ +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"i"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"."} +{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"2"} +{"Key":"o"} +{"Key":"k"} +{"Key":"k"} +{"Key":"escape"} +{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} +{"Key":"1"} +{"Key":"."} +{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_motion_counts.json b/crates/vim/test_data/test_repeat_motion_counts.json new file mode 100644 index 0000000000..c39b8b09c0 --- /dev/null +++ b/crates/vim/test_data/test_repeat_motion_counts.json @@ -0,0 +1,13 @@ +{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"3"} +{"Key":"d"} +{"Key":"3"} +{"Key":"l"} +{"Get":{"state":"ˇ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} +{"Key":"j"} +{"Key":"."} +{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}} +{"Key":"j"} +{"Key":"2"} +{"Key":"."} +{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_zero.json b/crates/vim/test_data/test_zero.json new file mode 100644 index 0000000000..bc1253deb5 --- /dev/null +++ b/crates/vim/test_data/test_zero.json @@ -0,0 +1,7 @@ +{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"0"} +{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} +{"Key":"1"} +{"Key":"0"} +{"Key":"l"} +{"Get":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 55b44e9673..36865eb22c 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -292,7 +292,7 @@ pub mod simple_message_notification { .with_child( MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") + Svg::new("icons/x.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 90a1fdd3f2..1b48275ee9 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -337,7 +337,7 @@ impl Pane { // New menu .with_child(Self::render_tab_bar_button( 0, - "icons/plus_12.svg", + "icons/plus.svg", false, Some(("New...".into(), None)), cx, @@ -352,7 +352,7 @@ impl Pane { )) .with_child(Self::render_tab_bar_button( 1, - "icons/split_12.svg", + "icons/split.svg", false, Some(("Split Pane".into(), None)), cx, @@ -369,10 +369,10 @@ impl Pane { let icon_path; let tooltip_label; if pane.is_zoomed() { - icon_path = "icons/minimize_8.svg"; + icon_path = "icons/minimize.svg"; tooltip_label = "Zoom In"; } else { - icon_path = "icons/maximize_8.svg"; + icon_path = "icons/maximize.svg"; tooltip_label = "Zoom In"; } @@ -1535,7 +1535,7 @@ impl Pane { let close_element = if hovered { let item_id = item.id(); enum TabCloseButton {} - let icon = Svg::new("icons/x_mark_8.svg"); + let icon = Svg::new("icons/x.svg"); MouseEventHandler::new::(item_id, cx, |mouse_state, _| { if mouse_state.hovered() { icon.with_color(tab_style.icon_close_active) @@ -1701,7 +1701,7 @@ impl View for Pane { let mut tab_row = Flex::row() .with_child(nav_button( - "icons/arrow_left_16.svg", + "icons/arrow_left.svg", button_style.clone(), nav_button_height, tooltip_style.clone(), @@ -1726,7 +1726,7 @@ impl View for Pane { )) .with_child( nav_button( - "icons/arrow_right_16.svg", + "icons/arrow_right.svg", button_style.clone(), nav_button_height, tooltip_style, diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 7a470db7c9..ddde5c3554 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -1,4 +1,4 @@ -use std::any::Any; +use std::{any::Any, sync::Arc}; use gpui::{ AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle, @@ -25,6 +25,8 @@ pub struct SearchOptions { pub case: bool, pub word: bool, pub regex: bool, + /// Specifies whether the item supports search & replace. + pub replacement: bool, } pub trait SearchableItem: Item { @@ -35,6 +37,7 @@ pub trait SearchableItem: Item { case: true, word: true, regex: true, + replacement: true, } } fn to_search_event( @@ -52,6 +55,7 @@ pub trait SearchableItem: Item { cx: &mut ViewContext, ); fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext); fn match_index_for_direction( &mut self, matches: &Vec, @@ -74,7 +78,7 @@ pub trait SearchableItem: Item { } fn find_matches( &mut self, - query: SearchQuery, + query: Arc, cx: &mut ViewContext, ) -> Task>; fn active_match_index( @@ -103,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle { cx: &mut WindowContext, ); fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); + fn replace(&self, _: &Box, _: &SearchQuery, _: &mut WindowContext); fn match_index_for_direction( &self, matches: &Vec>, @@ -113,7 +118,7 @@ pub trait SearchableItemHandle: ItemHandle { ) -> usize; fn find_matches( &self, - query: SearchQuery, + query: Arc, cx: &mut WindowContext, ) -> Task>>; fn active_match_index( @@ -189,7 +194,7 @@ impl SearchableItemHandle for ViewHandle { } fn find_matches( &self, - query: SearchQuery, + query: Arc, cx: &mut WindowContext, ) -> Task>> { let matches = self.update(cx, |this, cx| this.find_matches(query, cx)); @@ -209,6 +214,11 @@ impl SearchableItemHandle for ViewHandle { let matches = downcast_matches(matches); self.update(cx, |this, cx| this.active_match_index(matches, cx)) } + + fn replace(&self, matches: &Box, query: &SearchQuery, cx: &mut WindowContext) { + let matches = matches.downcast_ref().unwrap(); + self.update(cx, |this, cx| this.replace(matches, query, cx)) + } } fn downcast_matches(matches: &Vec>) -> Vec { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 1d014197e1..b2339f998f 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.104.0" +version = "0.105.0" publish = false [lib] diff --git a/script/deploy b/script/deploy index f675da6a99..d32d387339 100755 --- a/script/deploy +++ b/script/deploy @@ -13,10 +13,11 @@ version=$2 export_vars_for_environment ${environment} image_id=$(image_id_for_version ${version}) +export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header) export ZED_KUBE_NAMESPACE=${environment} export ZED_IMAGE_ID=${image_id} target_zed_kube_cluster envsubst < crates/collab/k8s/manifest.template.yml | kubectl apply -f - -echo "deployed collab v${version} to ${environment}" \ No newline at end of file +echo "deployed collab v${version} to ${environment}" diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 7a41f45e53..cc6ee4b080 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -141,26 +141,26 @@ export default function assistant(): any { background: background(theme.highest), }, hamburger_button: tab_bar_button(theme, { - icon: "icons/hamburger_15.svg", + icon: "icons/menu.svg", }), split_button: tab_bar_button(theme, { - icon: "icons/split_message_15.svg", + icon: "icons/split_message.svg", }), quote_button: tab_bar_button(theme, { - icon: "icons/radix/quote.svg", + icon: "icons/quote.svg", }), assist_button: tab_bar_button(theme, { - icon: "icons/radix/magic-wand.svg", + icon: "icons/magic-wand.svg", }), zoom_in_button: tab_bar_button(theme, { - icon: "icons/radix/maximize.svg", + icon: "icons/maximize.svg", }), zoom_out_button: tab_bar_button(theme, { - icon: "icons/radix/minimize.svg", + icon: "icons/minimize.svg", }), plus_button: tab_bar_button(theme, { - icon: "icons/radix/plus.svg", + icon: "icons/plus.svg", }), title: { ...text(theme.highest, "sans", "default", { size: "xs" }), diff --git a/styles/src/style_tree/contact_notification.ts b/styles/src/style_tree/contact_notification.ts index 365e3a646d..0f52201c16 100644 --- a/styles/src/style_tree/contact_notification.ts +++ b/styles/src/style_tree/contact_notification.ts @@ -42,10 +42,10 @@ export default function contact_notification(): any { dismiss_button: { default: { color: foreground(theme.lowest, "variant"), - icon_width: 8, - icon_height: 8, - button_width: 8, - button_height: 8, + icon_width: 14, + icon_height: 14, + button_width: 14, + button_height: 14, hover: { color: foreground(theme.lowest, "hovered"), }, diff --git a/styles/src/style_tree/copilot.ts b/styles/src/style_tree/copilot.ts index f002db5ef5..4e3e867c0a 100644 --- a/styles/src/style_tree/copilot.ts +++ b/styles/src/style_tree/copilot.ts @@ -41,7 +41,7 @@ export default function copilot(): any { base: { icon: svg( foreground(theme.middle, "variant"), - "icons/link_out_12.svg", + "icons/external_link.svg", 12, 12 ), @@ -91,7 +91,7 @@ export default function copilot(): any { base: { icon: svg( foreground(theme.middle, "variant"), - "icons/x_mark_8.svg", + "icons/x.svg", 8, 8 ), @@ -112,7 +112,7 @@ export default function copilot(): any { hovered: { icon: svg( foreground(theme.middle, "on"), - "icons/x_mark_8.svg", + "icons/x.svg", 8, 8 ), @@ -120,7 +120,7 @@ export default function copilot(): any { clicked: { icon: svg( foreground(theme.middle, "base"), - "icons/x_mark_8.svg", + "icons/x.svg", 8, 8 ), @@ -141,7 +141,7 @@ export default function copilot(): any { header: { icon: svg( foreground(theme.middle, "default"), - "icons/zed_plus_copilot_32.svg", + "icons/zed_x_copilot.svg", 92, 32 ), diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 8fd512c5d4..a15cd8dbb8 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -92,8 +92,8 @@ export default function editor(): any { }, folds: { icon_margin_scale: 2.5, - folded_icon: "icons/chevron_right_8.svg", - foldable_icon: "icons/chevron_down_8.svg", + folded_icon: "icons/chevron_right.svg", + foldable_icon: "icons/chevron_down.svg", indicator: toggleable({ base: interactive({ base: { diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 8174690fde..b0ac023c09 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -30,15 +30,13 @@ export default function search(): any { selection: theme.players[0], text: text(theme.highest, "mono", "default"), border: border(theme.highest), - margin: { - right: SEARCH_ROW_SPACING, - }, padding: { top: 4, bottom: 4, left: 10, right: 4, }, + margin: { right: SEARCH_ROW_SPACING } } const include_exclude_editor = { @@ -125,7 +123,7 @@ export default function search(): any { button_width: 32, background: background(theme.highest, "on"), - corner_radius: 2, + corner_radius: 6, margin: { right: 2 }, border: { width: 1, @@ -185,26 +183,6 @@ export default function search(): any { }, }, }), - // Search tool buttons - // HACK: This is not how disabled elements should be created - // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled - action_button: toggleable({ - state: { - inactive: text_button({ - variant: "ghost", - layer: theme.highest, - disabled: true, - margin: { right: SEARCH_ROW_SPACING }, - text_properties: { size: "sm" }, - }), - active: text_button({ - variant: "ghost", - layer: theme.highest, - margin: { right: SEARCH_ROW_SPACING }, - text_properties: { size: "sm" }, - }), - }, - }), editor, invalid_editor: { ...editor, @@ -218,12 +196,12 @@ export default function search(): any { match_index: { ...text(theme.highest, "mono", { size: "sm" }), padding: { + left: SEARCH_ROW_SPACING, right: SEARCH_ROW_SPACING, }, }, option_button_group: { padding: { - left: SEARCH_ROW_SPACING, right: SEARCH_ROW_SPACING, }, }, @@ -397,7 +375,64 @@ export default function search(): any { search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, - modes_container: {}, + modes_container: { + padding: { + right: SEARCH_ROW_SPACING, + } + }, + replace_icon: { + icon: { + color: foreground(theme.highest, "disabled"), + asset: "icons/replace.svg", + dimensions: { + width: 14, + height: 14, + }, + }, + container: { + margin: { right: 4 }, + padding: { left: 1, right: 1 }, + }, + }, + action_button: interactive({ + base: { + icon_size: 14, + color: foreground(theme.highest, "variant"), + + button_width: 32, + background: background(theme.highest, "on"), + corner_radius: 6, + margin: { right: 2 }, + border: { + width: 1, + color: background(theme.highest, "on"), + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1, + color: background(theme.highest, "on", "hovered"), + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1, + color: background(theme.highest, "on", "pressed"), + }, + }, + }, + }), ...search_results(), } } diff --git a/styles/src/style_tree/simple_message_notification.ts b/styles/src/style_tree/simple_message_notification.ts index 35133f04a2..45ecc0eab9 100644 --- a/styles/src/style_tree/simple_message_notification.ts +++ b/styles/src/style_tree/simple_message_notification.ts @@ -37,10 +37,10 @@ export default function simple_message_notification(): any { dismiss_button: interactive({ base: { color: foreground(theme.middle), - icon_width: 8, - icon_height: 8, - button_width: 8, - button_height: 8, + icon_width: 14, + icon_height: 14, + button_width: 14, + button_height: 14, }, state: { hovered: { diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 23ff03a6a3..e1ad1c6c8b 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -32,7 +32,7 @@ export default function tab_bar(): any { type_icon_width: 14, // Close icons - close_icon_width: 8, + close_icon_width: 14, icon_close: foreground(layer, "variant"), icon_close_active: foreground(layer, "hovered"),