Merge branch 'main' into Z-1292/show_search_results_in_scrollbar

Piotr Osiewicz created

Change summary

.cargo/config.toml                                                              |    2 
.config/nextest.toml                                                            |    6 
.github/workflows/ci.yml                                                        |    3 
.gitignore                                                                      |    3 
Cargo.lock                                                                      |  582 
Cargo.toml                                                                      |    6 
assets/icons/assist_15.svg                                                      |    0 
assets/icons/hamburger_15.svg                                                   |    3 
assets/icons/quote_15.svg                                                       |    0 
assets/icons/radix/accessibility.svg                                            |    4 
assets/icons/radix/activity-log.svg                                             |    4 
assets/icons/radix/align-baseline.svg                                           |    4 
assets/icons/radix/align-bottom.svg                                             |    8 
assets/icons/radix/align-center-horizontally.svg                                |    8 
assets/icons/radix/align-center-vertically.svg                                  |    8 
assets/icons/radix/align-center.svg                                             |    8 
assets/icons/radix/align-end.svg                                                |    8 
assets/icons/radix/align-horizontal-centers.svg                                 |    8 
assets/icons/radix/align-left.svg                                               |    8 
assets/icons/radix/align-right.svg                                              |    8 
assets/icons/radix/align-start.svg                                              |    8 
assets/icons/radix/align-stretch.svg                                            |    8 
assets/icons/radix/align-top.svg                                                |    8 
assets/icons/radix/align-vertical-centers.svg                                   |    8 
assets/icons/radix/all-sides.svg                                                |    8 
assets/icons/radix/angle.svg                                                    |    8 
assets/icons/radix/archive.svg                                                  |    8 
assets/icons/radix/arrow-bottom-left.svg                                        |    8 
assets/icons/radix/arrow-bottom-right.svg                                       |    8 
assets/icons/radix/arrow-down.svg                                               |    8 
assets/icons/radix/arrow-left.svg                                               |    8 
assets/icons/radix/arrow-right.svg                                              |    8 
assets/icons/radix/arrow-top-left.svg                                           |    8 
assets/icons/radix/arrow-top-right.svg                                          |    8 
assets/icons/radix/arrow-up.svg                                                 |    8 
assets/icons/radix/aspect-ratio.svg                                             |    8 
assets/icons/radix/avatar.svg                                                   |    4 
assets/icons/radix/backpack.svg                                                 |    8 
assets/icons/radix/badge.svg                                                    |    8 
assets/icons/radix/bar-chart.svg                                                |    8 
assets/icons/radix/bell.svg                                                     |    4 
assets/icons/radix/blending-mode.svg                                            |    8 
assets/icons/radix/bookmark-filled.svg                                          |    8 
assets/icons/radix/bookmark.svg                                                 |    8 
assets/icons/radix/border-all.svg                                               |   17 
assets/icons/radix/border-bottom.svg                                            |   29 
assets/icons/radix/border-dashed.svg                                            |    8 
assets/icons/radix/border-dotted.svg                                            |    8 
assets/icons/radix/border-left.svg                                              |   29 
assets/icons/radix/border-none.svg                                              |   35 
assets/icons/radix/border-right.svg                                             |   29 
assets/icons/radix/border-solid.svg                                             |    8 
assets/icons/radix/border-split.svg                                             |   21 
assets/icons/radix/border-style.svg                                             |    4 
assets/icons/radix/border-top.svg                                               |   29 
assets/icons/radix/border-width.svg                                             |    8 
assets/icons/radix/box-model.svg                                                |    8 
assets/icons/radix/box.svg                                                      |    8 
assets/icons/radix/button.svg                                                   |    8 
assets/icons/radix/calendar.svg                                                 |    4 
assets/icons/radix/camera.svg                                                   |    8 
assets/icons/radix/card-stack-minus.svg                                         |    8 
assets/icons/radix/card-stack-plus.svg                                          |    8 
assets/icons/radix/card-stack.svg                                               |    8 
assets/icons/radix/caret-down.svg                                               |    8 
assets/icons/radix/caret-left.svg                                               |    8 
assets/icons/radix/caret-right.svg                                              |    8 
assets/icons/radix/caret-sort.svg                                               |    8 
assets/icons/radix/caret-up.svg                                                 |    8 
assets/icons/radix/chat-bubble.svg                                              |    8 
assets/icons/radix/check-circled.svg                                            |    8 
assets/icons/radix/check.svg                                                    |    8 
assets/icons/radix/checkbox.svg                                                 |    8 
assets/icons/radix/chevron-down.svg                                             |    8 
assets/icons/radix/chevron-left.svg                                             |    8 
assets/icons/radix/chevron-right.svg                                            |    8 
assets/icons/radix/chevron-up.svg                                               |    8 
assets/icons/radix/circle-backslash.svg                                         |    8 
assets/icons/radix/circle.svg                                                   |    8 
assets/icons/radix/clipboard-copy.svg                                           |    4 
assets/icons/radix/clipboard.svg                                                |    8 
assets/icons/radix/clock.svg                                                    |    8 
assets/icons/radix/code.svg                                                     |    8 
assets/icons/radix/codesandbox-logo.svg                                         |    4 
assets/icons/radix/color-wheel.svg                                              |    8 
assets/icons/radix/column-spacing.svg                                           |    4 
assets/icons/radix/columns.svg                                                  |    8 
assets/icons/radix/commit.svg                                                   |    8 
assets/icons/radix/component-1.svg                                              |    4 
assets/icons/radix/component-2.svg                                              |    4 
assets/icons/radix/component-boolean.svg                                        |    8 
assets/icons/radix/component-instance.svg                                       |    8 
assets/icons/radix/component-none.svg                                           |    8 
assets/icons/radix/component-placeholder.svg                                    |    4 
assets/icons/radix/container.svg                                                |    4 
assets/icons/radix/cookie.svg                                                   |    4 
assets/icons/radix/copy.svg                                                     |    8 
assets/icons/radix/corner-bottom-left.svg                                       |    8 
assets/icons/radix/corner-bottom-right.svg                                      |    8 
assets/icons/radix/corner-top-left.svg                                          |    8 
assets/icons/radix/corner-top-right.svg                                         |    8 
assets/icons/radix/corners.svg                                                  |    4 
assets/icons/radix/countdown-timer.svg                                          |    8 
assets/icons/radix/counter-clockwise-clock.svg                                  |    8 
assets/icons/radix/crop.svg                                                     |    8 
assets/icons/radix/cross-1.svg                                                  |    8 
assets/icons/radix/cross-2.svg                                                  |    8 
assets/icons/radix/cross-circled.svg                                            |    8 
assets/icons/radix/crosshair-1.svg                                              |    8 
assets/icons/radix/crosshair-2.svg                                              |    8 
assets/icons/radix/crumpled-paper.svg                                           |    4 
assets/icons/radix/cube.svg                                                     |    8 
assets/icons/radix/cursor-arrow.svg                                             |    8 
assets/icons/radix/cursor-text.svg                                              |    4 
assets/icons/radix/dash.svg                                                     |    8 
assets/icons/radix/dashboard.svg                                                |    4 
assets/icons/radix/desktop-mute.svg                                             |    4 
assets/icons/radix/desktop.svg                                                  |    8 
assets/icons/radix/dimensions.svg                                               |    4 
assets/icons/radix/disc.svg                                                     |    8 
assets/icons/radix/discord-logo.svg                                             |    5 
assets/icons/radix/divider-horizontal.svg                                       |    8 
assets/icons/radix/divider-vertical.svg                                         |    8 
assets/icons/radix/dot-filled.svg                                               |    6 
assets/icons/radix/dot-solid.svg                                                |    6 
assets/icons/radix/dot.svg                                                      |    8 
assets/icons/radix/dots-horizontal.svg                                          |    8 
assets/icons/radix/dots-vertical.svg                                            |    8 
assets/icons/radix/double-arrow-down.svg                                        |    8 
assets/icons/radix/double-arrow-left.svg                                        |    8 
assets/icons/radix/double-arrow-right.svg                                       |    8 
assets/icons/radix/double-arrow-up.svg                                          |    8 
assets/icons/radix/download.svg                                                 |    8 
assets/icons/radix/drag-handle-dots-1.svg                                       |   26 
assets/icons/radix/drag-handle-dots-2.svg                                       |    4 
assets/icons/radix/drag-handle-horizontal.svg                                   |    8 
assets/icons/radix/drag-handle-vertical.svg                                     |    8 
assets/icons/radix/drawing-pin-filled.svg                                       |   14 
assets/icons/radix/drawing-pin-solid.svg                                        |   14 
assets/icons/radix/drawing-pin.svg                                              |    8 
assets/icons/radix/dropdown-menu.svg                                            |    8 
assets/icons/radix/enter-full-screen.svg                                        |    8 
assets/icons/radix/enter.svg                                                    |    8 
assets/icons/radix/envelope-closed.svg                                          |    8 
assets/icons/radix/envelope-open.svg                                            |    8 
assets/icons/radix/eraser.svg                                                   |    8 
assets/icons/radix/exclamation-triangle.svg                                     |    8 
assets/icons/radix/exit-full-screen.svg                                         |    8 
assets/icons/radix/exit.svg                                                     |    8 
assets/icons/radix/external-link.svg                                            |    8 
assets/icons/radix/eye-closed.svg                                               |    4 
assets/icons/radix/eye-none.svg                                                 |    8 
assets/icons/radix/eye-open.svg                                                 |    8 
assets/icons/radix/face.svg                                                     |    4 
assets/icons/radix/figma-logo.svg                                               |    4 
assets/icons/radix/file-minus.svg                                               |    8 
assets/icons/radix/file-plus.svg                                                |    8 
assets/icons/radix/file-text.svg                                                |    8 
assets/icons/radix/file.svg                                                     |    8 
assets/icons/radix/font-bold.svg                                                |    6 
assets/icons/radix/font-family.svg                                              |    6 
assets/icons/radix/font-italic.svg                                              |    8 
assets/icons/radix/font-roman.svg                                               |    8 
assets/icons/radix/font-size.svg                                                |    4 
assets/icons/radix/font-style.svg                                               |    4 
assets/icons/radix/frame.svg                                                    |    8 
assets/icons/radix/framer-logo.svg                                              |    8 
assets/icons/radix/gear.svg                                                     |    4 
assets/icons/radix/github-logo.svg                                              |    4 
assets/icons/radix/globe.svg                                                    |   26 
assets/icons/radix/grid.svg                                                     |    8 
assets/icons/radix/group.svg                                                    |    4 
assets/icons/radix/half-1.svg                                                   |    8 
assets/icons/radix/half-2.svg                                                   |    8 
assets/icons/radix/hamburger-menu.svg                                           |    8 
assets/icons/radix/hand.svg                                                     |    4 
assets/icons/radix/heading.svg                                                  |    8 
assets/icons/radix/heart-filled.svg                                             |    8 
assets/icons/radix/heart.svg                                                    |    4 
assets/icons/radix/height.svg                                                   |    8 
assets/icons/radix/hobby-knife.svg                                              |    8 
assets/icons/radix/home.svg                                                     |    8 
assets/icons/radix/iconjar-logo.svg                                             |    4 
assets/icons/radix/id-card.svg                                                  |    8 
assets/icons/radix/image.svg                                                    |    8 
assets/icons/radix/info-circled.svg                                             |    8 
assets/icons/radix/inner-shadow.svg                                             |   78 
assets/icons/radix/input.svg                                                    |    4 
assets/icons/radix/instagram-logo.svg                                           |    2 
assets/icons/radix/justify-center.svg                                           |    8 
assets/icons/radix/justify-end.svg                                              |    8 
assets/icons/radix/justify-start.svg                                            |    8 
assets/icons/radix/justify-stretch.svg                                          |    8 
assets/icons/radix/keyboard.svg                                                 |    7 
assets/icons/radix/lap-timer.svg                                                |    8 
assets/icons/radix/laptop.svg                                                   |    8 
assets/icons/radix/layers.svg                                                   |    4 
assets/icons/radix/layout.svg                                                   |    8 
assets/icons/radix/letter-case-capitalize.svg                                   |    4 
assets/icons/radix/letter-case-lowercase.svg                                    |    4 
assets/icons/radix/letter-case-toggle.svg                                       |    4 
assets/icons/radix/letter-case-uppercase.svg                                    |    8 
assets/icons/radix/letter-spacing.svg                                           |    4 
assets/icons/radix/lightning-bolt.svg                                           |    8 
assets/icons/radix/line-height.svg                                              |    4 
assets/icons/radix/link-1.svg                                                   |    4 
assets/icons/radix/link-2.svg                                                   |    4 
assets/icons/radix/link-break-1.svg                                             |    4 
assets/icons/radix/link-break-2.svg                                             |    4 
assets/icons/radix/link-none-1.svg                                              |    4 
assets/icons/radix/link-none-2.svg                                              |    4 
assets/icons/radix/linkedin-logo.svg                                            |    8 
assets/icons/radix/list-bullet.svg                                              |    8 
assets/icons/radix/lock-closed.svg                                              |    8 
assets/icons/radix/lock-open-1.svg                                              |    8 
assets/icons/radix/lock-open-2.svg                                              |    8 
assets/icons/radix/loop.svg                                                     |    8 
assets/icons/radix/magic-wand.svg                                               |    4 
assets/icons/radix/magnifying-glass.svg                                         |    8 
assets/icons/radix/margin.svg                                                   |    4 
assets/icons/radix/mask-off.svg                                                 |    8 
assets/icons/radix/mask-on.svg                                                  |    8 
assets/icons/radix/mic-mute.svg                                                 |    1 
assets/icons/radix/mic.svg                                                      |    1 
assets/icons/radix/minus-circled.svg                                            |    8 
assets/icons/radix/minus.svg                                                    |    8 
assets/icons/radix/mix.svg                                                      |    4 
assets/icons/radix/mixer-horizontal.svg                                         |    4 
assets/icons/radix/mixer-vertical.svg                                           |    4 
assets/icons/radix/mobile.svg                                                   |    8 
assets/icons/radix/modulz-logo.svg                                              |    8 
assets/icons/radix/moon.svg                                                     |    4 
assets/icons/radix/move.svg                                                     |    4 
assets/icons/radix/notion-logo.svg                                              |    2 
assets/icons/radix/opacity.svg                                                  |    8 
assets/icons/radix/open-in-new-window.svg                                       |   10 
assets/icons/radix/outer-shadow.svg                                             |   43 
assets/icons/radix/overline.svg                                                 |    8 
assets/icons/radix/padding.svg                                                  |    4 
assets/icons/radix/paper-plane.svg                                              |    8 
assets/icons/radix/pause.svg                                                    |    8 
assets/icons/radix/pencil-1.svg                                                 |    8 
assets/icons/radix/pencil-2.svg                                                 |    4 
assets/icons/radix/person.svg                                                   |    8 
assets/icons/radix/pie-chart.svg                                                |    8 
assets/icons/radix/pilcrow.svg                                                  |    8 
assets/icons/radix/pin-bottom.svg                                               |    8 
assets/icons/radix/pin-left.svg                                                 |    8 
assets/icons/radix/pin-right.svg                                                |    8 
assets/icons/radix/pin-top.svg                                                  |    8 
assets/icons/radix/play.svg                                                     |    8 
assets/icons/radix/plus-circled.svg                                             |    8 
assets/icons/radix/plus.svg                                                     |    8 
assets/icons/radix/question-mark-circled.svg                                    |    4 
assets/icons/radix/question-mark.svg                                            |    8 
assets/icons/radix/quote.svg                                                    |    4 
assets/icons/radix/radiobutton.svg                                              |    8 
assets/icons/radix/reader.svg                                                   |    4 
assets/icons/radix/reload.svg                                                   |    8 
assets/icons/radix/reset.svg                                                    |    8 
assets/icons/radix/resume.svg                                                   |    8 
assets/icons/radix/rocket.svg                                                   |    4 
assets/icons/radix/rotate-counter-clockwise.svg                                 |    8 
assets/icons/radix/row-spacing.svg                                              |    4 
assets/icons/radix/rows.svg                                                     |    8 
assets/icons/radix/ruler-horizontal.svg                                         |    8 
assets/icons/radix/ruler-square.svg                                             |    4 
assets/icons/radix/scissors.svg                                                 |    4 
assets/icons/radix/section.svg                                                  |    4 
assets/icons/radix/sewing-pin-filled.svg                                        |    8 
assets/icons/radix/sewing-pin-solid.svg                                         |    8 
assets/icons/radix/sewing-pin.svg                                               |    8 
assets/icons/radix/shadow-inner.svg                                             |   78 
assets/icons/radix/shadow-none.svg                                              |   78 
assets/icons/radix/shadow-outer.svg                                             |   43 
assets/icons/radix/shadow.svg                                                   |   78 
assets/icons/radix/share-1.svg                                                  |    4 
assets/icons/radix/share-2.svg                                                  |    4 
assets/icons/radix/shuffle.svg                                                  |    4 
assets/icons/radix/size.svg                                                     |    8 
assets/icons/radix/sketch-logo.svg                                              |    4 
assets/icons/radix/slash.svg                                                    |    8 
assets/icons/radix/slider.svg                                                   |    8 
assets/icons/radix/space-between-horizontally.svg                               |    8 
assets/icons/radix/space-between-vertically.svg                                 |    8 
assets/icons/radix/space-evenly-horizontally.svg                                |    4 
assets/icons/radix/space-evenly-vertically.svg                                  |    8 
assets/icons/radix/speaker-loud.svg                                             |    4 
assets/icons/radix/speaker-moderate.svg                                         |    8 
assets/icons/radix/speaker-off.svg                                              |    8 
assets/icons/radix/speaker-quiet.svg                                            |    8 
assets/icons/radix/square.svg                                                   |    8 
assets/icons/radix/stack.svg                                                    |    8 
assets/icons/radix/star-filled.svg                                              |    6 
assets/icons/radix/star.svg                                                     |    4 
assets/icons/radix/stitches-logo.svg                                            |    4 
assets/icons/radix/stop.svg                                                     |    8 
assets/icons/radix/stopwatch.svg                                                |    8 
assets/icons/radix/stretch-horizontally.svg                                     |    8 
assets/icons/radix/stretch-vertically.svg                                       |    8 
assets/icons/radix/strikethrough.svg                                            |    8 
assets/icons/radix/sun.svg                                                      |    4 
assets/icons/radix/switch.svg                                                   |    8 
assets/icons/radix/symbol.svg                                                   |    4 
assets/icons/radix/table.svg                                                    |    8 
assets/icons/radix/target.svg                                                   |    4 
assets/icons/radix/text-align-bottom.svg                                        |    4 
assets/icons/radix/text-align-center.svg                                        |    8 
assets/icons/radix/text-align-justify.svg                                       |    8 
assets/icons/radix/text-align-left.svg                                          |    8 
assets/icons/radix/text-align-middle.svg                                        |    4 
assets/icons/radix/text-align-right.svg                                         |    8 
assets/icons/radix/text-align-top.svg                                           |    4 
assets/icons/radix/text-none.svg                                                |    8 
assets/icons/radix/text.svg                                                     |    8 
assets/icons/radix/thick-arrow-down.svg                                         |    8 
assets/icons/radix/thick-arrow-left.svg                                         |    8 
assets/icons/radix/thick-arrow-right.svg                                        |    8 
assets/icons/radix/thick-arrow-up.svg                                           |    8 
assets/icons/radix/timer.svg                                                    |    8 
assets/icons/radix/tokens.svg                                                   |    8 
assets/icons/radix/track-next.svg                                               |    8 
assets/icons/radix/track-previous.svg                                           |    8 
assets/icons/radix/transform.svg                                                |    4 
assets/icons/radix/transparency-grid.svg                                        |    9 
assets/icons/radix/trash.svg                                                    |    8 
assets/icons/radix/triangle-down.svg                                            |    3 
assets/icons/radix/triangle-left.svg                                            |    3 
assets/icons/radix/triangle-right.svg                                           |    3 
assets/icons/radix/triangle-up.svg                                              |    3 
assets/icons/radix/twitter-logo.svg                                             |    4 
assets/icons/radix/underline.svg                                                |    8 
assets/icons/radix/update.svg                                                   |    4 
assets/icons/radix/upload.svg                                                   |    8 
assets/icons/radix/value-none.svg                                               |    8 
assets/icons/radix/value.svg                                                    |    8 
assets/icons/radix/vercel-logo.svg                                              |    8 
assets/icons/radix/video.svg                                                    |    4 
assets/icons/radix/view-grid.svg                                                |    8 
assets/icons/radix/view-horizontal.svg                                          |    8 
assets/icons/radix/view-none.svg                                                |    8 
assets/icons/radix/view-vertical.svg                                            |    8 
assets/icons/radix/width.svg                                                    |    8 
assets/icons/radix/zoom-in.svg                                                  |    8 
assets/icons/radix/zoom-out.svg                                                 |    8 
assets/icons/split_message_15.svg                                               |    0 
assets/keymaps/atom.json                                                        |   39 
assets/keymaps/default.json                                                     |   22 
assets/keymaps/sublime_text.json                                                |    4 
assets/keymaps/textmate.json                                                    |    6 
assets/keymaps/vim.json                                                         |   60 
assets/settings/default.json                                                    |   68 
assets/sounds/joined_call.wav                                                   |    0 
assets/sounds/leave_call.wav                                                    |    0 
assets/sounds/mute.wav                                                          |    0 
assets/sounds/start_screenshare.wav                                             |    0 
assets/sounds/stop_screenshare.wav                                              |    0 
assets/sounds/unmute.wav                                                        |    0 
crates/activity_indicator/src/activity_indicator.rs                             |   15 
crates/ai/Cargo.toml                                                            |    5 
crates/ai/src/ai.rs                                                             |   94 
crates/ai/src/assistant.rs                                                      |  753 
crates/audio/Cargo.toml                                                         |   23 
crates/audio/src/assets.rs                                                      |   44 
crates/audio/src/audio.rs                                                       |   67 
crates/auto_update/src/update_notification.rs                                   |    4 
crates/breadcrumbs/src/breadcrumbs.rs                                           |    2 
crates/call/Cargo.toml                                                          |    1 
crates/call/src/participant.rs                                                  |    6 
crates/call/src/room.rs                                                         |  428 
crates/client/src/telemetry.rs                                                  |   42 
crates/collab/Cargo.toml                                                        |    5 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql                  |    1 
crates/collab/migrations/20230616134535_add_is_external_to_worktree_entries.sql |    2 
crates/collab/src/db.rs                                                         |    3 
crates/collab/src/db/worktree_entry.rs                                          |    1 
crates/collab/src/rpc.rs                                                        |   17 
crates/collab/src/tests.rs                                                      |    1 
crates/collab/src/tests/integration_tests.rs                                    |  661 
crates/collab_ui/Cargo.toml                                                     |    4 
crates/collab_ui/src/branch_list.rs                                             |  238 
crates/collab_ui/src/collab_titlebar_item.rs                                    |  634 
crates/collab_ui/src/collab_ui.rs                                               |   42 
crates/collab_ui/src/contact_finder.rs                                          |    3 
crates/collab_ui/src/contact_list.rs                                            |   45 
crates/collab_ui/src/notifications.rs                                           |    4 
crates/command_palette/src/command_palette.rs                                   |    4 
crates/context_menu/src/context_menu.rs                                         |   69 
crates/copilot/src/copilot.rs                                                   |   13 
crates/copilot/src/sign_in.rs                                                   |    6 
crates/copilot_button/src/copilot_button.rs                                     |   12 
crates/diagnostics/src/diagnostics.rs                                           |    4 
crates/diagnostics/src/items.rs                                                 |    4 
crates/editor/src/display_map.rs                                                |  156 
crates/editor/src/display_map/block_map.rs                                      |   80 
crates/editor/src/display_map/fold_map.rs                                       |  610 
crates/editor/src/display_map/inlay_map.rs                                      | 1787 
crates/editor/src/display_map/suggestion_map.rs                                 |  871 
crates/editor/src/display_map/tab_map.rs                                        |  224 
crates/editor/src/display_map/wrap_map.rs                                       |   86 
crates/editor/src/editor.rs                                                     |  418 
crates/editor/src/editor_tests.rs                                               |  283 
crates/editor/src/element.rs                                                    |   23 
crates/editor/src/inlay_hint_cache.rs                                           | 2275 
crates/editor/src/multi_buffer.rs                                               |   10 
crates/editor/src/multi_buffer/anchor.rs                                        |   17 
crates/editor/src/scroll.rs                                                     |   26 
crates/editor/src/scroll/actions.rs                                             |   12 
crates/editor/src/scroll/scroll_amount.rs                                       |   32 
crates/feedback/src/deploy_feedback_button.rs                                   |    3 
crates/feedback/src/submit_feedback_button.rs                                   |    2 
crates/file_finder/src/file_finder.rs                                           |    2 
crates/fs/Cargo.toml                                                            |    4 
crates/fs/src/fs.rs                                                             |  112 
crates/fs/src/repository.rs                                                     |   48 
crates/go_to_line/src/go_to_line.rs                                             |   11 
crates/gpui/src/app.rs                                                          |   54 
crates/gpui/src/app/action.rs                                                   |    8 
crates/gpui/src/app/window.rs                                                   |   65 
crates/gpui/src/color.rs                                                        |    5 
crates/gpui/src/elements.rs                                                     |   92 
crates/gpui/src/elements/container.rs                                           |   10 
crates/gpui/src/elements/image.rs                                               |    3 
crates/gpui/src/elements/label.rs                                               |    4 
crates/gpui/src/elements/list.rs                                                |   10 
crates/gpui/src/elements/mouse_event_handler.rs                                 |   13 
crates/gpui/src/elements/svg.rs                                                 |   37 
crates/gpui/src/elements/tooltip.rs                                             |    5 
crates/gpui/src/executor.rs                                                     |    5 
crates/gpui/src/font_cache.rs                                                   |    3 
crates/gpui/src/fonts.rs                                                        |   33 
crates/gpui/src/gpui.rs                                                         |    4 
crates/gpui/src/platform.rs                                                     |    3 
crates/gpui/src/platform/mac/platform.rs                                        |    9 
crates/gpui/src/scene.rs                                                        |    3 
crates/gpui/src/scene/mouse_event.rs                                            |   22 
crates/gpui/src/scene/mouse_region.rs                                           |   37 
crates/gpui_macros/src/gpui_macros.rs                                           |   69 
crates/language/src/language.rs                                                 |  270 
crates/language/src/language_settings.rs                                        |   61 
crates/language/src/syntax_map.rs                                               |  247 
crates/language/src/syntax_map/syntax_map_tests.rs                              |   33 
crates/language_selector/src/active_buffer_language.rs                          |    2 
crates/language_selector/src/language_selector.rs                               |    2 
crates/language_tools/src/lsp_log.rs                                            |    8 
crates/language_tools/src/syntax_tree_view.rs                                   |    5 
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift  |  132 
crates/live_kit_client/examples/test_app.rs                                     |  106 
crates/live_kit_client/src/live_kit_client.rs                                   |    2 
crates/live_kit_client/src/prod.rs                                              |  385 
crates/live_kit_client/src/test.rs                                              |  168 
crates/lsp/src/lsp.rs                                                           |   85 
crates/node_runtime/Cargo.toml                                                  |    1 
crates/node_runtime/src/node_runtime.rs                                         |  203 
crates/outline/src/outline.rs                                                   |    2 
crates/picker/src/picker.rs                                                     |   25 
crates/project/Cargo.toml                                                       |    2 
crates/project/src/lsp_command.rs                                               |  353 
crates/project/src/project.rs                                                   |  876 
crates/project/src/project_tests.rs                                             |  133 
crates/project/src/worktree.rs                                                  |  765 
crates/project/src/worktree_tests.rs                                            |  789 
crates/project_panel/Cargo.toml                                                 |    1 
crates/project_panel/src/project_panel.rs                                       |  262 
crates/project_symbols/src/project_symbols.rs                                   |    7 
crates/recent_projects/Cargo.toml                                               |    1 
crates/recent_projects/src/recent_projects.rs                                   |   24 
crates/rope/src/rope.rs                                                         |    8 
crates/rpc/proto/zed.proto                                                      |   80 
crates/rpc/src/proto.rs                                                         |   11 
crates/rpc/src/rpc.rs                                                           |    2 
crates/search/src/buffer_search.rs                                              |   20 
crates/search/src/project_search.rs                                             |    8 
crates/settings/Cargo.toml                                                      |    6 
crates/settings/src/keymap_file.rs                                              |   70 
crates/settings/src/settings_store.rs                                           |    5 
crates/sum_tree/src/cursor.rs                                                   |   40 
crates/sum_tree/src/sum_tree.rs                                                 |   80 
crates/sum_tree/src/tree_map.rs                                                 |    6 
crates/terminal_view/src/terminal_element.rs                                    |   21 
crates/terminal_view/src/terminal_panel.rs                                      |    3 
crates/text/src/text.rs                                                         |   37 
crates/theme/src/theme.rs                                                       |  327 
crates/theme/src/theme_settings.rs                                              |    3 
crates/theme/src/ui.rs                                                          |   43 
crates/theme_selector/src/theme_selector.rs                                     |    2 
crates/theme_testbench/Cargo.toml                                               |   19 
crates/theme_testbench/src/theme_testbench.rs                                   |  300 
crates/util/src/paths.rs                                                        |    1 
crates/util/src/util.rs                                                         |   14 
crates/vim/src/motion.rs                                                        |   27 
crates/vim/src/normal.rs                                                        |   90 
crates/vim/src/normal/case.rs                                                   |   64 
crates/vim/src/normal/change.rs                                                 |    9 
crates/vim/src/normal/delete.rs                                                 |    2 
crates/vim/src/normal/scroll.rs                                                 |  120 
crates/vim/src/normal/substitute.rs                                             |   73 
crates/vim/src/normal/yank.rs                                                   |    2 
crates/vim/src/test.rs                                                          |   41 
crates/vim/src/vim.rs                                                           |    7 
crates/vim/src/visual.rs                                                        |    2 
crates/welcome/src/base_keymap_picker.rs                                        |    2 
crates/workspace/src/dock.rs                                                    |   11 
crates/workspace/src/item.rs                                                    |    4 
crates/workspace/src/notifications.rs                                           |    4 
crates/workspace/src/pane.rs                                                    |   71 
crates/workspace/src/persistence.rs                                             |   27 
crates/workspace/src/persistence/model.rs                                       |    6 
crates/workspace/src/toolbar.rs                                                 |   76 
crates/workspace/src/workspace.rs                                               |  189 
crates/xtask/Cargo.toml                                                         |   13 
crates/xtask/src/cli.rs                                                         |   23 
crates/xtask/src/main.rs                                                        |   29 
crates/zed-actions/Cargo.toml                                                   |   10 
crates/zed-actions/src/lib.rs                                                   |   28 
crates/zed/Cargo.toml                                                           |    6 
crates/zed/src/assets.rs                                                        |    1 
crates/zed/src/languages/c.rs                                                   |   93 
crates/zed/src/languages/elixir.rs                                              |  120 
crates/zed/src/languages/elixir/highlights.scm                                  |    9 
crates/zed/src/languages/go.rs                                                  |  131 
crates/zed/src/languages/heex/highlights.scm                                    |   15 
crates/zed/src/languages/heex/injections.scm                                    |   20 
crates/zed/src/languages/html.rs                                                |   95 
crates/zed/src/languages/json.rs                                                |   78 
crates/zed/src/languages/language_plugin.rs                                     |   22 
crates/zed/src/languages/lua.rs                                                 |   96 
crates/zed/src/languages/python.rs                                              |   84 
crates/zed/src/languages/ruby.rs                                                |   22 
crates/zed/src/languages/rust.rs                                                |   66 
crates/zed/src/languages/typescript.rs                                          |  137 
crates/zed/src/languages/yaml.rs                                                |   83 
crates/zed/src/main.rs                                                          |  181 
crates/zed/src/zed.rs                                                           |  215 
docs/backend-development.md                                                     |   52 
docs/building-zed.md                                                            |   79 
docs/company-and-vision.md                                                      |   34 
docs/design-tools.md                                                            |   74 
docs/index.md                                                                   |   14 
docs/local-collaboration.md                                                     |   22 
docs/release-process.md                                                         |   96 
docs/tools.md                                                                   |   82 
docs/zed/syntax-highlighting.md                                                 |   79 
script/build-theme-types                                                        |   10 
script/start-local-collaboration                                                |    2 
styles/.eslintrc.js                                                             |   33 
styles/.gitignore                                                               |    1 
styles/.prettierrc                                                              |    6 
styles/.zed/settings.json                                                       |   20 
styles/package-lock.json                                                        | 4464 
styles/package.json                                                             |   36 
styles/src/buildLicenses.ts                                                     |   50 
styles/src/buildThemes.ts                                                       |   43 
styles/src/buildTokens.ts                                                       |   85 
styles/src/build_licenses.ts                                                    |   50 
styles/src/build_themes.ts                                                      |   47 
styles/src/build_tokens.ts                                                      |   90 
styles/src/build_types.ts                                                       |   62 
styles/src/common.ts                                                            |   26 
styles/src/component/icon_button.ts                                             |   85 
styles/src/component/text_button.ts                                             |   93 
styles/src/element/index.ts                                                     |    4 
styles/src/element/interactive.test.ts                                          |   56 
styles/src/element/interactive.ts                                               |   97 
styles/src/element/toggle.test.ts                                               |   52 
styles/src/element/toggle.ts                                                    |   47 
styles/src/styleTree/app.ts                                                     |   74 
styles/src/styleTree/assistant.ts                                               |   85 
styles/src/styleTree/commandPalette.ts                                          |   30 
styles/src/styleTree/contactFinder.ts                                           |   70 
styles/src/styleTree/contactList.ts                                             |  182 
styles/src/styleTree/contactNotification.ts                                     |   45 
styles/src/styleTree/contactsPopover.ts                                         |   16 
styles/src/styleTree/contextMenu.ts                                             |   49 
styles/src/styleTree/copilot.ts                                                 |  267 
styles/src/styleTree/editor.ts                                                  |  281 
styles/src/styleTree/feedback.ts                                                |   44 
styles/src/styleTree/hoverPopover.ts                                            |   46 
styles/src/styleTree/incomingCallNotification.ts                                |   53 
styles/src/styleTree/picker.ts                                                  |   82 
styles/src/styleTree/projectDiagnostics.ts                                      |   13 
styles/src/styleTree/projectPanel.ts                                            |  107 
styles/src/styleTree/projectSharedNotification.ts                               |   54 
styles/src/styleTree/search.ts                                                  |  113 
styles/src/styleTree/sharedScreen.ts                                            |    9 
styles/src/styleTree/simpleMessageNotification.ts                               |   44 
styles/src/styleTree/statusBar.ts                                               |  126 
styles/src/styleTree/tabBar.ts                                                  |  109 
styles/src/styleTree/terminal.ts                                                |   52 
styles/src/styleTree/toolbarDropdownMenu.ts                                     |   46 
styles/src/styleTree/tooltip.ts                                                 |   23 
styles/src/styleTree/updateNotification.ts                                      |   31 
styles/src/styleTree/welcome.ts                                                 |  129 
styles/src/styleTree/workspace.ts                                               |  338 
styles/src/style_tree/app.ts                                                    |   62 
styles/src/style_tree/assistant.ts                                              |  281 
styles/src/style_tree/command_palette.ts                                        |   46 
styles/src/style_tree/components.ts                                             |  130 
styles/src/style_tree/contact_finder.ts                                         |   74 
styles/src/style_tree/contact_list.ts                                           |  247 
styles/src/style_tree/contact_notification.ts                                   |   55 
styles/src/style_tree/contacts_popover.ts                                       |   16 
styles/src/style_tree/context_menu.ts                                           |   70 
styles/src/style_tree/copilot.ts                                                |  293 
styles/src/style_tree/editor.ts                                                 |  319 
styles/src/style_tree/feedback.ts                                               |   51 
styles/src/style_tree/hover_popover.ts                                          |   49 
styles/src/style_tree/incoming_call_notification.ts                             |   55 
styles/src/style_tree/picker.ts                                                 |  132 
styles/src/style_tree/project_diagnostics.ts                                    |   14 
styles/src/style_tree/project_panel.ts                                          |  199 
styles/src/style_tree/project_shared_notification.ts                            |   55 
styles/src/style_tree/search.ts                                                 |  138 
styles/src/style_tree/shared_screen.ts                                          |   10 
styles/src/style_tree/simple_message_notification.ts                            |   52 
styles/src/style_tree/status_bar.ts                                             |  156 
styles/src/style_tree/tab_bar.ts                                                |  131 
styles/src/style_tree/terminal.ts                                               |   54 
styles/src/style_tree/titlebar.ts                                               |  278 
styles/src/style_tree/toolbar_dropdown_menu.ts                                  |   66 
styles/src/style_tree/tooltip.ts                                                |   24 
styles/src/style_tree/update_notification.ts                                    |   41 
styles/src/style_tree/welcome.ts                                                |  157 
styles/src/style_tree/workspace.ts                                              |  192 
styles/src/system/lib/convert.ts                                                |   11 
styles/src/system/lib/curve.ts                                                  |   26 
styles/src/system/lib/generate.ts                                               |  159 
styles/src/system/ref/color.ts                                                  |  445 
styles/src/system/ref/curves.ts                                                 |   25 
styles/src/system/system.ts                                                     |   32 
styles/src/system/types.ts                                                      |   66 
styles/src/theme/color.ts                                                       |    2 
styles/src/theme/colorScheme.ts                                                 |  286 
styles/src/theme/create_theme.ts                                                |  282 
styles/src/theme/index.ts                                                       |   25 
styles/src/theme/ramps.ts                                                       |   44 
styles/src/theme/syntax.ts                                                      |  123 
styles/src/theme/themeConfig.ts                                                 |  148 
styles/src/theme/theme_config.ts                                                |   81 
styles/src/theme/tokens/colorScheme.ts                                          |   81 
styles/src/theme/tokens/layer.ts                                                |   47 
styles/src/theme/tokens/players.ts                                              |   43 
styles/src/theme/tokens/theme.ts                                                |  101 
styles/src/theme/tokens/token.ts                                                |    9 
styles/src/themes/andromeda/LICENSE                                             |    2 
styles/src/themes/andromeda/andromeda.ts                                        |   26 
styles/src/themes/atelier/LICENSE                                               |    2 
styles/src/themes/atelier/atelier-cave-dark.ts                                  |   34 
styles/src/themes/atelier/atelier-cave-light.ts                                 |   34 
styles/src/themes/atelier/atelier-dune-dark.ts                                  |   34 
styles/src/themes/atelier/atelier-dune-light.ts                                 |   34 
styles/src/themes/atelier/atelier-estuary-dark.ts                               |   34 
styles/src/themes/atelier/atelier-estuary-light.ts                              |   34 
styles/src/themes/atelier/atelier-forest-dark.ts                                |   34 
styles/src/themes/atelier/atelier-forest-light.ts                               |   36 
styles/src/themes/atelier/atelier-heath-dark.ts                                 |   34 
styles/src/themes/atelier/atelier-heath-light.ts                                |   34 
styles/src/themes/atelier/atelier-lakeside-dark.ts                              |   34 
styles/src/themes/atelier/atelier-lakeside-light.ts                             |   34 
styles/src/themes/atelier/atelier-plateau-dark.ts                               |   34 
styles/src/themes/atelier/atelier-plateau-light.ts                              |   34 
styles/src/themes/atelier/atelier-savanna-dark.ts                               |   34 
styles/src/themes/atelier/atelier-savanna-light.ts                              |   34 
styles/src/themes/atelier/atelier-seaside-dark.ts                               |   34 
styles/src/themes/atelier/atelier-seaside-light.ts                              |   34 
styles/src/themes/atelier/atelier-sulphurpool-dark.ts                           |   34 
styles/src/themes/atelier/atelier-sulphurpool-light.ts                          |   34 
styles/src/themes/atelier/common.ts                                             |    6 
styles/src/themes/ayu/LICENSE                                                   |    2 
styles/src/themes/ayu/ayu-dark.ts                                               |   12 
styles/src/themes/ayu/ayu-light.ts                                              |   12 
styles/src/themes/ayu/ayu-mirage.ts                                             |   12 
styles/src/themes/ayu/common.ts                                                 |   30 
styles/src/themes/gruvbox/LICENSE                                               |    2 
styles/src/themes/gruvbox/gruvbox-common.ts                                     |  117 
styles/src/themes/gruvbox/gruvbox-dark-hard.ts                                  |    2 
styles/src/themes/gruvbox/gruvbox-dark-soft.ts                                  |    2 
styles/src/themes/gruvbox/gruvbox-dark.ts                                       |    2 
styles/src/themes/gruvbox/gruvbox-light-hard.ts                                 |    2 
styles/src/themes/gruvbox/gruvbox-light-soft.ts                                 |    2 
styles/src/themes/gruvbox/gruvbox-light.ts                                      |    2 
styles/src/themes/index.ts                                                      |  148 
styles/src/themes/one/LICENSE                                                   |    2 
styles/src/themes/one/one-dark.ts                                               |   39 
styles/src/themes/one/one-light.ts                                              |   38 
styles/src/themes/rose-pine/LICENSE                                             |    2 
styles/src/themes/rose-pine/common.ts                                           |   75 
styles/src/themes/rose-pine/rose-pine-dawn.ts                                   |   58 
styles/src/themes/rose-pine/rose-pine-moon.ts                                   |   52 
styles/src/themes/rose-pine/rose-pine.ts                                        |   51 
styles/src/themes/sandcastle/LICENSE                                            |    2 
styles/src/themes/sandcastle/sandcastle.ts                                      |   26 
styles/src/themes/solarized/LICENSE                                             |    2 
styles/src/themes/solarized/solarized.ts                                        |   34 
styles/src/themes/summercamp/LICENSE                                            |    2 
styles/src/themes/summercamp/summercamp.ts                                      |   26 
styles/src/utils/slugify.ts                                                     |   11 
styles/src/utils/snakeCase.ts                                                   |   35 
styles/tsconfig.json                                                            |    8 
styles/vitest.config.ts                                                         |    8 
700 files changed, 28,676 insertions(+), 9,797 deletions(-)

Detailed changes

.config/nextest.toml πŸ”—

@@ -0,0 +1,6 @@
+[test-groups]
+sequential-db-tests = { max-threads = 1 }
+
+[[profile.default.overrides]]
+filter = 'package(db)'
+test-group = 'sequential-db-tests'

.github/workflows/ci.yml πŸ”—

@@ -51,6 +51,7 @@ jobs:
           rustup set profile minimal
           rustup update stable
           rustup target add wasm32-wasi
+          cargo install cargo-nextest
 
       - name: Install Node
         uses: actions/setup-node@v2
@@ -70,7 +71,7 @@ jobs:
         run: cargo check --workspace
 
       - name: Run tests
-        run: cargo test --workspace --no-fail-fast
+        run: cargo nextest run --workspace --no-fail-fast
 
       - name: Build collab
         run: cargo build -p collab

.gitignore πŸ”—

@@ -4,6 +4,8 @@
 /plugins/bin
 /script/node_modules
 /styles/node_modules
+/styles/src/types/zed.ts
+/crates/theme/schemas/theme.json
 /crates/collab/static/styles.css
 /vendor/bin
 /assets/themes/*.json
@@ -18,4 +20,5 @@ DerivedData/
 .swiftpm/config/registries.json
 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
 .netrc
+.swiftpm
 **/*.db

Cargo.lock πŸ”—

@@ -109,11 +109,14 @@ dependencies = [
  "isahc",
  "language",
  "menu",
+ "project",
+ "regex",
  "schemars",
  "search",
  "serde",
  "serde_json",
  "settings",
+ "smol",
  "theme",
  "tiktoken-rs",
  "util",
@@ -174,6 +177,28 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
+[[package]]
+name = "alsa"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
+dependencies = [
+ "alsa-sys",
+ "bitflags",
+ "libc",
+ "nix",
+]
+
+[[package]]
+name = "alsa-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
 [[package]]
 name = "ambient-authority"
 version = "0.0.1"
@@ -189,6 +214,55 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "anstream"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is-terminal 0.4.7",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.71"
@@ -538,6 +612,19 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "audio"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "gpui",
+ "log",
+ "parking_lot 0.11.2",
+ "rodio",
+ "util",
+]
+
 [[package]]
 name = "auto_update"
 version = "0.1.0"
@@ -593,7 +680,7 @@ dependencies = [
  "http",
  "http-body",
  "hyper",
- "itoa",
+ "itoa 1.0.6",
  "matchit",
  "memchr",
  "mime",
@@ -704,6 +791,26 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bindgen"
+version = "0.64.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "bindgen"
 version = "0.65.1"
@@ -805,7 +912,7 @@ checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
 dependencies = [
  "borsh-derive-internal",
  "borsh-schema-derive-internal",
- "proc-macro-crate",
+ "proc-macro-crate 0.1.5",
  "proc-macro2",
  "syn 1.0.109",
 ]
@@ -934,6 +1041,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-broadcast",
+ "audio",
  "client",
  "collections",
  "fs",
@@ -1030,6 +1138,12 @@ dependencies = [
  "jobserver",
 ]
 
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
 [[package]]
 name = "cexpr"
 version = "0.6.0"
@@ -1101,15 +1215,39 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 dependencies = [
  "atty",
  "bitflags",
- "clap_derive",
- "clap_lex",
- "indexmap",
+ "clap_derive 3.2.25",
+ "clap_lex 0.2.4",
+ "indexmap 1.9.3",
  "once_cell",
  "strsim",
  "termcolor",
  "textwrap",
 ]
 
+[[package]]
+name = "clap"
+version = "4.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc"
+dependencies = [
+ "clap_builder",
+ "clap_derive 4.3.2",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "bitflags",
+ "clap_lex 0.5.0",
+ "strsim",
+]
+
 [[package]]
 name = "clap_derive"
 version = "3.2.25"
@@ -1123,6 +1261,18 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "clap_derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.18",
+]
+
 [[package]]
 name = "clap_lex"
 version = "0.2.4"
@@ -1132,12 +1282,24 @@ dependencies = [
  "os_str_bytes",
 ]
 
+[[package]]
+name = "clap_lex"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+
+[[package]]
+name = "claxon"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
+
 [[package]]
 name = "cli"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "clap",
+ "clap 3.2.25",
  "core-foundation",
  "core-services",
  "dirs 3.0.2",
@@ -1239,15 +1401,16 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.14.2"
+version = "0.16.0"
 dependencies = [
  "anyhow",
  "async-tungstenite",
+ "audio",
  "axum",
  "axum-extra",
  "base64 0.13.1",
  "call",
- "clap",
+ "clap 3.2.25",
  "client",
  "collections",
  "ctor",
@@ -1321,12 +1484,15 @@ dependencies = [
  "picker",
  "postage",
  "project",
+ "recent_projects",
  "serde",
  "serde_derive",
  "settings",
  "theme",
+ "theme_selector",
  "util",
  "workspace",
+ "zed-actions",
 ]
 
 [[package]]
@@ -1342,6 +1508,22 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "combine"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
+dependencies = [
+ "bytes 1.4.0",
+ "memchr",
+]
+
 [[package]]
 name = "command_palette"
 version = "0.1.0"
@@ -1438,11 +1620,17 @@ name = "core-foundation"
 version = "0.9.3"
 source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
 dependencies = [
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
  "uuid 0.5.1",
 ]
 
+[[package]]
+name = "core-foundation-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.3"
@@ -1492,6 +1680,51 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "coreaudio-rs"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
+dependencies = [
+ "bitflags",
+ "core-foundation-sys 0.6.2",
+ "coreaudio-sys",
+]
+
+[[package]]
+name = "coreaudio-sys"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24"
+dependencies = [
+ "bindgen 0.64.0",
+]
+
+[[package]]
+name = "cpal"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
+dependencies = [
+ "alsa",
+ "core-foundation-sys 0.8.3",
+ "coreaudio-rs",
+ "dasp_sample",
+ "jni 0.19.0",
+ "js-sys",
+ "libc",
+ "mach2",
+ "ndk",
+ "ndk-context",
+ "oboe",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows 0.46.0",
+]
+
 [[package]]
 name = "cpp_demangle"
 version = "0.3.5"
@@ -1822,6 +2055,12 @@ dependencies = [
  "parking_lot_core 0.9.7",
 ]
 
+[[package]]
+name = "dasp_sample"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
+
 [[package]]
 name = "data-url"
 version = "0.1.1"
@@ -2131,6 +2370,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
 [[package]]
 name = "erased-serde"
 version = "0.3.25"
@@ -2447,6 +2692,7 @@ dependencies = [
  "smol",
  "sum_tree",
  "tempfile",
+ "time 0.3.21",
  "util",
 ]
 
@@ -2691,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
 dependencies = [
  "fallible-iterator",
- "indexmap",
+ "indexmap 1.9.3",
  "stable_deref_trait",
 ]
 
@@ -2787,7 +3033,7 @@ dependencies = [
  "anyhow",
  "async-task",
  "backtrace",
- "bindgen",
+ "bindgen 0.65.1",
  "block",
  "cc",
  "cocoa",
@@ -2859,7 +3105,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap",
+ "indexmap 1.9.3",
  "slab",
  "tokio",
  "tokio-util 0.7.8",
@@ -2893,6 +3139,12 @@ dependencies = [
  "ahash 0.8.3",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
 [[package]]
 name = "hashlink"
 version = "0.8.1"
@@ -3003,6 +3255,12 @@ dependencies = [
  "digest 0.10.6",
 ]
 
+[[package]]
+name = "hound"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
+
 [[package]]
 name = "http"
 version = "0.2.9"
@@ -3011,7 +3269,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
 dependencies = [
  "bytes 1.4.0",
  "fnv",
- "itoa",
+ "itoa 1.0.6",
 ]
 
 [[package]]
@@ -3070,7 +3328,7 @@ dependencies = [
  "http-body",
  "httparse",
  "httpdate",
- "itoa",
+ "itoa 1.0.6",
  "pin-project-lite 0.2.9",
  "socket2",
  "tokio",
@@ -3111,11 +3369,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
 dependencies = [
  "android_system_properties",
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "iana-time-zone-haiku",
  "js-sys",
  "wasm-bindgen",
- "windows",
+ "windows 0.48.0",
 ]
 
 [[package]]
@@ -3185,6 +3443,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.0",
+]
+
 [[package]]
 name = "indoc"
 version = "1.0.9"
@@ -3336,6 +3604,12 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
 [[package]]
 name = "itoa"
 version = "1.0.6"
@@ -3351,6 +3625,40 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "jni"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
 [[package]]
 name = "jobserver"
 version = "0.1.26"
@@ -3396,12 +3704,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "json_comments"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5"
-
 [[package]]
 name = "jwt"
 version = "0.16.0"
@@ -3559,6 +3861,17 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
+[[package]]
+name = "lewton"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
+dependencies = [
+ "byteorder",
+ "ogg",
+ "tinyvec",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.144"
@@ -3791,6 +4104,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "mach2"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -3847,7 +4169,7 @@ name = "media"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "bindgen",
+ "bindgen 0.65.1",
  "block",
  "bytes 1.4.0",
  "core-foundation",
@@ -4105,6 +4427,35 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "ndk"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
+dependencies = [
+ "bitflags",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.4.1+23.1.7779620"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
+dependencies = [
+ "jni-sys",
+]
+
 [[package]]
 name = "net2"
 version = "0.2.38"
@@ -4137,6 +4488,7 @@ dependencies = [
  "async-tar",
  "futures 0.3.28",
  "gpui",
+ "log",
  "parking_lot 0.11.2",
  "serde",
  "serde_derive",
@@ -4212,6 +4564,17 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.45"
@@ -4264,6 +4627,27 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "num_enum"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "nvim-rs"
 version = "0.5.0"
@@ -4306,7 +4690,7 @@ checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424"
 dependencies = [
  "crc32fast",
  "hashbrown 0.11.2",
- "indexmap",
+ "indexmap 1.9.3",
  "memchr",
 ]
 
@@ -4319,6 +4703,38 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "oboe"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0"
+dependencies = [
+ "jni 0.20.0",
+ "ndk",
+ "ndk-context",
+ "num-derive",
+ "num-traits",
+ "oboe-sys",
+]
+
+[[package]]
+name = "oboe-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ogg"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.17.1"
@@ -4608,7 +5024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
 dependencies = [
  "fixedbitset",
- "indexmap",
+ "indexmap 1.9.3",
 ]
 
 [[package]]
@@ -4685,7 +5101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
 dependencies = [
  "base64 0.21.0",
- "indexmap",
+ "indexmap 1.9.3",
  "line-wrap",
  "quick-xml",
  "serde",
@@ -4818,6 +5234,16 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -4930,6 +5356,7 @@ dependencies = [
  "language",
  "menu",
  "postage",
+ "pretty_assertions",
  "project",
  "schemars",
  "serde",
@@ -5229,6 +5656,12 @@ dependencies = [
  "rand_core 0.5.1",
 ]
 
+[[package]]
+name = "raw-window-handle"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
+
 [[package]]
 name = "rayon"
 version = "1.7.0"
@@ -5272,6 +5705,7 @@ version = "0.1.0"
 dependencies = [
  "db",
  "editor",
+ "futures 0.3.28",
  "fuzzy",
  "gpui",
  "language",
@@ -5512,6 +5946,19 @@ dependencies = [
  "rmp",
 ]
 
+[[package]]
+name = "rodio"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa"
+dependencies = [
+ "claxon",
+ "cpal",
+ "hound",
+ "lewton",
+ "symphonia",
+]
+
 [[package]]
 name = "rope"
 version = "0.1.0"
@@ -5667,7 +6114,7 @@ dependencies = [
  "bitflags",
  "errno 0.2.8",
  "io-lifetimes 0.5.3",
- "itoa",
+ "itoa 1.0.6",
  "libc",
  "linux-raw-sys 0.0.42",
  "once_cell",
@@ -6013,7 +6460,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
 dependencies = [
  "bitflags",
  "core-foundation",
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
  "security-framework-sys",
 ]
@@ -6024,7 +6471,7 @@ version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
 dependencies = [
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
  "libc",
 ]
 
@@ -6098,8 +6545,20 @@ version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
 dependencies = [
- "indexmap",
- "itoa",
+ "indexmap 1.9.3",
+ "itoa 1.0.6",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_json_lenient"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add"
+dependencies = [
+ "indexmap 1.9.3",
+ "itoa 0.4.8",
  "ryu",
  "serde",
 ]
@@ -6122,7 +6581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
 dependencies = [
  "form_urlencoded",
- "itoa",
+ "itoa 1.0.6",
  "ryu",
  "serde",
 ]
@@ -6133,7 +6592,7 @@ version = "0.8.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "ryu",
  "serde",
  "yaml-rust",
@@ -6148,7 +6607,7 @@ dependencies = [
  "fs",
  "futures 0.3.28",
  "gpui",
- "json_comments",
+ "indoc",
  "lazy_static",
  "postage",
  "pretty_assertions",
@@ -6157,6 +6616,7 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
+ "serde_json_lenient",
  "smallvec",
  "sqlez",
  "staff_mode",
@@ -6506,8 +6966,8 @@ dependencies = [
  "hex",
  "hkdf",
  "hmac 0.12.1",
- "indexmap",
- "itoa",
+ "indexmap 1.9.3",
+ "itoa 1.0.6",
  "libc",
  "libsqlite3-sys",
  "log",
@@ -6657,6 +7117,56 @@ dependencies = [
  "siphasher",
 ]
 
+[[package]]
+name = "symphonia"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941"
+dependencies = [
+ "lazy_static",
+ "symphonia-bundle-mp3",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-bundle-mp3"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "log",
+ "symphonia-core",
+ "symphonia-metadata",
+]
+
+[[package]]
+name = "symphonia-core"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142"
+dependencies = [
+ "arrayvec 0.7.2",
+ "bitflags",
+ "bytemuck",
+ "lazy_static",
+ "log",
+]
+
+[[package]]
+name = "symphonia-metadata"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0"
+dependencies = [
+ "encoding_rs",
+ "lazy_static",
+ "log",
+ "symphonia-core",
+]
+
 [[package]]
 name = "syn"
 version = "1.0.109"

Cargo.toml πŸ”—

@@ -2,6 +2,7 @@
 members = [
     "crates/activity_indicator",
     "crates/ai",
+    "crates/audio",
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
@@ -61,12 +62,13 @@ members = [
     "crates/text",
     "crates/theme",
     "crates/theme_selector",
-    "crates/theme_testbench",
     "crates/util",
     "crates/vim",
     "crates/workspace",
     "crates/welcome",
+    "crates/xtask",
     "crates/zed",
+    "crates/zed-actions"
 ]
 default-members = ["crates/zed"]
 resolver = "2"
@@ -100,6 +102,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
 toml = { version = "0.5" }
 tree-sitter = "0.20"
 unindent = { version = "0.1.7" }
+pretty_assertions = "1.3.0"
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
@@ -118,3 +121,4 @@ split-debuginfo = "unpacked"
 [profile.release]
 debug = true
 lto = "thin"
+codegen-units = 1

assets/icons/hamburger_15.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="#CCCAC2"/>
+</svg>

assets/icons/radix/align-bottom.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9 3C9 2.44772 8.55229 2 8 2H7C6.44772 2 6 2.44772 6 3L6 14H1.5C1.22386 14 1 14.2239 1 14.5C1 14.7761 1.22386 15 1.5 15L6 15H9H13.5C13.7761 15 14 14.7761 14 14.5C14 14.2239 13.7761 14 13.5 14H9V3Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-center-horizontally.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.99988 6C1.44759 6 0.999877 6.44772 0.999877 7L0.999877 8C0.999877 8.55228 1.44759 9 1.99988 9L6.99988 9L6.99988 13.5C6.99988 13.7761 7.22374 14 7.49988 14C7.77602 14 7.99988 13.7761 7.99988 13.5L7.99988 9L12.9999 9C13.5522 9 13.9999 8.55228 13.9999 8L13.9999 7C13.9999 6.44772 13.5522 6 12.9999 6L7.99988 6L7.99988 1.5C7.99988 1.22386 7.77602 1 7.49988 1C7.22373 1 6.99988 1.22386 6.99988 1.5L6.99988 6L1.99988 6Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-center-vertically.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.99988 1C6.44759 1 5.99988 1.44772 5.99988 2V7H1.49988C1.22374 7 0.999878 7.22386 0.999878 7.5C0.999878 7.77614 1.22374 8 1.49988 8H5.99988V13C5.99988 13.5523 6.44759 14 6.99988 14H7.99988C8.55216 14 8.99988 13.5523 8.99988 13V8H13.4999C13.776 8 13.9999 7.77614 13.9999 7.5C13.9999 7.22386 13.776 7 13.4999 7H8.99988V2C8.99988 1.44772 8.55216 1 7.99988 1L6.99988 1Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-center.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6 7.05002V4H9V7.05002L6 7.05002ZM5 7.05002H1.49919C1.25067 7.05002 1.04919 7.25149 1.04919 7.50002C1.04919 7.74855 1.25067 7.95002 1.49919 7.95002H5V11.25C5 11.6642 5.33579 12 5.75 12H9.25C9.66421 12 10 11.6642 10 11.25V7.95002H13.4992C13.7477 7.95002 13.9492 7.74855 13.9492 7.50002C13.9492 7.2515 13.7477 7.05002 13.4992 7.05002H10V3.75C10 3.33579 9.66421 3 9.25 3H5.75C5.33579 3 5 3.33579 5 3.75V7.05002ZM9 7.95002V11H6V7.95002L9 7.95002Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-end.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6 11V4H9V11H6ZM5 3.75C5 3.33579 5.33579 3 5.75 3H9.25C9.66421 3 10 3.33579 10 3.75V11.25C10 11.6642 9.66421 12 9.25 12H5.75C5.33579 12 5 11.6642 5 11.25V3.75ZM1.49919 13.05C1.25067 13.05 1.04919 13.2515 1.04919 13.5C1.04919 13.7486 1.25067 13.95 1.49919 13.95L13.4992 13.95C13.7477 13.95 13.9492 13.7486 13.9492 13.5C13.9492 13.2515 13.7477 13.05 13.4992 13.05H1.49919Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-horizontal-centers.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.24988 2C2.55952 2 1.99988 2.55964 1.99988 3.25V11.75C1.99988 12.4404 2.55952 13 3.24988 13H5.74988C6.44023 13 6.99988 12.4404 6.99988 11.75V3.25C6.99988 2.55964 6.44023 2 5.74988 2H3.24988ZM2.99988 3.25C2.99988 3.11193 3.11181 3 3.24988 3H5.74988C5.88795 3 5.99988 3.11193 5.99988 3.25V11.75C5.99988 11.8881 5.88795 12 5.74988 12H3.24988C3.11181 12 2.99988 11.8881 2.99988 11.75V3.25ZM9.25 4C8.55964 4 8 4.55964 8 5.25V9.75C8 10.4404 8.55964 11 9.25 11H11.75C12.4404 11 13 10.4404 13 9.75V5.25C13 4.55964 12.4404 4 11.75 4H9.25Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.499995 0.999995C0.223855 0.999995 -5.58458e-07 1.22385 -5.46388e-07 1.49999L-2.18554e-08 13.4999C-9.78492e-09 13.776 0.223855 13.9999 0.499995 13.9999C0.776136 13.9999 0.999991 13.776 0.999991 13.4999L0.999991 8.99993L12 8.99993C12.5523 8.99993 13 8.55222 13 7.99993L13 6.99994C13 6.44766 12.5523 5.99995 12 5.99995L0.999991 5.99995L0.999991 1.49999C0.999991 1.22385 0.776135 0.999995 0.499995 0.999995Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14.4999 1C14.2237 1 13.9999 1.22386 13.9999 1.5L13.9999 6L2.99988 6C2.44759 6 1.99988 6.44772 1.99988 7L1.99988 8C1.99988 8.55228 2.44759 9 2.99988 9L13.9999 9L13.9999 13.5C13.9999 13.7761 14.2237 14 14.4999 14C14.776 14 14.9999 13.7761 14.9999 13.5L14.9999 9L14.9999 6L14.9999 1.5C14.9999 1.22386 14.776 1 14.4999 1Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-start.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.49956 1.05002C1.25103 1.05002 1.04956 1.25149 1.04956 1.50002C1.04956 1.74855 1.25103 1.95002 1.49956 1.95002L13.4996 1.95002C13.7481 1.95002 13.9496 1.74855 13.9496 1.50002C13.9496 1.25149 13.7481 1.05002 13.4996 1.05002H1.49956ZM6 11V3.99999H9V11H6ZM5 3.74999C5 3.33578 5.33579 2.99999 5.75 2.99999H9.25C9.66421 2.99999 10 3.33578 10 3.74999V11.25C10 11.6642 9.66421 12 9.25 12H5.75C5.33579 12 5 11.6642 5 11.25V3.74999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-stretch.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.04956 1.50002C1.04956 1.25149 1.25103 1.05002 1.49956 1.05002H13.4996C13.7481 1.05002 13.9496 1.25149 13.9496 1.50002C13.9496 1.74855 13.7481 1.95002 13.4996 1.95002L1.49956 1.95002C1.25103 1.95002 1.04956 1.74855 1.04956 1.50002ZM1.04966 13.5C1.04966 13.2515 1.25113 13.05 1.49966 13.05H13.4997C13.7482 13.05 13.9497 13.2515 13.9497 13.5C13.9497 13.7485 13.7482 13.95 13.4997 13.95L1.49966 13.95C1.25113 13.95 1.04966 13.7485 1.04966 13.5ZM6 11V3.99999H9V11H6ZM5 3.74999C5 3.33578 5.33579 2.99999 5.75 2.99999H9.25C9.66421 2.99999 10 3.33578 10 3.74999V11.25C10 11.6642 9.66421 12 9.25 12H5.75C5.33579 12 5 11.6642 5 11.25V3.74999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-top.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.5 0C1.22386 0 1 0.223858 1 0.5C1 0.776142 1.22386 1 1.5 1H6V12C6 12.5523 6.44772 13 7 13H8C8.55228 13 9 12.5523 9 12V1H13.5C13.7761 1 14 0.776142 14 0.5C14 0.223858 13.7761 0 13.5 0H9H6H1.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/align-vertical-centers.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 3.25C2 2.55964 2.55964 2 3.25 2L11.75 2C12.4404 2 13 2.55964 13 3.25L13 5.75C13 6.44036 12.4404 7 11.75 7L3.25 7C2.55964 7 2 6.44036 2 5.75L2 3.25ZM3.25 3C3.11193 3 3 3.11193 3 3.25L3 5.75C3 5.88807 3.11193 6 3.25 6L11.75 6C11.8881 6 12 5.88807 12 5.75L12 3.25C12 3.11193 11.8881 3 11.75 3L3.25 3ZM4 9.25C4 8.55964 4.55964 8 5.25 8L9.75 8C10.4404 8 11 8.55964 11 9.25L11 11.75C11 12.4404 10.4404 13 9.75 13L5.25 13C4.55964 13 4 12.4404 4 11.75L4 9.25Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/all-sides.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 0.75L9.75 3H5.25L7.5 0.75ZM7.5 14.25L9.75 12H5.25L7.5 14.25ZM3 5.25L0.75 7.5L3 9.75V5.25ZM14.25 7.5L12 5.25V9.75L14.25 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/angle.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.8914 2.1937C9.1158 2.35464 9.16725 2.66701 9.00631 2.89141L2.47388 12H13.5C13.7761 12 14 12.2239 14 12.5C14 12.7762 13.7761 13 13.5 13H1.5C1.31254 13 1.14082 12.8952 1.0552 12.7284C0.969578 12.5616 0.984438 12.361 1.09369 12.2086L8.19369 2.30862C8.35462 2.08422 8.667 2.03277 8.8914 2.1937ZM11.1 6.50001C11.1 6.22387 11.3238 6.00001 11.6 6.00001C11.8761 6.00001 12.1 6.22387 12.1 6.50001C12.1 6.77615 11.8761 7.00001 11.6 7.00001C11.3238 7.00001 11.1 6.77615 11.1 6.50001ZM10.4 4.00001C10.1239 4.00001 9.90003 4.22387 9.90003 4.50001C9.90003 4.77615 10.1239 5.00001 10.4 5.00001C10.6762 5.00001 10.9 4.77615 10.9 4.50001C10.9 4.22387 10.6762 4.00001 10.4 4.00001ZM12.1 8.50001C12.1 8.22387 12.3238 8.00001 12.6 8.00001C12.8761 8.00001 13.1 8.22387 13.1 8.50001C13.1 8.77615 12.8761 9.00001 12.6 9.00001C12.3238 9.00001 12.1 8.77615 12.1 8.50001ZM13.4 10C13.1239 10 12.9 10.2239 12.9 10.5C12.9 10.7761 13.1239 11 13.4 11C13.6762 11 13.9 10.7761 13.9 10.5C13.9 10.2239 13.6762 10 13.4 10Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/archive.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.30902 1C2.93025 1 2.58398 1.214 2.41459 1.55279L1.05279 4.27639C1.01807 4.34582 1 4.42238 1 4.5V13C1 13.5523 1.44772 14 2 14H13C13.5523 14 14 13.5523 14 13V4.5C14 4.42238 13.9819 4.34582 13.9472 4.27639L12.5854 1.55281C12.416 1.21403 12.0698 1.00003 11.691 1.00003L7.5 1.00001L3.30902 1ZM3.30902 2L7 2.00001V4H2.30902L3.30902 2ZM8 4V2.00002L11.691 2.00003L12.691 4H8ZM7.5 5H13V13H2V5H7.5ZM5.5 7C5.22386 7 5 7.22386 5 7.5C5 7.77614 5.22386 8 5.5 8H9.5C9.77614 8 10 7.77614 10 7.5C10 7.22386 9.77614 7 9.5 7H5.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-bottom-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.3536 3.64644C11.5488 3.8417 11.5488 4.15828 11.3536 4.35354L4.70711 11L9 11C9.27614 11 9.5 11.2239 9.5 11.5C9.5 11.7761 9.27614 12 9 12L3.5 12C3.36739 12 3.24021 11.9473 3.14645 11.8536C3.05268 11.7598 3 11.6326 3 11.5L3 5.99999C3 5.72385 3.22386 5.49999 3.5 5.49999C3.77614 5.49999 4 5.72385 4 5.99999V10.2929L10.6464 3.64643C10.8417 3.45117 11.1583 3.45117 11.3536 3.64644Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-bottom-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.64645 3.64644C3.45118 3.8417 3.45118 4.15828 3.64645 4.35354L10.2929 11L6 11C5.72386 11 5.5 11.2239 5.5 11.5C5.5 11.7761 5.72386 12 6 12L11.5 12C11.6326 12 11.7598 11.9473 11.8536 11.8536C11.9473 11.7598 12 11.6326 12 11.5L12 5.99999C12 5.72385 11.7761 5.49999 11.5 5.49999C11.2239 5.49999 11 5.72385 11 5.99999V10.2929L4.35355 3.64643C4.15829 3.45117 3.84171 3.45117 3.64645 3.64644Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-down.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 2C7.77614 2 8 2.22386 8 2.5L8 11.2929L11.1464 8.14645C11.3417 7.95118 11.6583 7.95118 11.8536 8.14645C12.0488 8.34171 12.0488 8.65829 11.8536 8.85355L7.85355 12.8536C7.75979 12.9473 7.63261 13 7.5 13C7.36739 13 7.24021 12.9473 7.14645 12.8536L3.14645 8.85355C2.95118 8.65829 2.95118 8.34171 3.14645 8.14645C3.34171 7.95118 3.65829 7.95118 3.85355 8.14645L7 11.2929L7 2.5C7 2.22386 7.22386 2 7.5 2Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.85355 3.14645C7.04882 3.34171 7.04882 3.65829 6.85355 3.85355L3.70711 7H12.5C12.7761 7 13 7.22386 13 7.5C13 7.77614 12.7761 8 12.5 8H3.70711L6.85355 11.1464C7.04882 11.3417 7.04882 11.6583 6.85355 11.8536C6.65829 12.0488 6.34171 12.0488 6.14645 11.8536L2.14645 7.85355C1.95118 7.65829 1.95118 7.34171 2.14645 7.14645L6.14645 3.14645C6.34171 2.95118 6.65829 2.95118 6.85355 3.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.14645 3.14645C8.34171 2.95118 8.65829 2.95118 8.85355 3.14645L12.8536 7.14645C13.0488 7.34171 13.0488 7.65829 12.8536 7.85355L8.85355 11.8536C8.65829 12.0488 8.34171 12.0488 8.14645 11.8536C7.95118 11.6583 7.95118 11.3417 8.14645 11.1464L11.2929 8H2.5C2.22386 8 2 7.77614 2 7.5C2 7.22386 2.22386 7 2.5 7H11.2929L8.14645 3.85355C7.95118 3.65829 7.95118 3.34171 8.14645 3.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-top-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.3536 11.3536C11.5488 11.1583 11.5488 10.8417 11.3536 10.6465L4.70711 4L9 4C9.27614 4 9.5 3.77614 9.5 3.5C9.5 3.22386 9.27614 3 9 3L3.5 3C3.36739 3 3.24021 3.05268 3.14645 3.14645C3.05268 3.24022 3 3.36739 3 3.5L3 9.00001C3 9.27615 3.22386 9.50001 3.5 9.50001C3.77614 9.50001 4 9.27615 4 9.00001V4.70711L10.6464 11.3536C10.8417 11.5488 11.1583 11.5488 11.3536 11.3536Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-top-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.64645 11.3536C3.45118 11.1583 3.45118 10.8417 3.64645 10.6465L10.2929 4L6 4C5.72386 4 5.5 3.77614 5.5 3.5C5.5 3.22386 5.72386 3 6 3L11.5 3C11.6326 3 11.7598 3.05268 11.8536 3.14645C11.9473 3.24022 12 3.36739 12 3.5L12 9.00001C12 9.27615 11.7761 9.50001 11.5 9.50001C11.2239 9.50001 11 9.27615 11 9.00001V4.70711L4.35355 11.3536C4.15829 11.5488 3.84171 11.5488 3.64645 11.3536Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/arrow-up.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/aspect-ratio.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.5 2H12.5C12.7761 2 13 2.22386 13 2.5V12.5C13 12.7761 12.7761 13 12.5 13H2.5C2.22386 13 2 12.7761 2 12.5V2.5C2 2.22386 2.22386 2 2.5 2ZM1 2.5C1 1.67157 1.67157 1 2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5ZM7.5 4C7.77614 4 8 3.77614 8 3.5C8 3.22386 7.77614 3 7.5 3C7.22386 3 7 3.22386 7 3.5C7 3.77614 7.22386 4 7.5 4ZM8 5.5C8 5.77614 7.77614 6 7.5 6C7.22386 6 7 5.77614 7 5.5C7 5.22386 7.22386 5 7.5 5C7.77614 5 8 5.22386 8 5.5ZM7.5 8C7.77614 8 8 7.77614 8 7.5C8 7.22386 7.77614 7 7.5 7C7.22386 7 7 7.22386 7 7.5C7 7.77614 7.22386 8 7.5 8ZM10 7.5C10 7.77614 9.77614 8 9.5 8C9.22386 8 9 7.77614 9 7.5C9 7.22386 9.22386 7 9.5 7C9.77614 7 10 7.22386 10 7.5ZM11.5 8C11.7761 8 12 7.77614 12 7.5C12 7.22386 11.7761 7 11.5 7C11.2239 7 11 7.22386 11 7.5C11 7.77614 11.2239 8 11.5 8Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/avatar.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/backpack.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5 1C5 0.447715 5.44772 0 6 0H9C9.55228 0 10 0.447715 10 1V2H14C14.5523 2 15 2.44772 15 3V6C15 6.8888 14.6131 7.68734 14 8.23608V11.5C14 12.3284 13.3284 13 12.5 13H2.5C1.67157 13 1 12.3284 1 11.5V8.2359C0.38697 7.68721 0 6.88883 0 6V3C0 2.44772 0.447716 2 1 2H5V1ZM9 1V2H6V1H9ZM1 3H5H5.5H9.5H10H14V6C14 6.654 13.6866 7.23467 13.1997 7.6004C12.8655 7.85144 12.4508 8 12 8H8V7.5C8 7.22386 7.77614 7 7.5 7C7.22386 7 7 7.22386 7 7.5V8H3C2.5493 8 2.1346 7.85133 1.80029 7.60022C1.31335 7.23446 1 6.65396 1 6V3ZM7 9H3C2.64961 9 2.31292 8.93972 2 8.82905V11.5C2 11.7761 2.22386 12 2.5 12H12.5C12.7761 12 13 11.7761 13 11.5V8.82915C12.6871 8.93978 12.3504 9 12 9H8V9.5C8 9.77614 7.77614 10 7.5 10C7.22386 10 7 9.77614 7 9.5V9Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/badge.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.5 6H11.5C12.3284 6 13 6.67157 13 7.5C13 8.32843 12.3284 9 11.5 9H3.5C2.67157 9 2 8.32843 2 7.5C2 6.67157 2.67157 6 3.5 6ZM1 7.5C1 6.11929 2.11929 5 3.5 5H11.5C12.8807 5 14 6.11929 14 7.5C14 8.88071 12.8807 10 11.5 10H3.5C2.11929 10 1 8.88071 1 7.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/bar-chart.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.5 1C11.7761 1 12 1.22386 12 1.5V13.5C12 13.7761 11.7761 14 11.5 14C11.2239 14 11 13.7761 11 13.5V1.5C11 1.22386 11.2239 1 11.5 1ZM9.5 3C9.77614 3 10 3.22386 10 3.5V13.5C10 13.7761 9.77614 14 9.5 14C9.22386 14 9 13.7761 9 13.5V3.5C9 3.22386 9.22386 3 9.5 3ZM13.5 3C13.7761 3 14 3.22386 14 3.5V13.5C14 13.7761 13.7761 14 13.5 14C13.2239 14 13 13.7761 13 13.5V3.5C13 3.22386 13.2239 3 13.5 3ZM5.5 4C5.77614 4 6 4.22386 6 4.5V13.5C6 13.7761 5.77614 14 5.5 14C5.22386 14 5 13.7761 5 13.5V4.5C5 4.22386 5.22386 4 5.5 4ZM1.5 5C1.77614 5 2 5.22386 2 5.5V13.5C2 13.7761 1.77614 14 1.5 14C1.22386 14 1 13.7761 1 13.5V5.5C1 5.22386 1.22386 5 1.5 5ZM7.5 5C7.77614 5 8 5.22386 8 5.5V13.5C8 13.7761 7.77614 14 7.5 14C7.22386 14 7 13.7761 7 13.5V5.5C7 5.22386 7.22386 5 7.5 5ZM3.5 7C3.77614 7 4 7.22386 4 7.5V13.5C4 13.7761 3.77614 14 3.5 14C3.22386 14 3 13.7761 3 13.5V7.5C3 7.22386 3.22386 7 3.5 7Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/bell.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/blending-mode.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 9C3 6.5 4.5 4.25 7.5 1.5C10.5 4.25 12 6.5 12 9C12 11.4853 9.98528 13.5 7.5 13.5C5.01472 13.5 3 11.4853 3 9ZM10.9524 8.30307C9.67347 7.82121 8.2879 8.46208 6.98956 9.06259C5.9327 9.55142 4.93365 10.0135 4.09695 9.82153C4.03357 9.55804 4 9.28294 4 9C4 7.11203 5.02686 5.27195 7.5 2.87357C9.66837 4.97639 10.725 6.65004 10.9524 8.30307Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/bookmark-filled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.5 2C3.22386 2 3 2.22386 3 2.5V13.5C3 13.6818 3.09864 13.8492 3.25762 13.9373C3.41659 14.0254 3.61087 14.0203 3.765 13.924L7.5 11.5896L11.235 13.924C11.3891 14.0203 11.5834 14.0254 11.7424 13.9373C11.9014 13.8492 12 13.6818 12 13.5V2.5C12 2.22386 11.7761 2 11.5 2H3.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/bookmark.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 2.5C3 2.22386 3.22386 2 3.5 2H11.5C11.7761 2 12 2.22386 12 2.5V13.5C12 13.6818 11.9014 13.8492 11.7424 13.9373C11.5834 14.0254 11.3891 14.0203 11.235 13.924L7.5 11.5896L3.765 13.924C3.61087 14.0203 3.41659 14.0254 3.25762 13.9373C3.09864 13.8492 3 13.6818 3 13.5V2.5ZM4 3V12.5979L6.97 10.7416C7.29427 10.539 7.70573 10.539 8.03 10.7416L11 12.5979V3H4Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/border-all.svg πŸ”—

@@ -0,0 +1,17 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.25 1C0.25 0.585786 0.585786 0.25 1 0.25H14C14.4142 0.25 14.75 0.585786 14.75 1V14C14.75 14.4142 14.4142 14.75 14 14.75H1C0.585786 14.75 0.25 14.4142 0.25 14V1ZM1.75 1.75V13.25H13.25V1.75H1.75Z"
+    fill="currentColor"
+  />
+  <rect x="7" y="5" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="3" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="9" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="11" width="1" height="1" rx=".5" fill="currentColor" />
+</svg>

assets/icons/radix/border-bottom.svg πŸ”—

@@ -0,0 +1,29 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd" d="M1 13.25L14 13.25V14.75L1 14.75V13.25Z" fill="currentColor" />
+  <rect x="7" y="5" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="5" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="3" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="3" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="9" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="9" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="11" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="11" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="5" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="3" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="7" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="1" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="9" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="11" width="1" height="1" rx=".5" fill="currentColor" />
+</svg>

assets/icons/radix/border-dashed.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0 7.5C0 7.22386 0.223858 7 0.5 7H3C3.27614 7 3.5 7.22386 3.5 7.5C3.5 7.77614 3.27614 8 3 8H0.5C0.223858 8 0 7.77614 0 7.5ZM5.75 7.5C5.75 7.22386 5.97386 7 6.25 7H8.75C9.02614 7 9.25 7.22386 9.25 7.5C9.25 7.77614 9.02614 8 8.75 8H6.25C5.97386 8 5.75 7.77614 5.75 7.5ZM12 7C11.7239 7 11.5 7.22386 11.5 7.5C11.5 7.77614 11.7239 8 12 8H14.5C14.7761 8 15 7.77614 15 7.5C15 7.22386 14.7761 7 14.5 7H12Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/border-dotted.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.5 6.625C1.01675 6.625 0.625 7.01675 0.625 7.5C0.625 7.98325 1.01675 8.375 1.5 8.375C1.98325 8.375 2.375 7.98325 2.375 7.5C2.375 7.01675 1.98325 6.625 1.5 6.625ZM5.5 6.625C5.01675 6.625 4.625 7.01675 4.625 7.5C4.625 7.98325 5.01675 8.375 5.5 8.375C5.98325 8.375 6.375 7.98325 6.375 7.5C6.375 7.01675 5.98325 6.625 5.5 6.625ZM9.5 6.625C9.01675 6.625 8.625 7.01675 8.625 7.5C8.625 7.98325 9.01675 8.375 9.5 8.375C9.98325 8.375 10.375 7.98325 10.375 7.5C10.375 7.01675 9.98325 6.625 9.5 6.625ZM12.625 7.5C12.625 7.01675 13.0168 6.625 13.5 6.625C13.9832 6.625 14.375 7.01675 14.375 7.5C14.375 7.98325 13.9832 8.375 13.5 8.375C13.0168 8.375 12.625 7.98325 12.625 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/border-left.svg πŸ”—

@@ -0,0 +1,29 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd" d="M1.75 1L1.75 14L0.249999 14L0.25 1L1.75 1Z" fill="currentColor" />
+  <rect x="10" y="7" width="1" height="1" rx=".5" transform="rotate(90 10 7)" fill="currentColor" />
+  <rect x="10" y="13" width="1" height="1" rx=".5" transform="rotate(90 10 13)" fill="currentColor" />
+  <rect x="12" y="7" width="1" height="1" rx=".5" transform="rotate(90 12 7)" fill="currentColor" />
+  <rect x="12" y="13" width="1" height="1" rx=".5" transform="rotate(90 12 13)" fill="currentColor" />
+  <rect x="8" y="7" width="1" height="1" rx=".5" transform="rotate(90 8 7)" fill="currentColor" />
+  <rect x="14" y="7" width="1" height="1" rx=".5" transform="rotate(90 14 7)" fill="currentColor" />
+  <rect x="8" y="13" width="1" height="1" rx=".5" transform="rotate(90 8 13)" fill="currentColor" />
+  <rect x="14" y="13" width="1" height="1" rx=".5" transform="rotate(90 14 13)" fill="currentColor" />
+  <rect x="8" y="5" width="1" height="1" rx=".5" transform="rotate(90 8 5)" fill="currentColor" />
+  <rect x="14" y="5" width="1" height="1" rx=".5" transform="rotate(90 14 5)" fill="currentColor" />
+  <rect x="8" y="3" width="1" height="1" rx=".5" transform="rotate(90 8 3)" fill="currentColor" />
+  <rect x="14" y="3" width="1" height="1" rx=".5" transform="rotate(90 14 3)" fill="currentColor" />
+  <rect x="8" y="9" width="1" height="1" rx=".5" transform="rotate(90 8 9)" fill="currentColor" />
+  <rect x="14" y="9" width="1" height="1" rx=".5" transform="rotate(90 14 9)" fill="currentColor" />
+  <rect x="8" y="11" width="1" height="1" rx=".5" transform="rotate(90 8 11)" fill="currentColor" />
+  <rect x="14" y="11" width="1" height="1" rx=".5" transform="rotate(90 14 11)" fill="currentColor" />
+  <rect x="6" y="7" width="1" height="1" rx=".5" transform="rotate(90 6 7)" fill="currentColor" />
+  <rect x="6" y="13" width="1" height="1" rx=".5" transform="rotate(90 6 13)" fill="currentColor" />
+  <rect x="4" y="7" width="1" height="1" rx=".5" transform="rotate(90 4 7)" fill="currentColor" />
+  <rect x="4" y="13" width="1" height="1" rx=".5" transform="rotate(90 4 13)" fill="currentColor" />
+  <rect x="10" y="1" width="1" height="1" rx=".5" transform="rotate(90 10 1)" fill="currentColor" />
+  <rect x="12" y="1" width="1" height="1" rx=".5" transform="rotate(90 12 1)" fill="currentColor" />
+  <rect x="8" y="1" width="1" height="1" rx=".5" transform="rotate(90 8 1)" fill="currentColor" />
+  <rect x="14" y="1" width="1" height="1" rx=".5" transform="rotate(90 14 1)" fill="currentColor" />
+  <rect x="6" y="1" width="1" height="1" rx=".5" transform="rotate(90 6 1)" fill="currentColor" />
+  <rect x="4" y="1" width="1" height="1" rx=".5" transform="rotate(90 4 1)" fill="currentColor" />
+</svg>

assets/icons/radix/border-none.svg πŸ”—

@@ -0,0 +1,35 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect x="7" y="5.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="5.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="3.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="3.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="9.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="9.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="11.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="11.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="5.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="3.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="9.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="11.025" width="1" height="1" rx=".5" fill="currentColor" />
+</svg>

assets/icons/radix/border-right.svg πŸ”—

@@ -0,0 +1,29 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd" d="M13.25 1L13.25 14L14.75 14L14.75 1L13.25 1Z" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 5 7)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 5 13)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 3 7)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 3 13)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 7)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 7)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 13)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 13)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 5)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 5)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 3)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 3)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 9)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 9)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 11)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 11)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 9 7)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 9 13)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 11 7)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 11 13)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 5 1)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 3 1)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 7 1)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 1 1)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 9 1)" fill="currentColor" />
+  <rect width="1" height="1" rx=".5" transform="matrix(0 1 1 0 11 1)" fill="currentColor" />
+</svg>

assets/icons/radix/border-solid.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.25 7.5C1.25 7.22386 1.47386 7 1.75 7H13.25C13.5261 7 13.75 7.22386 13.75 7.5C13.75 7.77614 13.5261 8 13.25 8H1.75C1.47386 8 1.25 7.77614 1.25 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/border-split.svg πŸ”—

@@ -0,0 +1,21 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect x="7" y="5.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="3.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="13.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="1.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="13" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="5" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="3" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="9" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="11" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="9.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="7" y="11.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <rect x="1" y="7.025" width="1" height="1" rx=".5" fill="currentColor" />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 1.49994C1 1.2238 1.22386 0.999939 1.5 0.999939H6V1.99994H2V5.99994H1V1.49994ZM13 1.99994H9V0.999939H13.5C13.7761 0.999939 14 1.2238 14 1.49994V5.99994H13V1.99994ZM1 13.4999V8.99994H2V12.9999H6V13.9999H1.5C1.22386 13.9999 1 13.7761 1 13.4999ZM13 12.9999V8.99994H14V13.4999C14 13.7761 13.7761 13.9999 13.5 13.9999H9.5V12.9999H13Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/border-top.svg πŸ”—

@@ -0,0 +1,29 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd" d="M14 1.75L1 1.75L1 0.249999L14 0.25L14 1.75Z" fill="currentColor" />
+  <rect x="8" y="10" width="1" height="1" rx=".5" transform="rotate(-180 8 10)" fill="currentColor" />
+  <rect x="2" y="10" width="1" height="1" rx=".5" transform="rotate(-180 2 10)" fill="currentColor" />
+  <rect x="8" y="12" width="1" height="1" rx=".5" transform="rotate(-180 8 12)" fill="currentColor" />
+  <rect x="2" y="12" width="1" height="1" rx=".5" transform="rotate(-180 2 12)" fill="currentColor" />
+  <rect x="8" y="8" width="1" height="1" rx=".5" transform="rotate(-180 8 8)" fill="currentColor" />
+  <rect x="8" y="14" width="1" height="1" rx=".5" transform="rotate(-180 8 14)" fill="currentColor" />
+  <rect x="2" y="8" width="1" height="1" rx=".5" transform="rotate(-180 2 8)" fill="currentColor" />
+  <rect x="2" y="14" width="1" height="1" rx=".5" transform="rotate(-180 2 14)" fill="currentColor" />
+  <rect x="10" y="8" width="1" height="1" rx=".5" transform="rotate(-180 10 8)" fill="currentColor" />
+  <rect x="10" y="14" width="1" height="1" rx=".5" transform="rotate(-180 10 14)" fill="currentColor" />
+  <rect x="12" y="8" width="1" height="1" rx=".5" transform="rotate(-180 12 8)" fill="currentColor" />
+  <rect x="12" y="14" width="1" height="1" rx=".5" transform="rotate(-180 12 14)" fill="currentColor" />
+  <rect x="6" y="8" width="1" height="1" rx=".5" transform="rotate(-180 6 8)" fill="currentColor" />
+  <rect x="6" y="14" width="1" height="1" rx=".5" transform="rotate(-180 6 14)" fill="currentColor" />
+  <rect x="4" y="8" width="1" height="1" rx=".5" transform="rotate(-180 4 8)" fill="currentColor" />
+  <rect x="4" y="14" width="1" height="1" rx=".5" transform="rotate(-180 4 14)" fill="currentColor" />
+  <rect x="8" y="6" width="1" height="1" rx=".5" transform="rotate(-180 8 6)" fill="currentColor" />
+  <rect x="2" y="6" width="1" height="1" rx=".5" transform="rotate(-180 2 6)" fill="currentColor" />
+  <rect x="8" y="4" width="1" height="1" rx=".5" transform="rotate(-180 8 4)" fill="currentColor" />
+  <rect x="2" y="4" width="1" height="1" rx=".5" transform="rotate(-180 2 4)" fill="currentColor" />
+  <rect x="14" y="10" width="1" height="1" rx=".5" transform="rotate(-180 14 10)" fill="currentColor" />
+  <rect x="14" y="12" width="1" height="1" rx=".5" transform="rotate(-180 14 12)" fill="currentColor" />
+  <rect x="14" y="8" width="1" height="1" rx=".5" transform="rotate(-180 14 8)" fill="currentColor" />
+  <rect x="14" y="14" width="1" height="1" rx=".5" transform="rotate(-180 14 14)" fill="currentColor" />
+  <rect x="14" y="6" width="1" height="1" rx=".5" transform="rotate(-180 14 6)" fill="currentColor" />
+  <rect x="14" y="4" width="1" height="1" rx=".5" transform="rotate(-180 14 4)" fill="currentColor" />
+</svg>

assets/icons/radix/border-width.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 3H14V4H1V3ZM1 6H14V8H1V6ZM14 10.25H1V12.75H14V10.25Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/box-model.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.99998 0.999976C1.44769 0.999976 0.999976 1.44769 0.999976 1.99998V13C0.999976 13.5523 1.44769 14 1.99998 14H13C13.5523 14 14 13.5523 14 13V1.99998C14 1.44769 13.5523 0.999976 13 0.999976H1.99998ZM1.99998 1.99998L13 1.99998V13H1.99998V1.99998ZM4.49996 3.99996C4.22382 3.99996 3.99996 4.22382 3.99996 4.49996V10.5C3.99996 10.7761 4.22382 11 4.49996 11H10.5C10.7761 11 11 10.7761 11 10.5V4.49996C11 4.22382 10.7761 3.99996 10.5 3.99996H4.49996ZM4.99996 9.99996V4.99996H9.99996V9.99996H4.99996Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/box.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.5 2H2.5C2.22386 2 2 2.22386 2 2.5V12.5C2 12.7761 2.22386 13 2.5 13H12.5C12.7761 13 13 12.7761 13 12.5V2.5C13 2.22386 12.7761 2 12.5 2ZM2.5 1C1.67157 1 1 1.67157 1 2.5V12.5C1 13.3284 1.67157 14 2.5 14H12.5C13.3284 14 14 13.3284 14 12.5V2.5C14 1.67157 13.3284 1 12.5 1H2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/button.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 5H13C13.5523 5 14 5.44772 14 6V9C14 9.55228 13.5523 10 13 10H2C1.44772 10 1 9.55228 1 9V6C1 5.44772 1.44772 5 2 5ZM0 6C0 4.89543 0.895431 4 2 4H13C14.1046 4 15 4.89543 15 6V9C15 10.1046 14.1046 11 13 11H2C0.89543 11 0 10.1046 0 9V6ZM4.5 6.75C4.08579 6.75 3.75 7.08579 3.75 7.5C3.75 7.91421 4.08579 8.25 4.5 8.25C4.91421 8.25 5.25 7.91421 5.25 7.5C5.25 7.08579 4.91421 6.75 4.5 6.75ZM6.75 7.5C6.75 7.08579 7.08579 6.75 7.5 6.75C7.91421 6.75 8.25 7.08579 8.25 7.5C8.25 7.91421 7.91421 8.25 7.5 8.25C7.08579 8.25 6.75 7.91421 6.75 7.5ZM10.5 6.75C10.0858 6.75 9.75 7.08579 9.75 7.5C9.75 7.91421 10.0858 8.25 10.5 8.25C10.9142 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 10.9142 6.75 10.5 6.75Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/calendar.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/camera.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 3C1.44772 3 1 3.44772 1 4V11C1 11.5523 1.44772 12 2 12H13C13.5523 12 14 11.5523 14 11V4C14 3.44772 13.5523 3 13 3H2ZM0 4C0 2.89543 0.895431 2 2 2H13C14.1046 2 15 2.89543 15 4V11C15 12.1046 14.1046 13 13 13H2C0.895431 13 0 12.1046 0 11V4ZM2 4.25C2 4.11193 2.11193 4 2.25 4H4.75C4.88807 4 5 4.11193 5 4.25V5.75454C5 5.89261 4.88807 6.00454 4.75 6.00454H2.25C2.11193 6.00454 2 5.89261 2 5.75454V4.25ZM12.101 7.58421C12.101 9.02073 10.9365 10.1853 9.49998 10.1853C8.06346 10.1853 6.89893 9.02073 6.89893 7.58421C6.89893 6.14769 8.06346 4.98315 9.49998 4.98315C10.9365 4.98315 12.101 6.14769 12.101 7.58421ZM13.101 7.58421C13.101 9.57302 11.4888 11.1853 9.49998 11.1853C7.51117 11.1853 5.89893 9.57302 5.89893 7.58421C5.89893 5.5954 7.51117 3.98315 9.49998 3.98315C11.4888 3.98315 13.101 5.5954 13.101 7.58421Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/card-stack-minus.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.5 3C2.22386 3 2 3.22386 2 3.5V9.5C2 9.77614 2.22386 10 2.5 10H12.5C12.7761 10 13 9.77614 13 9.5V3.5C13 3.22386 12.7761 3 12.5 3H2.5ZM1 9.5C1 10.1531 1.4174 10.7087 2 10.9146V11.5C2 12.3284 2.67157 13 3.5 13H11.5C12.3284 13 13 12.3284 13 11.5V10.9146C13.5826 10.7087 14 10.1531 14 9.5V3.5C14 2.67157 13.3284 2 12.5 2H2.5C1.67157 2 1 2.67157 1 3.5V9.5ZM12 11.5V11H3V11.5C3 11.7761 3.22386 12 3.5 12H11.5C11.7761 12 12 11.7761 12 11.5ZM5.5 6C5.22386 6 5 6.22386 5 6.5C5 6.77614 5.22386 7 5.5 7H9.5C9.77614 7 10 6.77614 10 6.5C10 6.22386 9.77614 6 9.5 6H5.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/card-stack-plus.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 3.5C2 3.22386 2.22386 3 2.5 3H12.5C12.7761 3 13 3.22386 13 3.5V9.5C13 9.77614 12.7761 10 12.5 10H2.5C2.22386 10 2 9.77614 2 9.5V3.5ZM2 10.9146C1.4174 10.7087 1 10.1531 1 9.5V3.5C1 2.67157 1.67157 2 2.5 2H12.5C13.3284 2 14 2.67157 14 3.5V9.5C14 10.1531 13.5826 10.7087 13 10.9146V11.5C13 12.3284 12.3284 13 11.5 13H3.5C2.67157 13 2 12.3284 2 11.5V10.9146ZM12 11V11.5C12 11.7761 11.7761 12 11.5 12H3.5C3.22386 12 3 11.7761 3 11.5V11H12ZM5 6.5C5 6.22386 5.22386 6 5.5 6H7V4.5C7 4.22386 7.22386 4 7.5 4C7.77614 4 8 4.22386 8 4.5V6H9.5C9.77614 6 10 6.22386 10 6.5C10 6.77614 9.77614 7 9.5 7H8V8.5C8 8.77614 7.77614 9 7.5 9C7.22386 9 7 8.77614 7 8.5V7H5.5C5.22386 7 5 6.77614 5 6.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/card-stack.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 3.5C2 3.22386 2.22386 3 2.5 3H12.5C12.7761 3 13 3.22386 13 3.5V9.5C13 9.77614 12.7761 10 12.5 10H2.5C2.22386 10 2 9.77614 2 9.5V3.5ZM2 10.9146C1.4174 10.7087 1 10.1531 1 9.5V3.5C1 2.67157 1.67157 2 2.5 2H12.5C13.3284 2 14 2.67157 14 3.5V9.5C14 10.1531 13.5826 10.7087 13 10.9146V11.5C13 12.3284 12.3284 13 11.5 13H3.5C2.67157 13 2 12.3284 2 11.5V10.9146ZM12 11V11.5C12 11.7761 11.7761 12 11.5 12H3.5C3.22386 12 3 11.7761 3 11.5V11H12Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/caret-down.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.18179 6.18181C4.35753 6.00608 4.64245 6.00608 4.81819 6.18181L7.49999 8.86362L10.1818 6.18181C10.3575 6.00608 10.6424 6.00608 10.8182 6.18181C10.9939 6.35755 10.9939 6.64247 10.8182 6.81821L7.81819 9.81821C7.73379 9.9026 7.61934 9.95001 7.49999 9.95001C7.38064 9.95001 7.26618 9.9026 7.18179 9.81821L4.18179 6.81821C4.00605 6.64247 4.00605 6.35755 4.18179 6.18181Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/caret-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.81809 4.18179C8.99383 4.35753 8.99383 4.64245 8.81809 4.81819L6.13629 7.49999L8.81809 10.1818C8.99383 10.3575 8.99383 10.6424 8.81809 10.8182C8.64236 10.9939 8.35743 10.9939 8.1817 10.8182L5.1817 7.81819C5.09731 7.73379 5.0499 7.61933 5.0499 7.49999C5.0499 7.38064 5.09731 7.26618 5.1817 7.18179L8.1817 4.18179C8.35743 4.00605 8.64236 4.00605 8.81809 4.18179Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/caret-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.18194 4.18185C6.35767 4.00611 6.6426 4.00611 6.81833 4.18185L9.81833 7.18185C9.90272 7.26624 9.95013 7.3807 9.95013 7.50005C9.95013 7.6194 9.90272 7.73386 9.81833 7.81825L6.81833 10.8182C6.6426 10.994 6.35767 10.994 6.18194 10.8182C6.0062 10.6425 6.0062 10.3576 6.18194 10.1819L8.86374 7.50005L6.18194 4.81825C6.0062 4.64251 6.0062 4.35759 6.18194 4.18185Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/caret-sort.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.93179 5.43179C4.75605 5.60753 4.75605 5.89245 4.93179 6.06819C5.10753 6.24392 5.39245 6.24392 5.56819 6.06819L7.49999 4.13638L9.43179 6.06819C9.60753 6.24392 9.89245 6.24392 10.0682 6.06819C10.2439 5.89245 10.2439 5.60753 10.0682 5.43179L7.81819 3.18179C7.73379 3.0974 7.61933 3.04999 7.49999 3.04999C7.38064 3.04999 7.26618 3.0974 7.18179 3.18179L4.93179 5.43179ZM10.0682 9.56819C10.2439 9.39245 10.2439 9.10753 10.0682 8.93179C9.89245 8.75606 9.60753 8.75606 9.43179 8.93179L7.49999 10.8636L5.56819 8.93179C5.39245 8.75606 5.10753 8.75606 4.93179 8.93179C4.75605 9.10753 4.75605 9.39245 4.93179 9.56819L7.18179 11.8182C7.35753 11.9939 7.64245 11.9939 7.81819 11.8182L10.0682 9.56819Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/caret-up.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.18179 8.81819C4.00605 8.64245 4.00605 8.35753 4.18179 8.18179L7.18179 5.18179C7.26618 5.0974 7.38064 5.04999 7.49999 5.04999C7.61933 5.04999 7.73379 5.0974 7.81819 5.18179L10.8182 8.18179C10.9939 8.35753 10.9939 8.64245 10.8182 8.81819C10.6424 8.99392 10.3575 8.99392 10.1818 8.81819L7.49999 6.13638L4.81819 8.81819C4.64245 8.99392 4.35753 8.99392 4.18179 8.81819Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/chat-bubble.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.5 3L2.5 3.00002C1.67157 3.00002 1 3.6716 1 4.50002V9.50003C1 10.3285 1.67157 11 2.5 11H7.50003C7.63264 11 7.75982 11.0527 7.85358 11.1465L10 13.2929V11.5C10 11.2239 10.2239 11 10.5 11H12.5C13.3284 11 14 10.3285 14 9.50003V4.5C14 3.67157 13.3284 3 12.5 3ZM2.49999 2.00002L12.5 2C13.8807 2 15 3.11929 15 4.5V9.50003C15 10.8807 13.8807 12 12.5 12H11V14.5C11 14.7022 10.8782 14.8845 10.6913 14.9619C10.5045 15.0393 10.2894 14.9965 10.1464 14.8536L7.29292 12H2.5C1.11929 12 0 10.8807 0 9.50003V4.50002C0 3.11931 1.11928 2.00003 2.49999 2.00002Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/check-circled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.877045C3.84222 0.877045 0.877075 3.84219 0.877075 7.49988C0.877075 11.1575 3.84222 14.1227 7.49991 14.1227C11.1576 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 3.84219 11.1576 0.877045 7.49991 0.877045ZM1.82708 7.49988C1.82708 4.36686 4.36689 1.82704 7.49991 1.82704C10.6329 1.82704 13.1727 4.36686 13.1727 7.49988C13.1727 10.6329 10.6329 13.1727 7.49991 13.1727C4.36689 13.1727 1.82708 10.6329 1.82708 7.49988ZM10.1589 5.53774C10.3178 5.31191 10.2636 5.00001 10.0378 4.84109C9.81194 4.68217 9.50004 4.73642 9.34112 4.96225L6.51977 8.97154L5.35681 7.78706C5.16334 7.59002 4.84677 7.58711 4.64973 7.78058C4.45268 7.97404 4.44978 8.29061 4.64325 8.48765L6.22658 10.1003C6.33054 10.2062 6.47617 10.2604 6.62407 10.2483C6.77197 10.2363 6.90686 10.1591 6.99226 10.0377L10.1589 5.53774Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/check.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/checkbox.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 3H12V12H3L3 3ZM2 3C2 2.44771 2.44772 2 3 2H12C12.5523 2 13 2.44772 13 3V12C13 12.5523 12.5523 13 12 13H3C2.44771 13 2 12.5523 2 12V3ZM10.3498 5.51105C10.506 5.28337 10.4481 4.97212 10.2204 4.81587C9.99275 4.65961 9.6815 4.71751 9.52525 4.94519L6.64048 9.14857L5.19733 7.40889C5.02102 7.19635 4.7058 7.16699 4.49327 7.34329C4.28073 7.5196 4.25137 7.83482 4.42767 8.04735L6.2934 10.2964C6.39348 10.4171 6.54437 10.4838 6.70097 10.4767C6.85757 10.4695 7.00177 10.3894 7.09047 10.2601L10.3498 5.51105Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/chevron-down.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/chevron-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.84182 3.13514C9.04327 3.32401 9.05348 3.64042 8.86462 3.84188L5.43521 7.49991L8.86462 11.1579C9.05348 11.3594 9.04327 11.6758 8.84182 11.8647C8.64036 12.0535 8.32394 12.0433 8.13508 11.8419L4.38508 7.84188C4.20477 7.64955 4.20477 7.35027 4.38508 7.15794L8.13508 3.15794C8.32394 2.95648 8.64036 2.94628 8.84182 3.13514Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/chevron-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/chevron-up.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.13523 8.84197C3.3241 9.04343 3.64052 9.05363 3.84197 8.86477L7.5 5.43536L11.158 8.86477C11.3595 9.05363 11.6759 9.04343 11.8648 8.84197C12.0536 8.64051 12.0434 8.32409 11.842 8.13523L7.84197 4.38523C7.64964 4.20492 7.35036 4.20492 7.15803 4.38523L3.15803 8.13523C2.95657 8.32409 2.94637 8.64051 3.13523 8.84197Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/circle-backslash.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.877075C3.84222 0.877075 0.877075 3.84222 0.877075 7.49991C0.877075 11.1576 3.84222 14.1227 7.49991 14.1227C11.1576 14.1227 14.1227 11.1576 14.1227 7.49991C14.1227 3.84222 11.1576 0.877075 7.49991 0.877075ZM3.85768 3.15057C4.84311 2.32448 6.11342 1.82708 7.49991 1.82708C10.6329 1.82708 13.1727 4.36689 13.1727 7.49991C13.1727 8.88638 12.6753 10.1567 11.8492 11.1421L3.85768 3.15057ZM3.15057 3.85768C2.32448 4.84311 1.82708 6.11342 1.82708 7.49991C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C8.88638 13.1727 10.1567 12.6753 11.1421 11.8492L3.15057 3.85768Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/circle.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49991C0.877075 3.84222 3.84222 0.877075 7.49991 0.877075C11.1576 0.877075 14.1227 3.84222 14.1227 7.49991C14.1227 11.1576 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1576 0.877075 7.49991ZM7.49991 1.82708C4.36689 1.82708 1.82708 4.36689 1.82708 7.49991C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49991C13.1727 4.36689 10.6329 1.82708 7.49991 1.82708Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/clipboard.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5 2V1H10V2H5ZM4.75 0C4.33579 0 4 0.335786 4 0.75V1H3.5C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V2.5C13 1.67157 12.3284 1 11.5 1H11V0.75C11 0.335786 10.6642 0 10.25 0H4.75ZM11 2V2.25C11 2.66421 10.6642 3 10.25 3H4.75C4.33579 3 4 2.66421 4 2.25V2H3.5C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V2.5C12 2.22386 11.7761 2 11.5 2H11Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/clock.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.50009 0.877014C3.84241 0.877014 0.877258 3.84216 0.877258 7.49984C0.877258 11.1575 3.8424 14.1227 7.50009 14.1227C11.1578 14.1227 14.1229 11.1575 14.1229 7.49984C14.1229 3.84216 11.1577 0.877014 7.50009 0.877014ZM1.82726 7.49984C1.82726 4.36683 4.36708 1.82701 7.50009 1.82701C10.6331 1.82701 13.1729 4.36683 13.1729 7.49984C13.1729 10.6328 10.6331 13.1727 7.50009 13.1727C4.36708 13.1727 1.82726 10.6328 1.82726 7.49984ZM8 4.50001C8 4.22387 7.77614 4.00001 7.5 4.00001C7.22386 4.00001 7 4.22387 7 4.50001V7.50001C7 7.63262 7.05268 7.7598 7.14645 7.85357L9.14645 9.85357C9.34171 10.0488 9.65829 10.0488 9.85355 9.85357C10.0488 9.65831 10.0488 9.34172 9.85355 9.14646L8 7.29291V4.50001Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/code.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.96424 2.68571C10.0668 2.42931 9.94209 2.13833 9.6857 2.03577C9.4293 1.93322 9.13832 2.05792 9.03576 2.31432L5.03576 12.3143C4.9332 12.5707 5.05791 12.8617 5.3143 12.9642C5.5707 13.0668 5.86168 12.9421 5.96424 12.6857L9.96424 2.68571ZM3.85355 5.14646C4.04882 5.34172 4.04882 5.6583 3.85355 5.85356L2.20711 7.50001L3.85355 9.14646C4.04882 9.34172 4.04882 9.6583 3.85355 9.85356C3.65829 10.0488 3.34171 10.0488 3.14645 9.85356L1.14645 7.85356C0.951184 7.6583 0.951184 7.34172 1.14645 7.14646L3.14645 5.14646C3.34171 4.9512 3.65829 4.9512 3.85355 5.14646ZM11.1464 5.14646C11.3417 4.9512 11.6583 4.9512 11.8536 5.14646L13.8536 7.14646C14.0488 7.34172 14.0488 7.6583 13.8536 7.85356L11.8536 9.85356C11.6583 10.0488 11.3417 10.0488 11.1464 9.85356C10.9512 9.6583 10.9512 9.34172 11.1464 9.14646L12.7929 7.50001L11.1464 5.85356C10.9512 5.6583 10.9512 5.34172 11.1464 5.14646Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/color-wheel.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49985C0.877075 3.84216 3.84222 0.877014 7.49991 0.877014C11.1576 0.877014 14.1227 3.84216 14.1227 7.49985C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49985ZM3.78135 3.21565C4.68298 2.43239 5.83429 1.92904 7.09998 1.84089V6.53429L3.78135 3.21565ZM3.21567 3.78134C2.43242 4.68298 1.92909 5.83428 1.84095 7.09997H6.5343L3.21567 3.78134ZM6.5343 7.89997H1.84097C1.92916 9.16562 2.43253 10.3169 3.21579 11.2185L6.5343 7.89997ZM3.78149 11.7842C4.6831 12.5673 5.83435 13.0707 7.09998 13.1588V8.46566L3.78149 11.7842ZM7.89998 8.46566V13.1588C9.16559 13.0706 10.3168 12.5673 11.2184 11.7841L7.89998 8.46566ZM11.7841 11.2184C12.5673 10.3168 13.0707 9.16558 13.1588 7.89997H8.46567L11.7841 11.2184ZM8.46567 7.09997H13.1589C13.0707 5.83432 12.5674 4.68305 11.7842 3.78143L8.46567 7.09997ZM11.2185 3.21573C10.3169 2.43246 9.16565 1.92909 7.89998 1.8409V6.53429L11.2185 3.21573Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/columns.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.14998 14V1H0.849976V14H2.14998ZM6.14998 14V1H4.84998V14H6.14998ZM10.15 1V14H8.84998V1H10.15ZM14.15 14V1H12.85V14H14.15Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/commit.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.94969 7.49989C9.94969 8.85288 8.85288 9.94969 7.49989 9.94969C6.14691 9.94969 5.0501 8.85288 5.0501 7.49989C5.0501 6.14691 6.14691 5.0501 7.49989 5.0501C8.85288 5.0501 9.94969 6.14691 9.94969 7.49989ZM10.8632 8C10.6213 9.64055 9.20764 10.8997 7.49989 10.8997C5.79214 10.8997 4.37847 9.64055 4.13662 8H0.5C0.223858 8 0 7.77614 0 7.5C0 7.22386 0.223858 7 0.5 7H4.13659C4.37835 5.35935 5.79206 4.1001 7.49989 4.1001C9.20772 4.1001 10.6214 5.35935 10.8632 7H14.5C14.7761 7 15 7.22386 15 7.5C15 7.77614 14.7761 8 14.5 8H10.8632Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/component-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/component-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/component-boolean.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.85367 1.48956C7.65841 1.29429 7.34182 1.29429 7.14656 1.48956L1.48971 7.14641C1.29445 7.34167 1.29445 7.65825 1.48971 7.85352L7.14656 13.5104C7.34182 13.7056 7.65841 13.7056 7.85367 13.5104L13.5105 7.85352C13.7058 7.65825 13.7058 7.34167 13.5105 7.14641L7.85367 1.48956ZM7.5 2.55033L2.55037 7.49996L7.5 12.4496V2.55033Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/component-instance.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.1465 1.48959C7.34176 1.29432 7.65835 1.29432 7.85361 1.48959L13.5105 7.14644C13.7057 7.3417 13.7057 7.65829 13.5105 7.85355L7.85361 13.5104C7.65835 13.7057 7.34176 13.7057 7.1465 13.5104L1.48965 7.85355C1.29439 7.65829 1.29439 7.3417 1.48965 7.14644L7.1465 1.48959ZM7.50005 2.55025L2.55031 7.49999L7.50005 12.4497L12.4498 7.49999L7.50005 2.55025Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/component-none.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.85361 1.48959C7.65835 1.29432 7.34176 1.29432 7.1465 1.48959L1.48965 7.14644C1.29439 7.3417 1.29439 7.65829 1.48965 7.85355L3.9645 10.3284L1.64644 12.6464C1.45118 12.8417 1.45118 13.1583 1.64644 13.3536C1.84171 13.5488 2.15829 13.5488 2.35355 13.3536L4.6716 11.0355L7.1465 13.5104C7.34176 13.7057 7.65835 13.7057 7.85361 13.5104L13.5105 7.85355C13.7057 7.65829 13.7057 7.3417 13.5105 7.14644L11.0356 4.67154L13.3535 2.35355C13.5488 2.15829 13.5488 1.84171 13.3535 1.64645C13.1583 1.45118 12.8417 1.45118 12.6464 1.64645L10.3285 3.96443L7.85361 1.48959ZM9.62135 4.67154L7.50005 2.55025L2.55031 7.49999L4.6716 9.62129L9.62135 4.67154ZM5.37871 10.3284L7.50005 12.4497L12.4498 7.49999L10.3285 5.37865L5.37871 10.3284Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/container.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/cookie.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/copy.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 9.50006C1 10.3285 1.67157 11.0001 2.5 11.0001H4L4 10.0001H2.5C2.22386 10.0001 2 9.7762 2 9.50006L2 2.50006C2 2.22392 2.22386 2.00006 2.5 2.00006L9.5 2.00006C9.77614 2.00006 10 2.22392 10 2.50006V4.00002H5.5C4.67158 4.00002 4 4.67159 4 5.50002V12.5C4 13.3284 4.67158 14 5.5 14H12.5C13.3284 14 14 13.3284 14 12.5V5.50002C14 4.67159 13.3284 4.00002 12.5 4.00002H11V2.50006C11 1.67163 10.3284 1.00006 9.5 1.00006H2.5C1.67157 1.00006 1 1.67163 1 2.50006V9.50006ZM5 5.50002C5 5.22388 5.22386 5.00002 5.5 5.00002H12.5C12.7761 5.00002 13 5.22388 13 5.50002V12.5C13 12.7762 12.7761 13 12.5 13H5.5C5.22386 13 5 12.7762 5 12.5V5.50002Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/corner-bottom-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.87737 12H9.9H11.5C11.7761 12 12 11.7761 12 11.5C12 11.2239 11.7761 11 11.5 11H9.9C8.77164 11 7.95545 10.9996 7.31352 10.9472C6.67744 10.8952 6.25662 10.7946 5.91103 10.6185C5.25247 10.283 4.71703 9.74753 4.38148 9.08897C4.20539 8.74338 4.10481 8.32256 4.05284 7.68648C4.00039 7.04455 4 6.22836 4 5.1V3.5C4 3.22386 3.77614 3 3.5 3C3.22386 3 3 3.22386 3 3.5V5.1V5.12263C3 6.22359 3 7.08052 3.05616 7.76791C3.11318 8.46584 3.23058 9.0329 3.49047 9.54296C3.9219 10.3897 4.61031 11.0781 5.45704 11.5095C5.9671 11.7694 6.53416 11.8868 7.23209 11.9438C7.91948 12 8.77641 12 9.87737 12Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/corner-bottom-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.12263 12H5.1H3.5C3.22386 12 3 11.7761 3 11.5C3 11.2239 3.22386 11 3.5 11H5.1C6.22836 11 7.04455 10.9996 7.68648 10.9472C8.32256 10.8952 8.74338 10.7946 9.08897 10.6185C9.74753 10.283 10.283 9.74753 10.6185 9.08897C10.7946 8.74338 10.8952 8.32256 10.9472 7.68648C10.9996 7.04455 11 6.22836 11 5.1V3.5C11 3.22386 11.2239 3 11.5 3C11.7761 3 12 3.22386 12 3.5V5.1V5.12263C12 6.22359 12 7.08052 11.9438 7.76791C11.8868 8.46584 11.7694 9.0329 11.5095 9.54296C11.0781 10.3897 10.3897 11.0781 9.54296 11.5095C9.0329 11.7694 8.46584 11.8868 7.76791 11.9438C7.08052 12 6.22359 12 5.12263 12Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/corner-top-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.87737 3H9.9H11.5C11.7761 3 12 3.22386 12 3.5C12 3.77614 11.7761 4 11.5 4H9.9C8.77164 4 7.95545 4.00039 7.31352 4.05284C6.67744 4.10481 6.25662 4.20539 5.91103 4.38148C5.25247 4.71703 4.71703 5.25247 4.38148 5.91103C4.20539 6.25662 4.10481 6.67744 4.05284 7.31352C4.00039 7.95545 4 8.77164 4 9.9V11.5C4 11.7761 3.77614 12 3.5 12C3.22386 12 3 11.7761 3 11.5V9.9V9.87737C3 8.77641 3 7.91948 3.05616 7.23209C3.11318 6.53416 3.23058 5.9671 3.49047 5.45704C3.9219 4.61031 4.61031 3.9219 5.45704 3.49047C5.9671 3.23058 6.53416 3.11318 7.23209 3.05616C7.91948 3 8.77641 3 9.87737 3Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/corner-top-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.12263 3H5.1H3.5C3.22386 3 3 3.22386 3 3.5C3 3.77614 3.22386 4 3.5 4H5.1C6.22836 4 7.04455 4.00039 7.68648 4.05284C8.32256 4.10481 8.74338 4.20539 9.08897 4.38148C9.74753 4.71703 10.283 5.25247 10.6185 5.91103C10.7946 6.25662 10.8952 6.67744 10.9472 7.31352C10.9996 7.95545 11 8.77164 11 9.9V11.5C11 11.7761 11.2239 12 11.5 12C11.7761 12 12 11.7761 12 11.5V9.9V9.87737C12 8.77641 12 7.91948 11.9438 7.23209C11.8868 6.53416 11.7694 5.9671 11.5095 5.45704C11.0781 4.61031 10.3897 3.9219 9.54296 3.49047C9.0329 3.23058 8.46584 3.11318 7.76791 3.05616C7.08052 3 6.22359 3 5.12263 3Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/corners.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/countdown-timer.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.15 7.49998C13.15 4.66458 10.9402 1.84998 7.50002 1.84998C4.7217 1.84998 3.34851 3.90636 2.76336 4.99997H4.5C4.77614 4.99997 5 5.22383 5 5.49997C5 5.77611 4.77614 5.99997 4.5 5.99997H1.5C1.22386 5.99997 1 5.77611 1 5.49997V2.49997C1 2.22383 1.22386 1.99997 1.5 1.99997C1.77614 1.99997 2 2.22383 2 2.49997V4.31318C2.70453 3.07126 4.33406 0.849976 7.50002 0.849976C11.5628 0.849976 14.15 4.18537 14.15 7.49998C14.15 10.8146 11.5628 14.15 7.50002 14.15C5.55618 14.15 3.93778 13.3808 2.78548 12.2084C2.16852 11.5806 1.68668 10.839 1.35816 10.0407C1.25306 9.78536 1.37488 9.49315 1.63024 9.38806C1.8856 9.28296 2.17781 9.40478 2.2829 9.66014C2.56374 10.3425 2.97495 10.9745 3.4987 11.5074C4.47052 12.4963 5.83496 13.15 7.50002 13.15C10.9402 13.15 13.15 10.3354 13.15 7.49998ZM7 10V5.00001H8V10H7Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/counter-clockwise-clock.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.15 7.49998C13.15 4.66458 10.9402 1.84998 7.50002 1.84998C4.72167 1.84998 3.34849 3.9064 2.76335 5H4.5C4.77614 5 5 5.22386 5 5.5C5 5.77614 4.77614 6 4.5 6H1.5C1.22386 6 1 5.77614 1 5.5V2.5C1 2.22386 1.22386 2 1.5 2C1.77614 2 2 2.22386 2 2.5V4.31318C2.70453 3.07126 4.33406 0.849976 7.50002 0.849976C11.5628 0.849976 14.15 4.18537 14.15 7.49998C14.15 10.8146 11.5628 14.15 7.50002 14.15C5.55618 14.15 3.93778 13.3808 2.78548 12.2084C2.16852 11.5806 1.68668 10.839 1.35816 10.0407C1.25306 9.78536 1.37488 9.49315 1.63024 9.38806C1.8856 9.28296 2.17781 9.40478 2.2829 9.66014C2.56374 10.3425 2.97495 10.9745 3.4987 11.5074C4.47052 12.4963 5.83496 13.15 7.50002 13.15C10.9402 13.15 13.15 10.3354 13.15 7.49998ZM7.5 4.00001C7.77614 4.00001 8 4.22387 8 4.50001V7.29291L9.85355 9.14646C10.0488 9.34172 10.0488 9.65831 9.85355 9.85357C9.65829 10.0488 9.34171 10.0488 9.14645 9.85357L7.14645 7.85357C7.05268 7.7598 7 7.63262 7 7.50001V4.50001C7 4.22387 7.22386 4.00001 7.5 4.00001Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/crop.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.5 8.00684e-07C3.77614 7.88614e-07 4 0.223859 4 0.500001L4 3.00006L11.5 3.00006C11.7761 3.00006 12 3.22392 12 3.50006L12 11.0001L14.5 11C14.7761 11 15 11.2238 15 11.5C15 11.7761 14.7762 12 14.5 12L12 12.0001L12 14.5C12 14.7761 11.7761 15 11.5 15C11.2239 15 11 14.7761 11 14.5L11 12.0001L3.5 12.0001C3.22386 12.0001 3 11.7762 3 11.5001L3 4.00005L0.499989 4C0.223847 4 -6.10541e-06 3.77613 -5.02576e-07 3.49999C5.13006e-06 3.22385 0.223867 3 0.50001 3L3 3.00005L3 0.500001C3 0.223859 3.22386 8.12755e-07 3.5 8.00684e-07ZM4 4.00006L4 11.0001L11 11.0001L11 4.00006L4 4.00006Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/cross-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.8536 2.85355C13.0488 2.65829 13.0488 2.34171 12.8536 2.14645C12.6583 1.95118 12.3417 1.95118 12.1464 2.14645L7.5 6.79289L2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L6.79289 7.5L2.14645 12.1464C1.95118 12.3417 1.95118 12.6583 2.14645 12.8536C2.34171 13.0488 2.65829 13.0488 2.85355 12.8536L7.5 8.20711L12.1464 12.8536C12.3417 13.0488 12.6583 13.0488 12.8536 12.8536C13.0488 12.6583 13.0488 12.3417 12.8536 12.1464L8.20711 7.5L12.8536 2.85355Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/cross-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/cross-circled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704ZM9.85358 5.14644C10.0488 5.3417 10.0488 5.65829 9.85358 5.85355L8.20713 7.49999L9.85358 9.14644C10.0488 9.3417 10.0488 9.65829 9.85358 9.85355C9.65832 10.0488 9.34173 10.0488 9.14647 9.85355L7.50002 8.2071L5.85358 9.85355C5.65832 10.0488 5.34173 10.0488 5.14647 9.85355C4.95121 9.65829 4.95121 9.3417 5.14647 9.14644L6.79292 7.49999L5.14647 5.85355C4.95121 5.65829 4.95121 5.3417 5.14647 5.14644C5.34173 4.95118 5.65832 4.95118 5.85358 5.14644L7.50002 6.79289L9.14647 5.14644C9.34173 4.95118 9.65832 4.95118 9.85358 5.14644Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/crosshair-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.50207C0.877075 3.84319 3.84319 0.877075 7.50208 0.877075C11.1609 0.877075 14.1271 3.84319 14.1271 7.50207C14.1271 11.1609 11.1609 14.1271 7.50208 14.1271C3.84319 14.1271 0.877075 11.1609 0.877075 7.50207ZM1.84898 7.00003C2.0886 4.26639 4.26639 2.0886 7.00003 1.84898V4.50003C7.00003 4.77617 7.22388 5.00003 7.50003 5.00003C7.77617 5.00003 8.00003 4.77617 8.00003 4.50003V1.84862C10.7356 2.08643 12.9154 4.26502 13.1552 7.00003H10.5C10.2239 7.00003 10 7.22388 10 7.50003C10 7.77617 10.2239 8.00003 10.5 8.00003H13.1555C12.9176 10.7369 10.7369 12.9176 8.00003 13.1555V10.5C8.00003 10.2239 7.77617 10 7.50003 10C7.22388 10 7.00003 10.2239 7.00003 10.5V13.1552C4.26502 12.9154 2.08643 10.7356 1.84862 8.00003H4.50003C4.77617 8.00003 5.00003 7.77617 5.00003 7.50003C5.00003 7.22388 4.77617 7.00003 4.50003 7.00003H1.84898Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/crosshair-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 0C7.77614 0 8 0.223858 8 0.5V1.80687C10.6922 2.0935 12.8167 4.28012 13.0068 7H14.5C14.7761 7 15 7.22386 15 7.5C15 7.77614 14.7761 8 14.5 8H12.9888C12.7094 10.6244 10.6244 12.7094 8 12.9888V14.5C8 14.7761 7.77614 15 7.5 15C7.22386 15 7 14.7761 7 14.5V13.0068C4.28012 12.8167 2.0935 10.6922 1.80687 8H0.5C0.223858 8 0 7.77614 0 7.5C0 7.22386 0.223858 7 0.5 7H1.78886C1.98376 4.21166 4.21166 1.98376 7 1.78886V0.5C7 0.223858 7.22386 0 7.5 0ZM8 12.0322V9.5C8 9.22386 7.77614 9 7.5 9C7.22386 9 7 9.22386 7 9.5V12.054C4.80517 11.8689 3.04222 10.1668 2.76344 8H5.5C5.77614 8 6 7.77614 6 7.5C6 7.22386 5.77614 7 5.5 7H2.7417C2.93252 4.73662 4.73662 2.93252 7 2.7417V5.5C7 5.77614 7.22386 6 7.5 6C7.77614 6 8 5.77614 8 5.5V2.76344C10.1668 3.04222 11.8689 4.80517 12.054 7H9.5C9.22386 7 9 7.22386 9 7.5C9 7.77614 9.22386 8 9.5 8H12.0322C11.7621 10.0991 10.0991 11.7621 8 12.0322Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/cube.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.28856 0.796908C7.42258 0.734364 7.57742 0.734364 7.71144 0.796908L13.7114 3.59691C13.8875 3.67906 14 3.85574 14 4.05V10.95C14 11.1443 13.8875 11.3209 13.7114 11.4031L7.71144 14.2031C7.57742 14.2656 7.42258 14.2656 7.28856 14.2031L1.28856 11.4031C1.11252 11.3209 1 11.1443 1 10.95V4.05C1 3.85574 1.11252 3.67906 1.28856 3.59691L7.28856 0.796908ZM2 4.80578L7 6.93078V12.9649L2 10.6316V4.80578ZM8 12.9649L13 10.6316V4.80578L8 6.93078V12.9649ZM7.5 6.05672L12.2719 4.02866L7.5 1.80176L2.72809 4.02866L7.5 6.05672Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/cursor-arrow.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.29227 0.048984C3.47033 -0.032338 3.67946 -0.00228214 3.8274 0.125891L12.8587 7.95026C13.0134 8.08432 13.0708 8.29916 13.0035 8.49251C12.9362 8.68586 12.7578 8.81866 12.5533 8.82768L9.21887 8.97474L11.1504 13.2187C11.2648 13.47 11.1538 13.7664 10.9026 13.8808L8.75024 14.8613C8.499 14.9758 8.20255 14.8649 8.08802 14.6137L6.15339 10.3703L3.86279 12.7855C3.72196 12.934 3.50487 12.9817 3.31479 12.9059C3.1247 12.8301 3 12.6461 3 12.4414V0.503792C3 0.308048 3.11422 0.130306 3.29227 0.048984ZM4 1.59852V11.1877L5.93799 9.14425C6.05238 9.02363 6.21924 8.96776 6.38319 8.99516C6.54715 9.02256 6.68677 9.12965 6.75573 9.2809L8.79056 13.7441L10.0332 13.178L8.00195 8.71497C7.93313 8.56376 7.94391 8.38824 8.03072 8.24659C8.11753 8.10494 8.26903 8.01566 8.435 8.00834L11.2549 7.88397L4 1.59852Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/cursor-text.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/dash.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5 7.5C5 7.22386 5.22386 7 5.5 7H9.5C9.77614 7 10 7.22386 10 7.5C10 7.77614 9.77614 8 9.5 8H5.5C5.22386 8 5 7.77614 5 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dashboard.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/desktop-mute.svg πŸ”—

@@ -0,0 +1,4 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.73284 2H1.25C0.559643 2 0 2.55964 0 3.25V10.75C0 11.249 0.292407 11.6797 0.715228 11.8802C0.729901 11.8616 0.74533 11.8435 0.761518 11.8257L1.51545 11H1.25C1.11193 11 1 10.8881 1 10.75V3.25C1 3.11193 1.11193 3 1.25 3H8.8198L9.73284 2ZM13.5232 3L14.316 2.13518C14.7219 2.34168 15 2.76336 15 3.25V10.75C15 11.4404 14.4404 12 13.75 12H9.92659L10.1701 13.2986C10.2336 13.6371 9.97389 13.95 9.62951 13.95H5.37049C5.02612 13.95 4.76645 13.6371 4.82991 13.2986L5.02202 12.2741L6.18991 11H13.75C13.8881 11 14 10.8881 14 10.75V3.25C14 3.11193 13.8881 3 13.75 3H13.5232ZM5.98909 12H9.01091L9.20778 13.05H5.79222L5.98909 12Z" fill="black"/>
+<path d="M13 1L2 13" stroke="black" stroke-linecap="round"/>
+</svg>

assets/icons/radix/desktop.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 3.25C1 3.11193 1.11193 3 1.25 3H13.75C13.8881 3 14 3.11193 14 3.25V10.75C14 10.8881 13.8881 11 13.75 11H1.25C1.11193 11 1 10.8881 1 10.75V3.25ZM1.25 2C0.559643 2 0 2.55964 0 3.25V10.75C0 11.4404 0.559644 12 1.25 12H5.07341L4.82991 13.2986C4.76645 13.6371 5.02612 13.95 5.37049 13.95H9.62951C9.97389 13.95 10.2336 13.6371 10.1701 13.2986L9.92659 12H13.75C14.4404 12 15 11.4404 15 10.75V3.25C15 2.55964 14.4404 2 13.75 2H1.25ZM9.01091 12H5.98909L5.79222 13.05H9.20778L9.01091 12Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dimensions.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/disc.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.877075C3.84222 0.877075 0.877075 3.84222 0.877075 7.49991C0.877075 11.1576 3.84222 14.1227 7.49991 14.1227C11.1576 14.1227 14.1227 11.1576 14.1227 7.49991C14.1227 3.84222 11.1576 0.877075 7.49991 0.877075ZM1.82708 7.49991C1.82708 4.36689 4.36689 1.82707 7.49991 1.82707C10.6329 1.82707 13.1727 4.36689 13.1727 7.49991C13.1727 10.6329 10.6329 13.1727 7.49991 13.1727C4.36689 13.1727 1.82708 10.6329 1.82708 7.49991ZM8.37287 7.50006C8.37287 7.98196 7.98221 8.37263 7.5003 8.37263C7.01839 8.37263 6.62773 7.98196 6.62773 7.50006C6.62773 7.01815 7.01839 6.62748 7.5003 6.62748C7.98221 6.62748 8.37287 7.01815 8.37287 7.50006ZM9.32287 7.50006C9.32287 8.50664 8.50688 9.32263 7.5003 9.32263C6.49372 9.32263 5.67773 8.50664 5.67773 7.50006C5.67773 6.49348 6.49372 5.67748 7.5003 5.67748C8.50688 5.67748 9.32287 6.49348 9.32287 7.50006Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/discord-logo.svg πŸ”—

@@ -0,0 +1,13 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <g clip-path="url(#clip0_16200_18)">
+    <path
+      fill-rule="evenodd"
+      clip-rule="evenodd"

assets/icons/radix/divider-horizontal.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 7.5C2 7.22386 2.22386 7 2.5 7H12.5C12.7761 7 13 7.22386 13 7.5C13 7.77614 12.7761 8 12.5 8H2.5C2.22386 8 2 7.77614 2 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/divider-vertical.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 2C7.77614 2 8 2.22386 8 2.5L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 2.5C7 2.22386 7.22386 2 7.5 2Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dot-filled.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M9.875 7.5C9.875 8.81168 8.81168 9.875 7.5 9.875C6.18832 9.875 5.125 8.81168 5.125 7.5C5.125 6.18832 6.18832 5.125 7.5 5.125C8.81168 5.125 9.875 6.18832 9.875 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dot-solid.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M9.875 7.5C9.875 8.81168 8.81168 9.875 7.5 9.875C6.18832 9.875 5.125 8.81168 5.125 7.5C5.125 6.18832 6.18832 5.125 7.5 5.125C8.81168 5.125 9.875 6.18832 9.875 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dot.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 9.125C8.39746 9.125 9.125 8.39746 9.125 7.5C9.125 6.60254 8.39746 5.875 7.5 5.875C6.60254 5.875 5.875 6.60254 5.875 7.5C5.875 8.39746 6.60254 9.125 7.5 9.125ZM7.5 10.125C8.94975 10.125 10.125 8.94975 10.125 7.5C10.125 6.05025 8.94975 4.875 7.5 4.875C6.05025 4.875 4.875 6.05025 4.875 7.5C4.875 8.94975 6.05025 10.125 7.5 10.125Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dots-horizontal.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.625 7.5C3.625 8.12132 3.12132 8.625 2.5 8.625C1.87868 8.625 1.375 8.12132 1.375 7.5C1.375 6.87868 1.87868 6.375 2.5 6.375C3.12132 6.375 3.625 6.87868 3.625 7.5ZM8.625 7.5C8.625 8.12132 8.12132 8.625 7.5 8.625C6.87868 8.625 6.375 8.12132 6.375 7.5C6.375 6.87868 6.87868 6.375 7.5 6.375C8.12132 6.375 8.625 6.87868 8.625 7.5ZM12.5 8.625C13.1213 8.625 13.625 8.12132 13.625 7.5C13.625 6.87868 13.1213 6.375 12.5 6.375C11.8787 6.375 11.375 6.87868 11.375 7.5C11.375 8.12132 11.8787 8.625 12.5 8.625Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dots-vertical.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.625 2.5C8.625 3.12132 8.12132 3.625 7.5 3.625C6.87868 3.625 6.375 3.12132 6.375 2.5C6.375 1.87868 6.87868 1.375 7.5 1.375C8.12132 1.375 8.625 1.87868 8.625 2.5ZM8.625 7.5C8.625 8.12132 8.12132 8.625 7.5 8.625C6.87868 8.625 6.375 8.12132 6.375 7.5C6.375 6.87868 6.87868 6.375 7.5 6.375C8.12132 6.375 8.625 6.87868 8.625 7.5ZM7.5 13.625C8.12132 13.625 8.625 13.1213 8.625 12.5C8.625 11.8787 8.12132 11.375 7.5 11.375C6.87868 11.375 6.375 11.8787 6.375 12.5C6.375 13.1213 6.87868 13.625 7.5 13.625Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/double-arrow-down.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.85355 2.14645C3.65829 1.95118 3.34171 1.95118 3.14645 2.14645C2.95118 2.34171 2.95118 2.65829 3.14645 2.85355L7.14645 6.85355C7.34171 7.04882 7.65829 7.04882 7.85355 6.85355L11.8536 2.85355C12.0488 2.65829 12.0488 2.34171 11.8536 2.14645C11.6583 1.95118 11.3417 1.95118 11.1464 2.14645L7.5 5.79289L3.85355 2.14645ZM3.85355 8.14645C3.65829 7.95118 3.34171 7.95118 3.14645 8.14645C2.95118 8.34171 2.95118 8.65829 3.14645 8.85355L7.14645 12.8536C7.34171 13.0488 7.65829 13.0488 7.85355 12.8536L11.8536 8.85355C12.0488 8.65829 12.0488 8.34171 11.8536 8.14645C11.6583 7.95118 11.3417 7.95118 11.1464 8.14645L7.5 11.7929L3.85355 8.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/double-arrow-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.85355 3.85355C7.04882 3.65829 7.04882 3.34171 6.85355 3.14645C6.65829 2.95118 6.34171 2.95118 6.14645 3.14645L2.14645 7.14645C1.95118 7.34171 1.95118 7.65829 2.14645 7.85355L6.14645 11.8536C6.34171 12.0488 6.65829 12.0488 6.85355 11.8536C7.04882 11.6583 7.04882 11.3417 6.85355 11.1464L3.20711 7.5L6.85355 3.85355ZM12.8536 3.85355C13.0488 3.65829 13.0488 3.34171 12.8536 3.14645C12.6583 2.95118 12.3417 2.95118 12.1464 3.14645L8.14645 7.14645C7.95118 7.34171 7.95118 7.65829 8.14645 7.85355L12.1464 11.8536C12.3417 12.0488 12.6583 12.0488 12.8536 11.8536C13.0488 11.6583 13.0488 11.3417 12.8536 11.1464L9.20711 7.5L12.8536 3.85355Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/double-arrow-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.14645 11.1464C1.95118 11.3417 1.95118 11.6583 2.14645 11.8536C2.34171 12.0488 2.65829 12.0488 2.85355 11.8536L6.85355 7.85355C7.04882 7.65829 7.04882 7.34171 6.85355 7.14645L2.85355 3.14645C2.65829 2.95118 2.34171 2.95118 2.14645 3.14645C1.95118 3.34171 1.95118 3.65829 2.14645 3.85355L5.79289 7.5L2.14645 11.1464ZM8.14645 11.1464C7.95118 11.3417 7.95118 11.6583 8.14645 11.8536C8.34171 12.0488 8.65829 12.0488 8.85355 11.8536L12.8536 7.85355C13.0488 7.65829 13.0488 7.34171 12.8536 7.14645L8.85355 3.14645C8.65829 2.95118 8.34171 2.95118 8.14645 3.14645C7.95118 3.34171 7.95118 3.65829 8.14645 3.85355L11.7929 7.5L8.14645 11.1464Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/double-arrow-up.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.1464 6.85355C11.3417 7.04882 11.6583 7.04882 11.8536 6.85355C12.0488 6.65829 12.0488 6.34171 11.8536 6.14645L7.85355 2.14645C7.65829 1.95118 7.34171 1.95118 7.14645 2.14645L3.14645 6.14645C2.95118 6.34171 2.95118 6.65829 3.14645 6.85355C3.34171 7.04882 3.65829 7.04882 3.85355 6.85355L7.5 3.20711L11.1464 6.85355ZM11.1464 12.8536C11.3417 13.0488 11.6583 13.0488 11.8536 12.8536C12.0488 12.6583 12.0488 12.3417 11.8536 12.1464L7.85355 8.14645C7.65829 7.95118 7.34171 7.95118 7.14645 8.14645L3.14645 12.1464C2.95118 12.3417 2.95118 12.6583 3.14645 12.8536C3.34171 13.0488 3.65829 13.0488 3.85355 12.8536L7.5 9.20711L11.1464 12.8536Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/download.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.50005 1.04999C7.74858 1.04999 7.95005 1.25146 7.95005 1.49999V8.41359L10.1819 6.18179C10.3576 6.00605 10.6425 6.00605 10.8182 6.18179C10.994 6.35753 10.994 6.64245 10.8182 6.81819L7.81825 9.81819C7.64251 9.99392 7.35759 9.99392 7.18185 9.81819L4.18185 6.81819C4.00611 6.64245 4.00611 6.35753 4.18185 6.18179C4.35759 6.00605 4.64251 6.00605 4.81825 6.18179L7.05005 8.41359V1.49999C7.05005 1.25146 7.25152 1.04999 7.50005 1.04999ZM2.5 10C2.77614 10 3 10.2239 3 10.5V12C3 12.5539 3.44565 13 3.99635 13H11.0012C11.5529 13 12 12.5528 12 12V10.5C12 10.2239 12.2239 10 12.5 10C12.7761 10 13 10.2239 13 10.5V12C13 13.1041 12.1062 14 11.0012 14H3.99635C2.89019 14 2 13.103 2 12V10.5C2 10.2239 2.22386 10 2.5 10Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/drag-handle-dots-1.svg πŸ”—

@@ -0,0 +1,26 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="4.5" cy="2.5" r=".6" fill="currentColor" />
+  <circle cx="4.5" cy="4.5" r=".6" fill="currentColor" />
+  <circle cx="4.5" cy="6.499" r=".6" fill="currentColor" />
+  <circle cx="4.5" cy="8.499" r=".6" fill="currentColor" />
+  <circle cx="4.5" cy="10.498" r=".6" fill="currentColor" />
+  <circle cx="4.5" cy="12.498" r=".6" fill="currentColor" />
+  <circle cx="6.5" cy="2.5" r=".6" fill="currentColor" />
+  <circle cx="6.5" cy="4.5" r=".6" fill="currentColor" />
+  <circle cx="6.5" cy="6.499" r=".6" fill="currentColor" />
+  <circle cx="6.5" cy="8.499" r=".6" fill="currentColor" />
+  <circle cx="6.5" cy="10.498" r=".6" fill="currentColor" />
+  <circle cx="6.5" cy="12.498" r=".6" fill="currentColor" />
+  <circle cx="8.499" cy="2.5" r=".6" fill="currentColor" />
+  <circle cx="8.499" cy="4.5" r=".6" fill="currentColor" />
+  <circle cx="8.499" cy="6.499" r=".6" fill="currentColor" />
+  <circle cx="8.499" cy="8.499" r=".6" fill="currentColor" />
+  <circle cx="8.499" cy="10.498" r=".6" fill="currentColor" />
+  <circle cx="8.499" cy="12.498" r=".6" fill="currentColor" />
+  <circle cx="10.499" cy="2.5" r=".6" fill="currentColor" />
+  <circle cx="10.499" cy="4.5" r=".6" fill="currentColor" />
+  <circle cx="10.499" cy="6.499" r=".6" fill="currentColor" />
+  <circle cx="10.499" cy="8.499" r=".6" fill="currentColor" />
+  <circle cx="10.499" cy="10.498" r=".6" fill="currentColor" />
+  <circle cx="10.499" cy="12.498" r=".6" fill="currentColor" />
+</svg>

assets/icons/radix/drag-handle-horizontal.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.49998 4.09998C2.27906 4.09998 2.09998 4.27906 2.09998 4.49998C2.09998 4.72089 2.27906 4.89998 2.49998 4.89998H12.5C12.7209 4.89998 12.9 4.72089 12.9 4.49998C12.9 4.27906 12.7209 4.09998 12.5 4.09998H2.49998ZM2.49998 6.09998C2.27906 6.09998 2.09998 6.27906 2.09998 6.49998C2.09998 6.72089 2.27906 6.89998 2.49998 6.89998H12.5C12.7209 6.89998 12.9 6.72089 12.9 6.49998C12.9 6.27906 12.7209 6.09998 12.5 6.09998H2.49998ZM2.09998 8.49998C2.09998 8.27906 2.27906 8.09998 2.49998 8.09998H12.5C12.7209 8.09998 12.9 8.27906 12.9 8.49998C12.9 8.72089 12.7209 8.89998 12.5 8.89998H2.49998C2.27906 8.89998 2.09998 8.72089 2.09998 8.49998ZM2.49998 10.1C2.27906 10.1 2.09998 10.2791 2.09998 10.5C2.09998 10.7209 2.27906 10.9 2.49998 10.9H12.5C12.7209 10.9 12.9 10.7209 12.9 10.5C12.9 10.2791 12.7209 10.1 12.5 10.1H2.49998Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/drag-handle-vertical.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.09998 12.5C4.09998 12.7209 4.27906 12.9 4.49998 12.9C4.72089 12.9 4.89998 12.7209 4.89998 12.5L4.89998 2.50002C4.89998 2.27911 4.72089 2.10003 4.49998 2.10003C4.27906 2.10003 4.09998 2.27911 4.09998 2.50002L4.09998 12.5ZM6.09998 12.5C6.09998 12.7209 6.27906 12.9 6.49998 12.9C6.72089 12.9 6.89998 12.7209 6.89998 12.5L6.89998 2.50002C6.89998 2.27911 6.72089 2.10003 6.49998 2.10003C6.27906 2.10003 6.09998 2.27911 6.09998 2.50002L6.09998 12.5ZM8.49998 12.9C8.27906 12.9 8.09998 12.7209 8.09998 12.5L8.09998 2.50002C8.09998 2.27911 8.27906 2.10002 8.49998 2.10002C8.72089 2.10002 8.89998 2.27911 8.89998 2.50002L8.89998 12.5C8.89998 12.7209 8.72089 12.9 8.49998 12.9ZM10.1 12.5C10.1 12.7209 10.2791 12.9 10.5 12.9C10.7209 12.9 10.9 12.7209 10.9 12.5L10.9 2.50002C10.9 2.27911 10.7209 2.10002 10.5 2.10002C10.2791 2.10002 10.1 2.27911 10.1 2.50002L10.1 12.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/drawing-pin-filled.svg πŸ”—

@@ -0,0 +1,14 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.62129 1.13607C9.81656 0.940808 10.1331 0.940809 10.3284 1.13607L11.3891 2.19673L12.8033 3.61094L13.8639 4.6716C14.0592 4.86687 14.0592 5.18345 13.8639 5.37871C13.6687 5.57397 13.3521 5.57397 13.1568 5.37871L12.5038 4.7257L8.86727 9.57443L9.97485 10.682C10.1701 10.8773 10.1701 11.1939 9.97485 11.3891C9.77959 11.5844 9.463 11.5844 9.26774 11.3891L7.85353 9.97491L6.79287 8.91425L3.5225 12.1846C3.32724 12.3799 3.01065 12.3799 2.81539 12.1846C2.62013 11.9894 2.62013 11.6728 2.81539 11.4775L6.08576 8.20714L5.0251 7.14648L3.61089 5.73226C3.41563 5.537 3.41562 5.22042 3.61089 5.02516C3.80615 4.8299 4.12273 4.8299 4.31799 5.02516L5.42557 6.13274L10.2743 2.49619L9.62129 1.84318C9.42603 1.64792 9.42603 1.33133 9.62129 1.13607Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.62129 1.13607C9.81656 0.940808 10.1331 0.940809 10.3284 1.13607L11.3891 2.19673L12.8033 3.61094L13.8639 4.6716C14.0592 4.86687 14.0592 5.18345 13.8639 5.37871C13.6687 5.57397 13.3521 5.57397 13.1568 5.37871L12.5038 4.7257L8.86727 9.57443L9.97485 10.682C10.1701 10.8773 10.1701 11.1939 9.97485 11.3891C9.77959 11.5844 9.463 11.5844 9.26774 11.3891L7.85353 9.97491L6.79287 8.91425L3.5225 12.1846C3.32724 12.3799 3.01065 12.3799 2.81539 12.1846C2.62013 11.9894 2.62013 11.6728 2.81539 11.4775L6.08576 8.20714L5.0251 7.14648L3.61089 5.73226C3.41563 5.537 3.41562 5.22042 3.61089 5.02516C3.80615 4.8299 4.12273 4.8299 4.31799 5.02516L5.42557 6.13274L10.2743 2.49619L9.62129 1.84318C9.42603 1.64792 9.42603 1.33133 9.62129 1.13607Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/drawing-pin-solid.svg πŸ”—

@@ -0,0 +1,14 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.62129 1.13607C9.81656 0.940808 10.1331 0.940809 10.3284 1.13607L11.3891 2.19673L12.8033 3.61094L13.8639 4.6716C14.0592 4.86687 14.0592 5.18345 13.8639 5.37871C13.6687 5.57397 13.3521 5.57397 13.1568 5.37871L12.5038 4.7257L8.86727 9.57443L9.97485 10.682C10.1701 10.8773 10.1701 11.1939 9.97485 11.3891C9.77959 11.5844 9.463 11.5844 9.26774 11.3891L7.85353 9.97491L6.79287 8.91425L3.5225 12.1846C3.32724 12.3799 3.01065 12.3799 2.81539 12.1846C2.62013 11.9894 2.62013 11.6728 2.81539 11.4775L6.08576 8.20714L5.0251 7.14648L3.61089 5.73226C3.41563 5.537 3.41562 5.22042 3.61089 5.02516C3.80615 4.8299 4.12273 4.8299 4.31799 5.02516L5.42557 6.13274L10.2743 2.49619L9.62129 1.84318C9.42603 1.64792 9.42603 1.33133 9.62129 1.13607Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.62129 1.13607C9.81656 0.940808 10.1331 0.940809 10.3284 1.13607L11.3891 2.19673L12.8033 3.61094L13.8639 4.6716C14.0592 4.86687 14.0592 5.18345 13.8639 5.37871C13.6687 5.57397 13.3521 5.57397 13.1568 5.37871L12.5038 4.7257L8.86727 9.57443L9.97485 10.682C10.1701 10.8773 10.1701 11.1939 9.97485 11.3891C9.77959 11.5844 9.463 11.5844 9.26774 11.3891L7.85353 9.97491L6.79287 8.91425L3.5225 12.1846C3.32724 12.3799 3.01065 12.3799 2.81539 12.1846C2.62013 11.9894 2.62013 11.6728 2.81539 11.4775L6.08576 8.20714L5.0251 7.14648L3.61089 5.73226C3.41563 5.537 3.41562 5.22042 3.61089 5.02516C3.80615 4.8299 4.12273 4.8299 4.31799 5.02516L5.42557 6.13274L10.2743 2.49619L9.62129 1.84318C9.42603 1.64792 9.42603 1.33133 9.62129 1.13607Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/drawing-pin.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.3285 1.13607C10.1332 0.940809 9.81662 0.940808 9.62136 1.13607C9.42609 1.33133 9.42609 1.64792 9.62136 1.84318L10.2744 2.49619L5.42563 6.13274L4.31805 5.02516C4.12279 4.8299 3.80621 4.8299 3.61095 5.02516C3.41569 5.22042 3.41569 5.537 3.61095 5.73226L5.02516 7.14648L6.08582 8.20714L2.81545 11.4775C2.62019 11.6728 2.62019 11.9894 2.81545 12.1846C3.01072 12.3799 3.3273 12.3799 3.52256 12.1846L6.79293 8.91425L7.85359 9.97491L9.2678 11.3891C9.46306 11.5844 9.77965 11.5844 9.97491 11.3891C10.1702 11.1939 10.1702 10.8773 9.97491 10.682L8.86733 9.57443L12.5039 4.7257L13.1569 5.37871C13.3522 5.57397 13.6687 5.57397 13.864 5.37871C14.0593 5.18345 14.0593 4.86687 13.864 4.6716L12.8033 3.61094L11.3891 2.19673L10.3285 1.13607ZM6.13992 6.84702L10.9887 3.21047L11.7896 4.01142L8.15305 8.86015L6.13992 6.84702Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/dropdown-menu.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49999 3.09998C7.27907 3.09998 7.09999 3.27906 7.09999 3.49998C7.09999 3.72089 7.27907 3.89998 7.49999 3.89998H14.5C14.7209 3.89998 14.9 3.72089 14.9 3.49998C14.9 3.27906 14.7209 3.09998 14.5 3.09998H7.49999ZM7.49998 5.1C7.27907 5.1 7.09998 5.27908 7.09998 5.5C7.09998 5.72091 7.27907 5.9 7.49998 5.9H14.5C14.7209 5.9 14.9 5.72091 14.9 5.5C14.9 5.27908 14.7209 5.1 14.5 5.1H7.49998ZM7.1 7.5C7.1 7.27908 7.27909 7.1 7.5 7.1H14.5C14.7209 7.1 14.9 7.27908 14.9 7.5C14.9 7.72091 14.7209 7.9 14.5 7.9H7.5C7.27909 7.9 7.1 7.72091 7.1 7.5ZM7.49998 9.1C7.27907 9.1 7.09998 9.27908 7.09998 9.5C7.09998 9.72091 7.27907 9.9 7.49998 9.9H14.5C14.7209 9.9 14.9 9.72091 14.9 9.5C14.9 9.27908 14.7209 9.1 14.5 9.1H7.49998ZM7.09998 11.5C7.09998 11.2791 7.27907 11.1 7.49998 11.1H14.5C14.7209 11.1 14.9 11.2791 14.9 11.5C14.9 11.7209 14.7209 11.9 14.5 11.9H7.49998C7.27907 11.9 7.09998 11.7209 7.09998 11.5ZM2.5 9.25003L5 6.00003H0L2.5 9.25003Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/enter-full-screen.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 2.5C2 2.22386 2.22386 2 2.5 2H5.5C5.77614 2 6 2.22386 6 2.5C6 2.77614 5.77614 3 5.5 3H3V5.5C3 5.77614 2.77614 6 2.5 6C2.22386 6 2 5.77614 2 5.5V2.5ZM9 2.5C9 2.22386 9.22386 2 9.5 2H12.5C12.7761 2 13 2.22386 13 2.5V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3H9.5C9.22386 3 9 2.77614 9 2.5ZM2.5 9C2.77614 9 3 9.22386 3 9.5V12H5.5C5.77614 12 6 12.2239 6 12.5C6 12.7761 5.77614 13 5.5 13H2.5C2.22386 13 2 12.7761 2 12.5V9.5C2 9.22386 2.22386 9 2.5 9ZM12.5 9C12.7761 9 13 9.22386 13 9.5V12.5C13 12.7761 12.7761 13 12.5 13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H12V9.5C12 9.22386 12.2239 9 12.5 9Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/enter.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.5 1C4.22386 1 4 1.22386 4 1.5C4 1.77614 4.22386 2 4.5 2H12V13H4.5C4.22386 13 4 13.2239 4 13.5C4 13.7761 4.22386 14 4.5 14H12C12.5523 14 13 13.5523 13 13V2C13 1.44772 12.5523 1 12 1H4.5ZM6.60355 4.89645C6.40829 4.70118 6.09171 4.70118 5.89645 4.89645C5.70118 5.09171 5.70118 5.40829 5.89645 5.60355L7.29289 7H0.5C0.223858 7 0 7.22386 0 7.5C0 7.77614 0.223858 8 0.5 8H7.29289L5.89645 9.39645C5.70118 9.59171 5.70118 9.90829 5.89645 10.1036C6.09171 10.2988 6.40829 10.2988 6.60355 10.1036L8.85355 7.85355C9.04882 7.65829 9.04882 7.34171 8.85355 7.14645L6.60355 4.89645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/envelope-closed.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 2C0.447715 2 0 2.44772 0 3V12C0 12.5523 0.447715 13 1 13H14C14.5523 13 15 12.5523 15 12V3C15 2.44772 14.5523 2 14 2H1ZM1 3L14 3V3.92494C13.9174 3.92486 13.8338 3.94751 13.7589 3.99505L7.5 7.96703L1.24112 3.99505C1.16621 3.94751 1.0826 3.92486 1 3.92494V3ZM1 4.90797V12H14V4.90797L7.74112 8.87995C7.59394 8.97335 7.40606 8.97335 7.25888 8.87995L1 4.90797Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/envelope-open.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.94721 0.164594C7.66569 0.0238299 7.33431 0.0238302 7.05279 0.164594L0.552786 3.41459C0.214002 3.58399 0 3.93025 0 4.30902V12C0 12.5523 0.447715 13 1 13H14C14.5523 13 15 12.5523 15 12V4.30902C15 3.93025 14.786 3.58399 14.4472 3.41459L7.94721 0.164594ZM13.5689 4.09349L7.5 1.05902L1.43105 4.09349L7.5 7.29136L13.5689 4.09349ZM1 4.88366V12H14V4.88366L7.70977 8.19813C7.57848 8.26731 7.42152 8.26731 7.29023 8.19813L1 4.88366Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/eraser.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.36052 0.72921C8.55578 0.533948 8.87236 0.533948 9.06763 0.72921L14.2708 5.93235C14.466 6.12761 14.466 6.4442 14.2708 6.63946L8.95513 11.9551L7.3466 13.5636C6.76081 14.1494 5.81106 14.1494 5.22528 13.5636L1.43635 9.7747C0.850563 9.18891 0.850563 8.23917 1.43635 7.65338L3.04488 6.04485L8.36052 0.72921ZM8.71407 1.78987L4.10554 6.3984L8.60157 10.8944L13.2101 6.28591L8.71407 1.78987ZM7.89447 11.6015L3.39843 7.10551L2.14346 8.36049C1.94819 8.55575 1.94819 8.87233 2.14346 9.06759L5.93238 12.8565C6.12765 13.0518 6.44423 13.0518 6.63949 12.8565L7.89447 11.6015Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/exclamation-triangle.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.4449 0.608765C8.0183 -0.107015 6.9817 -0.107015 6.55509 0.608766L0.161178 11.3368C-0.275824 12.07 0.252503 13 1.10608 13H13.8939C14.7475 13 15.2758 12.07 14.8388 11.3368L8.4449 0.608765ZM7.4141 1.12073C7.45288 1.05566 7.54712 1.05566 7.5859 1.12073L13.9798 11.8488C14.0196 11.9154 13.9715 12 13.8939 12H1.10608C1.02849 12 0.980454 11.9154 1.02018 11.8488L7.4141 1.12073ZM6.8269 4.48611C6.81221 4.10423 7.11783 3.78663 7.5 3.78663C7.88217 3.78663 8.18778 4.10423 8.1731 4.48612L8.01921 8.48701C8.00848 8.766 7.7792 8.98664 7.5 8.98664C7.2208 8.98664 6.99151 8.766 6.98078 8.48701L6.8269 4.48611ZM8.24989 10.476C8.24989 10.8902 7.9141 11.226 7.49989 11.226C7.08567 11.226 6.74989 10.8902 6.74989 10.476C6.74989 10.0618 7.08567 9.72599 7.49989 9.72599C7.9141 9.72599 8.24989 10.0618 8.24989 10.476Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/exit-full-screen.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.5 2C5.77614 2 6 2.22386 6 2.5V5.5C6 5.77614 5.77614 6 5.5 6H2.5C2.22386 6 2 5.77614 2 5.5C2 5.22386 2.22386 5 2.5 5H5V2.5C5 2.22386 5.22386 2 5.5 2ZM9.5 2C9.77614 2 10 2.22386 10 2.5V5H12.5C12.7761 5 13 5.22386 13 5.5C13 5.77614 12.7761 6 12.5 6H9.5C9.22386 6 9 5.77614 9 5.5V2.5C9 2.22386 9.22386 2 9.5 2ZM2 9.5C2 9.22386 2.22386 9 2.5 9H5.5C5.77614 9 6 9.22386 6 9.5V12.5C6 12.7761 5.77614 13 5.5 13C5.22386 13 5 12.7761 5 12.5V10H2.5C2.22386 10 2 9.77614 2 9.5ZM9 9.5C9 9.22386 9.22386 9 9.5 9H12.5C12.7761 9 13 9.22386 13 9.5C13 9.77614 12.7761 10 12.5 10H10V12.5C10 12.7761 9.77614 13 9.5 13C9.22386 13 9 12.7761 9 12.5V9.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/exit.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 1C2.44771 1 2 1.44772 2 2V13C2 13.5523 2.44772 14 3 14H10.5C10.7761 14 11 13.7761 11 13.5C11 13.2239 10.7761 13 10.5 13H3V2L10.5 2C10.7761 2 11 1.77614 11 1.5C11 1.22386 10.7761 1 10.5 1H3ZM12.6036 4.89645C12.4083 4.70118 12.0917 4.70118 11.8964 4.89645C11.7012 5.09171 11.7012 5.40829 11.8964 5.60355L13.2929 7H6.5C6.22386 7 6 7.22386 6 7.5C6 7.77614 6.22386 8 6.5 8H13.2929L11.8964 9.39645C11.7012 9.59171 11.7012 9.90829 11.8964 10.1036C12.0917 10.2988 12.4083 10.2988 12.6036 10.1036L14.8536 7.85355C15.0488 7.65829 15.0488 7.34171 14.8536 7.14645L12.6036 4.89645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/external-link.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/eye-closed.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/eye-none.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.3536 2.35355C13.5488 2.15829 13.5488 1.84171 13.3536 1.64645C13.1583 1.45118 12.8417 1.45118 12.6464 1.64645L10.6828 3.61012C9.70652 3.21671 8.63759 3 7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C0.902945 9.08812 2.02314 10.1861 3.36061 10.9323L1.64645 12.6464C1.45118 12.8417 1.45118 13.1583 1.64645 13.3536C1.84171 13.5488 2.15829 13.5488 2.35355 13.3536L4.31723 11.3899C5.29348 11.7833 6.36241 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C14.0971 5.9119 12.9769 4.81391 11.6394 4.06771L13.3536 2.35355ZM9.90428 4.38861C9.15332 4.1361 8.34759 4 7.5 4C4.80285 4 2.52952 5.37816 1.09622 7.50001C1.87284 8.6497 2.89609 9.58106 4.09974 10.1931L9.90428 4.38861ZM5.09572 10.6114L10.9003 4.80685C12.1039 5.41894 13.1272 6.35031 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11C6.65241 11 5.84668 10.8639 5.09572 10.6114Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/eye-open.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/face.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/figma-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/file-minus.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.29289L12 4.70711V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.60355C13 4.40464 12.921 4.21388 12.7803 4.07322L9.85355 1.14645C9.75979 1.05268 9.63261 1 9.5 1H3.5ZM5.25 7C4.97386 7 4.75 7.22386 4.75 7.5C4.75 7.77614 4.97386 8 5.25 8H9.75C10.0261 8 10.25 7.77614 10.25 7.5C10.25 7.22386 10.0261 7 9.75 7H5.25Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/file-plus.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V4.70711L9.29289 2H3.5ZM2 2.5C2 1.67157 2.67157 1 3.5 1H9.5C9.63261 1 9.75979 1.05268 9.85355 1.14645L12.7803 4.07322C12.921 4.21388 13 4.40464 13 4.60355V12.5C13 13.3284 12.3284 14 11.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5ZM4.75 7.5C4.75 7.22386 4.97386 7 5.25 7H7V5.25C7 4.97386 7.22386 4.75 7.5 4.75C7.77614 4.75 8 4.97386 8 5.25V7H9.75C10.0261 7 10.25 7.22386 10.25 7.5C10.25 7.77614 10.0261 8 9.75 8H8V9.75C8 10.0261 7.77614 10.25 7.5 10.25C7.22386 10.25 7 10.0261 7 9.75V8H5.25C4.97386 8 4.75 7.77614 4.75 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/file-text.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/file.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V6H8.5C8.22386 6 8 5.77614 8 5.5V2H3.5ZM9 2.70711L11.2929 5H9V2.70711ZM2 2.5C2 1.67157 2.67157 1 3.5 1H8.5C8.63261 1 8.75979 1.05268 8.85355 1.14645L12.8536 5.14645C12.9473 5.24021 13 5.36739 13 5.5V12.5C13 13.3284 12.3284 14 11.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/font-bold.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M5.10505 12C4.70805 12 4.4236 11.912 4.25171 11.736C4.0839 11.5559 4 11.2715 4 10.8827V4.11733C4 3.72033 4.08595 3.43588 4.25784 3.26398C4.43383 3.08799 4.71623 3 5.10505 3C6.42741 3 8.25591 3 9.02852 3C10.1373 3 11.0539 3.98153 11.0539 5.1846C11.0539 6.08501 10.6037 6.81855 9.70327 7.23602C10.8657 7.44851 11.5176 8.62787 11.5176 9.48128C11.5176 10.5125 10.9902 12 9.27734 12C8.77742 12 6.42626 12 5.10505 12ZM8.37891 8.00341H5.8V10.631H8.37891C8.9 10.631 9.6296 10.1211 9.6296 9.29877C9.6296 8.47643 8.9 8.00341 8.37891 8.00341ZM5.8 4.36903V6.69577H8.17969C8.53906 6.69577 9.27734 6.35939 9.27734 5.50002C9.27734 4.64064 8.48047 4.36903 8.17969 4.36903H5.8Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/font-family.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M2.5 4.5C2.5 3.09886 3.59886 2 5 2H12.499C12.7752 2 13 2.22386 13 2.5C13 2.77614 12.7761 3 12.5 3H8.69244L8.40509 3.85458C8.18869 4.49752 7.89401 5.37197 7.58091 6.29794C7.50259 6.52956 7.42308 6.76453 7.34332 7H8.5C8.77614 7 9 7.22386 9 7.5C9 7.77614 8.77614 8 8.5 8H7.00407C6.56724 9.28543 6.16435 10.4613 5.95799 11.0386C5.63627 11.9386 5.20712 12.4857 4.66741 12.7778C4.16335 13.0507 3.64154 13.0503 3.28378 13.05L3.25 13.05C2.94624 13.05 2.7 12.8037 2.7 12.5C2.7 12.1962 2.94624 11.95 3.25 11.95C3.64182 11.95 3.9035 11.9405 4.14374 11.8105C4.36443 11.691 4.65532 11.4148 4.92217 10.6683C5.10695 10.1514 5.45375 9.14134 5.8422 8H4.5C4.22386 8 4 7.77614 4 7.5C4 7.22386 4.22386 7 4.5 7H6.18187C6.30127 6.64785 6.42132 6.29323 6.53887 5.94559C6.85175 5.02025 7.14627 4.14631 7.36256 3.50368L7.53192 3H5C4.15114 3 3.5 3.65114 3.5 4.5C3.5 4.77614 3.27614 5 3 5C2.72386 5 2.5 4.77614 2.5 4.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/font-italic.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.67494 3.50017C5.67494 3.25164 5.87641 3.05017 6.12494 3.05017H10.6249C10.8735 3.05017 11.0749 3.25164 11.0749 3.50017C11.0749 3.7487 10.8735 3.95017 10.6249 3.95017H9.00587L7.2309 11.05H8.87493C9.12345 11.05 9.32493 11.2515 9.32493 11.5C9.32493 11.7486 9.12345 11.95 8.87493 11.95H4.37493C4.1264 11.95 3.92493 11.7486 3.92493 11.5C3.92493 11.2515 4.1264 11.05 4.37493 11.05H5.99397L7.76894 3.95017H6.12494C5.87641 3.95017 5.67494 3.7487 5.67494 3.50017Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/font-roman.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.79993 3.50017C4.79993 3.25164 5.0014 3.05017 5.24993 3.05017H9.74993C9.99845 3.05017 10.1999 3.25164 10.1999 3.50017C10.1999 3.7487 9.99845 3.95017 9.74993 3.95017H8.09993V11.05H9.74994C9.99847 11.05 10.1999 11.2515 10.1999 11.5C10.1999 11.7486 9.99847 11.95 9.74994 11.95H5.24994C5.00141 11.95 4.79994 11.7486 4.79994 11.5C4.79994 11.2515 5.00141 11.05 5.24994 11.05H6.89993V3.95017H5.24993C5.0014 3.95017 4.79993 3.7487 4.79993 3.50017Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/font-size.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/font-style.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/frame.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11 1.5C11 1.22386 10.7761 1 10.5 1C10.2239 1 10 1.22386 10 1.5V4H5V1.5C5 1.22386 4.77614 1 4.5 1C4.22386 1 4 1.22386 4 1.5V4H1.5C1.22386 4 1 4.22386 1 4.5C1 4.77614 1.22386 5 1.5 5H4V10H1.5C1.22386 10 1 10.2239 1 10.5C1 10.7761 1.22386 11 1.5 11H4V13.5C4 13.7761 4.22386 14 4.5 14C4.77614 14 5 13.7761 5 13.5V11H10V13.5C10 13.7761 10.2239 14 10.5 14C10.7761 14 11 13.7761 11 13.5V11H13.5C13.7761 11 14 10.7761 14 10.5C14 10.2239 13.7761 10 13.5 10H11V5H13.5C13.7761 5 14 4.77614 14 4.5C14 4.22386 13.7761 4 13.5 4H11V1.5ZM10 10V5H5V10H10Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/framer-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.3825 1.29567C3.46241 1.11432 3.64188 0.997284 3.84005 0.997284H11.5C11.7761 0.997284 12 1.22114 12 1.49728V5.5C12 5.77614 11.7761 6 11.5 6H8.63521L11.5288 9.16247C11.6626 9.3087 11.6974 9.52015 11.6175 9.70154C11.5376 9.88293 11.3582 10 11.16 10H8V13.5C8 13.7022 7.87818 13.8845 7.69134 13.9619C7.5045 14.0393 7.28945 13.9966 7.14645 13.8536L3.14645 9.85355C3.05268 9.75979 3 9.63261 3 9.5V5.5C3 5.22386 3.22386 5 3.5 5H6.36531L3.47105 1.83468C3.33732 1.68844 3.30259 1.47701 3.3825 1.29567ZM7.72032 5L4.97474 1.99728H11V5H7.72032ZM7.27978 6H4V9H7.5H10.0247L7.27978 6ZM4.70711 10L7 12.2929V10H4.70711Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/gear.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/github-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/globe.svg πŸ”—

@@ -0,0 +1,26 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49996 1.80002C4.35194 1.80002 1.79996 4.352 1.79996 7.50002C1.79996 10.648 4.35194 13.2 7.49996 13.2C10.648 13.2 13.2 10.648 13.2 7.50002C13.2 4.352 10.648 1.80002 7.49996 1.80002ZM0.899963 7.50002C0.899963 3.85494 3.85488 0.900024 7.49996 0.900024C11.145 0.900024 14.1 3.85494 14.1 7.50002C14.1 11.1451 11.145 14.1 7.49996 14.1C3.85488 14.1 0.899963 11.1451 0.899963 7.50002Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.4999 7.89998H1.49994V7.09998H13.4999V7.89998Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.09991 13.5V1.5H7.89991V13.5H7.09991zM10.375 7.49998C10.375 5.32724 9.59364 3.17778 8.06183 1.75656L8.53793 1.24341C10.2396 2.82218 11.075 5.17273 11.075 7.49998 11.075 9.82724 10.2396 12.1778 8.53793 13.7566L8.06183 13.2434C9.59364 11.8222 10.375 9.67273 10.375 7.49998zM3.99969 7.5C3.99969 5.17611 4.80786 2.82678 6.45768 1.24719L6.94177 1.75281C5.4582 3.17323 4.69969 5.32389 4.69969 7.5 4.6997 9.67611 5.45822 11.8268 6.94179 13.2472L6.45769 13.7528C4.80788 12.1732 3.9997 9.8239 3.99969 7.5z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49996 3.95801C9.66928 3.95801 11.8753 4.35915 13.3706 5.19448 13.5394 5.28875 13.5998 5.50197 13.5055 5.67073 13.4113 5.83948 13.198 5.89987 13.0293 5.8056 11.6794 5.05155 9.60799 4.65801 7.49996 4.65801 5.39192 4.65801 3.32052 5.05155 1.97064 5.8056 1.80188 5.89987 1.58866 5.83948 1.49439 5.67073 1.40013 5.50197 1.46051 5.28875 1.62927 5.19448 3.12466 4.35915 5.33063 3.95801 7.49996 3.95801zM7.49996 10.85C9.66928 10.85 11.8753 10.4488 13.3706 9.6135 13.5394 9.51924 13.5998 9.30601 13.5055 9.13726 13.4113 8.9685 13.198 8.90812 13.0293 9.00238 11.6794 9.75643 9.60799 10.15 7.49996 10.15 5.39192 10.15 3.32052 9.75643 1.97064 9.00239 1.80188 8.90812 1.58866 8.9685 1.49439 9.13726 1.40013 9.30601 1.46051 9.51924 1.62927 9.6135 3.12466 10.4488 5.33063 10.85 7.49996 10.85z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/grid.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.5 2H8V7H13V2.5C13 2.22386 12.7761 2 12.5 2ZM13 8H8V13H12.5C12.7761 13 13 12.7761 13 12.5V8ZM7 7V2H2.5C2.22386 2 2 2.22386 2 2.5V7H7ZM2 8V12.5C2 12.7761 2.22386 13 2.5 13H7V8H2ZM2.5 1C1.67157 1 1 1.67157 1 2.5V12.5C1 13.3284 1.67157 14 2.5 14H12.5C13.3284 14 14 13.3284 14 12.5V2.5C14 1.67157 13.3284 1 12.5 1H2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/group.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/half-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM7.00003 1.84861C4.10114 2.1017 1.82707 4.53515 1.82707 7.49972C1.82707 10.4643 4.10114 12.8977 7.00003 13.1508V1.84861ZM8.00003 13.1508C10.8988 12.8976 13.1727 10.4642 13.1727 7.49972C13.1727 4.53524 10.8988 2.10185 8.00003 1.84864V13.1508Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/half-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM7.49988 1.82689C4.36688 1.8269 1.82707 4.36672 1.82707 7.49972C1.82707 10.6327 4.36688 13.1725 7.49988 13.1726V1.82689Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/hamburger-menu.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/hand.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/heading.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.75432 2.0502C8.50579 2.0502 8.30432 2.25167 8.30432 2.5002C8.30432 2.74873 8.50579 2.9502 8.75432 2.9502H9.94997V7.05004H5.04997V2.9502H6.25432C6.50285 2.9502 6.70432 2.74873 6.70432 2.5002C6.70432 2.25167 6.50285 2.0502 6.25432 2.0502H2.75432C2.50579 2.0502 2.30432 2.25167 2.30432 2.5002C2.30432 2.74873 2.50579 2.9502 2.75432 2.9502H3.94997V12.0502H2.75432C2.50579 12.0502 2.30432 12.2517 2.30432 12.5002C2.30432 12.7487 2.50579 12.9502 2.75432 12.9502H6.25432C6.50285 12.9502 6.70432 12.7487 6.70432 12.5002C6.70432 12.2517 6.50285 12.0502 6.25432 12.0502H5.04997V7.95004H9.94997V12.0502H8.75432C8.50579 12.0502 8.30432 12.2517 8.30432 12.5002C8.30432 12.7487 8.50579 12.9502 8.75432 12.9502H12.2543C12.5028 12.9502 12.7043 12.7487 12.7043 12.5002C12.7043 12.2517 12.5028 12.0502 12.2543 12.0502H11.05V2.9502H12.2543C12.5028 2.9502 12.7043 2.74873 12.7043 2.5002C12.7043 2.25167 12.5028 2.0502 12.2543 2.0502H8.75432Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/heart-filled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.35248 4.90532C1.35248 2.94498 2.936 1.35248 4.89346 1.35248C6.25769 1.35248 6.86058 1.92336 7.50002 2.93545C8.13946 1.92336 8.74235 1.35248 10.1066 1.35248C12.064 1.35248 13.6476 2.94498 13.6476 4.90532C13.6476 6.74041 12.6013 8.50508 11.4008 9.96927C10.2636 11.3562 8.92194 12.5508 8.00601 13.3664C7.94645 13.4194 7.88869 13.4709 7.83291 13.5206C7.64324 13.6899 7.3568 13.6899 7.16713 13.5206C7.11135 13.4709 7.05359 13.4194 6.99403 13.3664C6.0781 12.5508 4.73641 11.3562 3.59926 9.96927C2.39872 8.50508 1.35248 6.74041 1.35248 4.90532Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/heart.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/height.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.1813 1.68179C7.35704 1.50605 7.64196 1.50605 7.8177 1.68179L10.3177 4.18179C10.4934 4.35753 10.4934 4.64245 10.3177 4.81819C10.142 4.99392 9.85704 4.99392 9.6813 4.81819L7.9495 3.08638L7.9495 11.9136L9.6813 10.1818C9.85704 10.0061 10.142 10.0061 10.3177 10.1818C10.4934 10.3575 10.4934 10.6424 10.3177 10.8182L7.8177 13.3182C7.73331 13.4026 7.61885 13.45 7.4995 13.45C7.38015 13.45 7.26569 13.4026 7.1813 13.3182L4.6813 10.8182C4.50557 10.6424 4.50557 10.3575 4.6813 10.1818C4.85704 10.0061 5.14196 10.0061 5.3177 10.1818L7.0495 11.9136L7.0495 3.08638L5.3177 4.81819C5.14196 4.99392 4.85704 4.99392 4.6813 4.81819C4.50557 4.64245 4.50557 4.35753 4.6813 4.18179L7.1813 1.68179Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/hobby-knife.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.3536 13.3536C12.1583 13.5488 11.8417 13.5488 11.6465 13.3536L6.39645 8.10355C6.36478 8.07188 6.33824 8.03702 6.31685 8H5.00002C4.78719 8 4.59769 7.86528 4.52777 7.66426L2.12777 0.764277C2.05268 0.548387 2.13355 0.309061 2.3242 0.182972C2.51486 0.0568819 2.76674 0.0761337 2.93602 0.229734L8.336 5.12972C8.44044 5.22449 8.50001 5.35897 8.50001 5.5V5.81684C8.53702 5.83824 8.57189 5.86478 8.60356 5.89645L13.8536 11.1464C14.0488 11.3417 14.0488 11.6583 13.8536 11.8536L12.3536 13.3536ZM8.25 6.95711L7.45711 7.75L12 12.2929L12.7929 11.5L8.25 6.95711ZM3.71669 2.28845L5.35549 7H6.2929L7.50001 5.79289V5.72146L3.71669 2.28845Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/home.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.07926 0.222253C7.31275 -0.007434 7.6873 -0.007434 7.92079 0.222253L14.6708 6.86227C14.907 7.09465 14.9101 7.47453 14.6778 7.71076C14.4454 7.947 14.0655 7.95012 13.8293 7.71773L13 6.90201V12.5C13 12.7761 12.7762 13 12.5 13H2.50002C2.22388 13 2.00002 12.7761 2.00002 12.5V6.90201L1.17079 7.71773C0.934558 7.95012 0.554672 7.947 0.32229 7.71076C0.0899079 7.47453 0.0930283 7.09465 0.32926 6.86227L7.07926 0.222253ZM7.50002 1.49163L12 5.91831V12H10V8.49999C10 8.22385 9.77617 7.99999 9.50002 7.99999H6.50002C6.22388 7.99999 6.00002 8.22385 6.00002 8.49999V12H3.00002V5.91831L7.50002 1.49163ZM7.00002 12H9.00002V8.99999H7.00002V12Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/id-card.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14 11.0001V4.00006L1 4.00006L1 11.0001H14ZM15 4.00006V11.0001C15 11.5523 14.5523 12.0001 14 12.0001H1C0.447715 12.0001 0 11.5523 0 11.0001V4.00006C0 3.44778 0.447715 3.00006 1 3.00006H14C14.5523 3.00006 15 3.44778 15 4.00006ZM2 5.25C2 5.11193 2.11193 5 2.25 5H5.75C5.88807 5 6 5.11193 6 5.25V9.75C6 9.88807 5.88807 10 5.75 10H2.25C2.11193 10 2 9.88807 2 9.75V5.25ZM7.5 7C7.22386 7 7 7.22386 7 7.5C7 7.77614 7.22386 8 7.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H7.5ZM7 9.5C7 9.22386 7.22386 9 7.5 9H12.5C12.7761 9 13 9.22386 13 9.5C13 9.77614 12.7761 10 12.5 10H7.5C7.22386 10 7 9.77614 7 9.5ZM7.5 5C7.22386 5 7 5.22386 7 5.5C7 5.77614 7.22386 6 7.5 6H11.5C11.7761 6 12 5.77614 12 5.5C12 5.22386 11.7761 5 11.5 5H7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/image.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5C1 1.67157 1.67157 1 2.5 1ZM2.5 2C2.22386 2 2 2.22386 2 2.5V8.3636L3.6818 6.6818C3.76809 6.59551 3.88572 6.54797 4.00774 6.55007C4.12975 6.55216 4.24568 6.60372 4.32895 6.69293L7.87355 10.4901L10.6818 7.6818C10.8575 7.50607 11.1425 7.50607 11.3182 7.6818L13 9.3636V2.5C13 2.22386 12.7761 2 12.5 2H2.5ZM2 12.5V9.6364L3.98887 7.64753L7.5311 11.4421L8.94113 13H2.5C2.22386 13 2 12.7761 2 12.5ZM12.5 13H10.155L8.48336 11.153L11 8.6364L13 10.6364V12.5C13 12.7761 12.7761 13 12.5 13ZM6.64922 5.5C6.64922 5.03013 7.03013 4.64922 7.5 4.64922C7.96987 4.64922 8.35078 5.03013 8.35078 5.5C8.35078 5.96987 7.96987 6.35078 7.5 6.35078C7.03013 6.35078 6.64922 5.96987 6.64922 5.5ZM7.5 3.74922C6.53307 3.74922 5.74922 4.53307 5.74922 5.5C5.74922 6.46693 6.53307 7.25078 7.5 7.25078C8.46693 7.25078 9.25078 6.46693 9.25078 5.5C9.25078 4.53307 8.46693 3.74922 7.5 3.74922Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/info-circled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/inner-shadow.svg πŸ”—

@@ -0,0 +1,78 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".05"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.1619 3.85182C8.35817 4.88918 4.88936 8.358 3.85199 12.1617L3.3696 12.0301C4.45356 8.05564 8.05581 4.45339 12.0303 3.36943L12.1619 3.85182Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".1"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.8807 3.42707C8.03441 4.50542 4.50561 8.03422 3.42726 11.8805L2.94582 11.7456C4.07129 7.73121 7.7314 4.0711 11.7458 2.94563L11.8807 3.42707Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".15"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.5201 3.02556C7.69092 4.16199 4.16779 7.68323 3.02805 11.512L2.54883 11.3694C3.73676 7.37869 7.38659 3.73076 11.3778 2.54623L11.5201 3.02556Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".2"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.0468 2.66169C7.31117 3.87664 3.87918 7.3079 2.66298 11.0434L2.18754 10.8886C3.45324 7.00109 7.00445 3.45062 10.8921 2.18621L11.0468 2.66169Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".25"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.5201 2.32365C6.92091 3.61447 3.62391 6.90876 2.32845 10.5073L1.858 10.338C3.20398 6.59909 6.61155 3.19424 10.3513 1.85301L10.5201 2.32365Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".3"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.90222 2.03122C6.50003 3.39465 3.39968 6.49367 2.03399 9.89551L1.56998 9.70924C2.98651 6.18076 6.18728 2.98133 9.71622 1.5671L9.90222 2.03122Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".35"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.20727 1.78873C6.06136 3.20349 3.21103 6.05203 1.79331 9.19738L1.33747 8.99192C2.80536 5.73528 5.74485 2.7976 9.0022 1.33272L9.20727 1.78873Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".4"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.40713 1.62085C5.59323 3.05117 3.05794 5.58509 1.62544 8.39847L1.17987 8.1716C2.66036 5.26397 5.27232 2.6534 8.18057 1.17513L8.40713 1.62085Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".45"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.46207 1.56747C5.08689 2.94695 2.95362 5.07912 1.57249 7.45379L1.14028 7.20241C2.56503 4.75273 4.7607 2.55818 7.21096 1.1351L7.46207 1.56747Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".5"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.30407 1.70487C4.51964 2.91063 2.90983 4.52061 1.7043 6.30513L1.28998 6.02524C2.5313 4.18773 4.18673 2.53214 6.02413 1.29059L6.30407 1.70487Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/input.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/justify-center.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.94998 5.99994L11 5.99994L11 8.99994L7.94998 8.99994L7.94998 5.99994ZM7.94998 4.99994L7.94998 1.49913C7.94998 1.25061 7.74851 1.04913 7.49998 1.04913C7.25145 1.04913 7.04998 1.2506 7.04998 1.49913L7.04998 4.99994L3.75 4.99994C3.33579 4.99994 3 5.33572 3 5.74994L3 9.24994C3 9.66415 3.33579 9.99994 3.75 9.99994L7.04998 9.99994L7.04998 13.4991C7.04998 13.7477 7.25145 13.9491 7.49998 13.9491C7.7485 13.9491 7.94998 13.7477 7.94998 13.4991L7.94998 9.99994L11.25 9.99994C11.6642 9.99994 12 9.66415 12 9.24994L12 5.74994C12 5.33573 11.6642 4.99994 11.25 4.99994L7.94998 4.99994ZM7.04998 8.99994L4 8.99994L4 5.99994L7.04998 5.99994L7.04998 8.99994Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/justify-end.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.95 1.49953C13.95 1.251 13.7485 1.04953 13.5 1.04953C13.2514 1.04953 13.05 1.251 13.05 1.49953L13.05 13.4995C13.05 13.7481 13.2514 13.9495 13.5 13.9495C13.7485 13.9495 13.95 13.7481 13.95 13.4995L13.95 1.49953ZM3.99997 5.99997L11 5.99997L11 8.99997L3.99997 8.99997L3.99997 5.99997ZM11.25 4.99997C11.6642 4.99997 12 5.33576 12 5.74997L12 9.24997C12 9.66418 11.6642 9.99997 11.25 9.99997L3.74997 9.99997C3.33576 9.99997 2.99997 9.66418 2.99997 9.24997L2.99997 5.74997C2.99998 5.33576 3.33576 4.99997 3.74998 4.99997L11.25 4.99997Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/justify-start.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.05005 13.5005C1.05005 13.749 1.25152 13.9505 1.50005 13.9505C1.74858 13.9505 1.95005 13.749 1.95005 13.5005L1.95005 1.50047C1.95005 1.25194 1.74858 1.05047 1.50005 1.05047C1.25152 1.05047 1.05005 1.25194 1.05005 1.50047L1.05005 13.5005ZM11 9.00003L4.00002 9.00003L4.00002 6.00003L11 6.00003L11 9.00003ZM3.75002 10C3.33581 10 3.00002 9.66424 3.00002 9.25003L3.00002 5.75003C3.00002 5.33582 3.33581 5.00003 3.75002 5.00003L11.25 5.00003C11.6642 5.00003 12 5.33582 12 5.75003L12 9.25003C12 9.66424 11.6642 10 11.25 10L3.75002 10Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/justify-stretch.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.5 1.04956C13.7485 1.04956 13.95 1.25103 13.95 1.49956L13.95 13.4996C13.95 13.7481 13.7485 13.9496 13.5 13.9496C13.2514 13.9496 13.05 13.7481 13.05 13.4996L13.05 1.49956C13.05 1.25103 13.2514 1.04956 13.5 1.04956ZM1.49995 1.04966C1.74848 1.04966 1.94995 1.25113 1.94995 1.49966L1.94995 13.4997C1.94995 13.7482 1.74848 13.9497 1.49995 13.9497C1.25142 13.9497 1.04995 13.7482 1.04995 13.4997L1.04995 1.49966C1.04995 1.25113 1.25142 1.04966 1.49995 1.04966ZM3.99997 6L11 6L11 9L3.99997 9L3.99997 6ZM11.25 5C11.6642 5 12 5.33579 12 5.75L12 9.25C12 9.66421 11.6642 10 11.25 10L3.74997 10C3.33576 10 2.99997 9.66421 2.99997 9.25L2.99997 5.75C2.99998 5.33579 3.33576 5 3.74998 5L11.25 5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/keyboard.svg πŸ”—

@@ -0,0 +1,7 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect x=".5" y="3.5" width="14" height="8" rx="1" stroke="currentColor" />
+  <path
+    fill="currentColor"
+    d="M2 5H3V6H2zM4 5H5V6H4zM6 5H7V6H6zM8 5H9V6H8zM10 5H11V6H10zM12 5H13V6H12zM11 7H12V8H11zM12 9H13V10H12zM9 7H10V8H9zM7 7H8V8H7zM5 7H6V8H5zM3 7H4V8H3zM2 9H3V10H2zM4 9H11V10H4z"
+  />
+</svg>

assets/icons/radix/lap-timer.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.49998 0.5C5.49998 0.223858 5.72383 0 5.99998 0H7.49998H8.99998C9.27612 0 9.49998 0.223858 9.49998 0.5C9.49998 0.776142 9.27612 1 8.99998 1H7.99998V2.11922C9.09832 2.20409 10.119 2.56622 10.992 3.13572C11.0116 3.10851 11.0336 3.08252 11.058 3.05806L12.058 2.05806C12.3021 1.81398 12.6978 1.81398 12.9419 2.05806C13.186 2.30214 13.186 2.69786 12.9419 2.94194L11.967 3.91682C13.1595 5.07925 13.9 6.70314 13.9 8.49998C13.9 12.0346 11.0346 14.9 7.49998 14.9C3.96535 14.9 1.09998 12.0346 1.09998 8.49998C1.09998 5.13361 3.69904 2.3743 6.99998 2.11922V1H5.99998C5.72383 1 5.49998 0.776142 5.49998 0.5ZM2.09998 8.49998C2.09998 5.51764 4.51764 3.09998 7.49998 3.09998C10.4823 3.09998 12.9 5.51764 12.9 8.49998C12.9 11.4823 10.4823 13.9 7.49998 13.9C4.51764 13.9 2.09998 11.4823 2.09998 8.49998ZM7.49998 8.49998V4.09998C5.06992 4.09998 3.09998 6.06992 3.09998 8.49998C3.09998 10.93 5.06992 12.9 7.49998 12.9C8.715 12.9 9.815 12.4075 10.6112 11.6112L7.49998 8.49998Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/laptop.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 4.25C2 4.11193 2.11193 4 2.25 4H12.75C12.8881 4 13 4.11193 13 4.25V11.5H2V4.25ZM2.25 3C1.55964 3 1 3.55964 1 4.25V12H0V12.5C0 12.7761 0.223858 13 0.5 13H14.5C14.7761 13 15 12.7761 15 12.5V12H14V4.25C14 3.55964 13.4404 3 12.75 3H2.25Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/layers.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/layout.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9 2H6V13H9V2ZM10 2V13H12.5C12.7761 13 13 12.7761 13 12.5V2.5C13 2.22386 12.7761 2 12.5 2H10ZM2.5 2H5V13H2.5C2.22386 13 2 12.7761 2 12.5V2.5C2 2.22386 2.22386 2 2.5 2ZM2.5 1C1.67157 1 1 1.67157 1 2.5V12.5C1 13.3284 1.67157 14 2.5 14H12.5C13.3284 14 14 13.3284 14 12.5V2.5C14 1.67157 13.3284 1 12.5 1H2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/letter-case-uppercase.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.6255 2.75C3.83478 2.75 4.02192 2.88034 4.09448 3.07664L7.16985 11.3962C7.2656 11.6552 7.13324 11.9428 6.87423 12.0386C6.61522 12.1343 6.32763 12.002 6.23188 11.7429L5.22387 9.01603H2.02712L1.01911 11.7429C0.923362 12.002 0.635774 12.1343 0.376762 12.0386C0.117749 11.9428 -0.0146052 11.6552 0.0811401 11.3962L3.15651 3.07664C3.22908 2.88034 3.41621 2.75 3.6255 2.75ZM3.6255 4.69207L4.90966 8.16603H2.34133L3.6255 4.69207ZM11.3719 2.75C11.5811 2.75 11.7683 2.88034 11.8408 3.07664L14.9162 11.3962C15.012 11.6552 14.8796 11.9428 14.6206 12.0386C14.3616 12.1343 14.074 12.002 13.9782 11.7429L12.9702 9.01603H9.77348L8.76547 11.7429C8.66972 12.002 8.38213 12.1343 8.12312 12.0386C7.86411 11.9428 7.73175 11.6552 7.8275 11.3962L10.9029 3.07664C10.9754 2.88034 11.1626 2.75 11.3719 2.75ZM11.3719 4.69207L12.656 8.16603H10.0877L11.3719 4.69207Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/lightning-bolt.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.69667 0.0403541C8.90859 0.131038 9.03106 0.354857 8.99316 0.582235L8.0902 6.00001H12.5C12.6893 6.00001 12.8625 6.10701 12.9472 6.27641C13.0319 6.4458 13.0136 6.6485 12.8999 6.80001L6.89997 14.8C6.76167 14.9844 6.51521 15.0503 6.30328 14.9597C6.09135 14.869 5.96888 14.6452 6.00678 14.4178L6.90974 9H2.49999C2.31061 9 2.13748 8.893 2.05278 8.72361C1.96809 8.55422 1.98636 8.35151 2.09999 8.2L8.09997 0.200038C8.23828 0.0156255 8.48474 -0.0503301 8.69667 0.0403541ZM3.49999 8.00001H7.49997C7.64695 8.00001 7.78648 8.06467 7.88148 8.17682C7.97648 8.28896 8.01733 8.43723 7.99317 8.5822L7.33027 12.5596L11.5 7.00001H7.49997C7.353 7.00001 7.21347 6.93534 7.11846 6.8232C7.02346 6.71105 6.98261 6.56279 7.00678 6.41781L7.66968 2.44042L3.49999 8.00001Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/line-height.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/linkedin-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 1C1.44772 1 1 1.44772 1 2V13C1 13.5523 1.44772 14 2 14H13C13.5523 14 14 13.5523 14 13V2C14 1.44772 13.5523 1 13 1H2ZM3.05 6H4.95V12H3.05V6ZM5.075 4.005C5.075 4.59871 4.59371 5.08 4 5.08C3.4063 5.08 2.925 4.59871 2.925 4.005C2.925 3.41129 3.4063 2.93 4 2.93C4.59371 2.93 5.075 3.41129 5.075 4.005ZM12 8.35713C12 6.55208 10.8334 5.85033 9.67449 5.85033C9.29502 5.83163 8.91721 5.91119 8.57874 6.08107C8.32172 6.21007 8.05265 6.50523 7.84516 7.01853H7.79179V6.00044H6V12.0047H7.90616V8.8112C7.8786 8.48413 7.98327 8.06142 8.19741 7.80987C8.41156 7.55832 8.71789 7.49825 8.95015 7.46774H9.02258C9.62874 7.46774 10.0786 7.84301 10.0786 8.78868V12.0047H11.9847L12 8.35713Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/list-bullet.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.5 5.25C1.91421 5.25 2.25 4.91421 2.25 4.5C2.25 4.08579 1.91421 3.75 1.5 3.75C1.08579 3.75 0.75 4.08579 0.75 4.5C0.75 4.91421 1.08579 5.25 1.5 5.25ZM4 4.5C4 4.22386 4.22386 4 4.5 4H13.5C13.7761 4 14 4.22386 14 4.5C14 4.77614 13.7761 5 13.5 5H4.5C4.22386 5 4 4.77614 4 4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H13.5C13.7761 8 14 7.77614 14 7.5C14 7.22386 13.7761 7 13.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H13.5C13.7761 11 14 10.7761 14 10.5C14 10.2239 13.7761 10 13.5 10H4.5ZM2.25 7.5C2.25 7.91421 1.91421 8.25 1.5 8.25C1.08579 8.25 0.75 7.91421 0.75 7.5C0.75 7.08579 1.08579 6.75 1.5 6.75C1.91421 6.75 2.25 7.08579 2.25 7.5ZM1.5 11.25C1.91421 11.25 2.25 10.9142 2.25 10.5C2.25 10.0858 1.91421 9.75 1.5 9.75C1.08579 9.75 0.75 10.0858 0.75 10.5C0.75 10.9142 1.08579 11.25 1.5 11.25Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/lock-closed.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5 4.63601C5 3.76031 5.24219 3.1054 5.64323 2.67357C6.03934 2.24705 6.64582 1.9783 7.5014 1.9783C8.35745 1.9783 8.96306 2.24652 9.35823 2.67208C9.75838 3.10299 10 3.75708 10 4.63325V5.99999H5V4.63601ZM4 5.99999V4.63601C4 3.58148 4.29339 2.65754 4.91049 1.99307C5.53252 1.32329 6.42675 0.978302 7.5014 0.978302C8.57583 0.978302 9.46952 1.32233 10.091 1.99162C10.7076 2.65557 11 3.57896 11 4.63325V5.99999H12C12.5523 5.99999 13 6.44771 13 6.99999V13C13 13.5523 12.5523 14 12 14H3C2.44772 14 2 13.5523 2 13V6.99999C2 6.44771 2.44772 5.99999 3 5.99999H4ZM3 6.99999H12V13H3V6.99999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/lock-open-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.4986 0C6.3257 0 5.36107 0.38943 4.73753 1.19361C4.23745 1.83856 4 2.68242 4 3.63325H5C5 2.84313 5.19691 2.23312 5.5278 1.80636C5.91615 1.30552 6.55152 1 7.4986 1C8.35683 1 8.96336 1.26502 9.35846 1.68623C9.75793 2.11211 10 2.76044 10 3.63601V6H3C2.44772 6 2 6.44772 2 7V13C2 13.5523 2.44772 14 3 14H12C12.5523 14 13 13.5523 13 13V7C13 6.44771 12.5523 6 12 6H11V3.63601C11 2.58135 10.7065 1.66167 10.0878 1.0021C9.46477 0.337871 8.57061 0 7.4986 0ZM3 7H12V13H3V7Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/lock-open-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9 3.63601C9 2.76044 9.24207 2.11211 9.64154 1.68623C10.0366 1.26502 10.6432 1 11.5014 1C12.4485 1 13.0839 1.30552 13.4722 1.80636C13.8031 2.23312 14 2.84313 14 3.63325H15C15 2.68242 14.7626 1.83856 14.2625 1.19361C13.6389 0.38943 12.6743 0 11.5014 0C10.4294 0 9.53523 0.337871 8.91218 1.0021C8.29351 1.66167 8 2.58135 8 3.63601V6H1C0.447715 6 0 6.44772 0 7V13C0 13.5523 0.447715 14 1 14H10C10.5523 14 11 13.5523 11 13V7C11 6.44772 10.5523 6 10 6H9V3.63601ZM1 7H10V13H1V7Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/loop.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.35355 1.85355C3.54882 1.65829 3.54882 1.34171 3.35355 1.14645C3.15829 0.951184 2.84171 0.951184 2.64645 1.14645L0.646447 3.14645C0.451184 3.34171 0.451184 3.65829 0.646447 3.85355L2.64645 5.85355C2.84171 6.04882 3.15829 6.04882 3.35355 5.85355C3.54882 5.65829 3.54882 5.34171 3.35355 5.14645L2.20711 4H9.5C11.433 4 13 5.567 13 7.5C13 7.77614 13.2239 8 13.5 8C13.7761 8 14 7.77614 14 7.5C14 5.01472 11.9853 3 9.5 3H2.20711L3.35355 1.85355ZM2 7.5C2 7.22386 1.77614 7 1.5 7C1.22386 7 1 7.22386 1 7.5C1 9.98528 3.01472 12 5.5 12H12.7929L11.6464 13.1464C11.4512 13.3417 11.4512 13.6583 11.6464 13.8536C11.8417 14.0488 12.1583 14.0488 12.3536 13.8536L14.3536 11.8536C14.5488 11.6583 14.5488 11.3417 14.3536 11.1464L12.3536 9.14645C12.1583 8.95118 11.8417 8.95118 11.6464 9.14645C11.4512 9.34171 11.4512 9.65829 11.6464 9.85355L12.7929 11H5.5C3.567 11 2 9.433 2 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/magic-wand.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/magnifying-glass.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10 6.5C10 8.433 8.433 10 6.5 10C4.567 10 3 8.433 3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5ZM9.30884 10.0159C8.53901 10.6318 7.56251 11 6.5 11C4.01472 11 2 8.98528 2 6.5C2 4.01472 4.01472 2 6.5 2C8.98528 2 11 4.01472 11 6.5C11 7.56251 10.6318 8.53901 10.0159 9.30884L12.8536 12.1464C13.0488 12.3417 13.0488 12.6583 12.8536 12.8536C12.6583 13.0488 12.3417 13.0488 12.1464 12.8536L9.30884 10.0159Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/margin.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/mask-off.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 2H14V13H1L1 2ZM0 2C0 1.44772 0.447715 1 1 1H14C14.5523 1 15 1.44772 15 2V13C15 13.5523 14.5523 14 14 14H1C0.447715 14 0 13.5523 0 13V2ZM4.875 7.5C4.875 6.05025 6.05025 4.875 7.5 4.875C8.94975 4.875 10.125 6.05025 10.125 7.5C10.125 8.94975 8.94975 10.125 7.5 10.125C6.05025 10.125 4.875 8.94975 4.875 7.5ZM7.5 3.875C5.49797 3.875 3.875 5.49797 3.875 7.5C3.875 9.50203 5.49797 11.125 7.5 11.125C9.50203 11.125 11.125 9.50203 11.125 7.5C11.125 5.49797 9.50203 3.875 7.5 3.875Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/mask-on.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 1C0.447715 1 0 1.44772 0 2V13C0 13.5523 0.447715 14 1 14H14C14.5523 14 15 13.5523 15 13V2C15 1.44772 14.5523 1 14 1H1ZM7.5 10.625C9.22589 10.625 10.625 9.22589 10.625 7.5C10.625 5.77411 9.22589 4.375 7.5 4.375C5.77411 4.375 4.375 5.77411 4.375 7.5C4.375 9.22589 5.77411 10.625 7.5 10.625Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/minus-circled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM4.50003 7C4.22389 7 4.00003 7.22386 4.00003 7.5C4.00003 7.77614 4.22389 8 4.50003 8H10.5C10.7762 8 11 7.77614 11 7.5C11 7.22386 10.7762 7 10.5 7H4.50003Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/minus.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.25 7.5C2.25 7.22386 2.47386 7 2.75 7H12.25C12.5261 7 12.75 7.22386 12.75 7.5C12.75 7.77614 12.5261 8 12.25 8H2.75C2.47386 8 2.25 7.77614 2.25 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/mix.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/mobile.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4 2.5C4 2.22386 4.22386 2 4.5 2H10.5C10.7761 2 11 2.22386 11 2.5V12.5C11 12.7761 10.7761 13 10.5 13H4.5C4.22386 13 4 12.7761 4 12.5V2.5ZM4.5 1C3.67157 1 3 1.67157 3 2.5V12.5C3 13.3284 3.67157 14 4.5 14H10.5C11.3284 14 12 13.3284 12 12.5V2.5C12 1.67157 11.3284 1 10.5 1H4.5ZM6 11.65C5.8067 11.65 5.65 11.8067 5.65 12C5.65 12.1933 5.8067 12.35 6 12.35H9C9.1933 12.35 9.35 12.1933 9.35 12C9.35 11.8067 9.1933 11.65 9 11.65H6Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/modulz-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.25925 3.16667L4.37036 5.33333V1L7.25925 3.16667ZM1 8.22222L3.88889 6.05555L1 3.88889V8.22222ZM1 14L3.88889 11.8333L1 9.66666V14ZM7.74072 8.22222L10.6296 6.05555L7.74072 3.88889V8.22222ZM14 3.16667L11.1111 5.33333V1L14 3.16667ZM11.1111 11.1111L14 8.94444L11.1111 6.77777V11.1111ZM3.88889 11.1111L1 8.94444L3.88889 6.77777V11.1111ZM4.37036 6.05555L7.25925 8.22222V3.88889L4.37036 6.05555ZM3.88889 5.33333L1 3.16667L3.88889 1V5.33333ZM7.74072 3.16667L10.6296 5.33333V1L7.74072 3.16667ZM14 8.22222L11.1111 6.05555L14 3.88889V8.22222ZM11.1111 11.8333L14 14V9.66666L11.1111 11.8333Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/moon.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/move.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/opacity.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 1.5C4.5 4.25 3 6.5 3 9C3 11.4853 5.01472 13.5 7.5 13.5C9.98528 13.5 12 11.4853 12 9C12 6.5 10.5 4.25 7.5 1.5ZM11 9C11 7.11203 9.97315 5.27195 7.5 2.87357C5.02686 5.27195 4 7.11203 4 9C4 10.933 5.567 12.5 7.5 12.5C9.433 12.5 11 10.933 11 9Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/open-in-new-window.svg πŸ”—

@@ -0,0 +1,10 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13 12C13 12.5523 12.5523 13 12 13H8.5C8.22386 13 8 12.7761 8 12.5C8 12.2239 8.22386 12 8.5 12H12V3H3V6.5C3 6.77614 2.77614 7 2.5 7C2.22386 7 2 6.77614 2 6.5V3C2 2.44771 2.44771 2 3 2H12C12.5523 2 13 2.44771 13 3V12Z"
+    fill="currentColor"
+  />
+  <path d="M5.5 6.5H8.5V9.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
+  <path d="M2.5 12.5L8.5 6.5" stroke="currentColor" stroke-linecap="round" />
+</svg>

assets/icons/radix/outer-shadow.svg πŸ”—

@@ -0,0 +1,43 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    opacity=".05"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.1398 3.88616C13.8553 4.94159 15 6.837 15 8.99999C15 12.3137 12.3137 15 9.00001 15C6.8435 15 4.95295 13.8621 3.89569 12.1552L4.32075 11.8919C5.29069 13.4578 7.02375 14.5 9.00001 14.5C12.0375 14.5 14.5 12.0375 14.5 8.99999C14.5 7.0178 13.4516 5.28026 11.8778 4.31202L12.1398 3.88616Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".2"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.851 5.0732C13.8683 6.07105 14.5 7.46198 14.5 8.99999C14.5 12.0375 12.0375 14.5 8.99996 14.5C7.46208 14.5 6.07125 13.8685 5.07342 12.8512L5.43036 12.5011C6.33803 13.4264 7.60179 14 8.99996 14C11.7614 14 14 11.7614 14 8.99999C14 7.6017 13.4263 6.33785 12.5009 5.43017L12.851 5.0732Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".35"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.3021 6.45071C13.7455 7.19737 14 8.06934 14 9C14 11.7614 11.7614 14 9.00001 14C8.04867 14 7.15867 13.7341 6.40118 13.2723L6.66141 12.8454C7.34274 13.2607 8.14305 13.5 9.00001 13.5C11.4853 13.5 13.5 11.4853 13.5 9C13.5 8.16164 13.271 7.37753 12.8722 6.70598L13.3021 6.45071Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".5"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.3744 7.94021C13.4566 8.2803 13.5 8.63524 13.5 9C13.5 11.4853 11.4853 13.5 9.00002 13.5C8.61103 13.5 8.23321 13.4506 7.87267 13.3576L7.99758 12.8734C8.31767 12.956 8.65352 13 9.00002 13C11.2091 13 13 11.2091 13 9C13 8.67507 12.9613 8.35952 12.8884 8.05756L13.3744 7.94021Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".65"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.9155 9.82132C12.5898 11.3813 11.3562 12.6072 9.79203 12.9215L9.69353 12.4313C11.0613 12.1565 12.1413 11.0833 12.4261 9.71913L12.9155 9.82132Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.2771 7.50252C1.2771 4.06455 4.06413 1.27753 7.50209 1.27753C10.94 1.27753 13.7271 4.06455 13.7271 7.50252C13.7271 10.9405 10.94 13.7275 7.50209 13.7275C4.06412 13.7275 1.2771 10.9405 1.2771 7.50252ZM7.50209 2.22752C4.5888 2.22752 2.2271 4.58922 2.2271 7.50252C2.2271 10.4158 4.5888 12.7775 7.50209 12.7775C10.4154 12.7775 12.7771 10.4158 12.7771 7.50252C12.7771 4.58922 10.4154 2.22752 7.50209 2.22752Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/overline.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.49985 1.10001C3.27894 1.10001 3.09985 1.27909 3.09985 1.50001C3.09985 1.72092 3.27894 1.90001 3.49985 1.90001H11.4999C11.7208 1.90001 11.8999 1.72092 11.8999 1.50001C11.8999 1.27909 11.7208 1.10001 11.4999 1.10001H3.49985ZM4.99995 4.25001C4.99995 3.97387 4.77609 3.75001 4.49995 3.75001C4.22381 3.75001 3.99995 3.97387 3.99995 4.25001V9.55001C3.99995 11.483 5.56695 13.05 7.49995 13.05C9.43295 13.05 11 11.483 11 9.55001V4.25001C11 3.97387 10.7761 3.75001 10.5 3.75001C10.2238 3.75001 9.99995 3.97387 9.99995 4.25001V9.55001C9.99995 10.9307 8.88066 12.05 7.49995 12.05C6.11924 12.05 4.99995 10.9307 4.99995 9.55001V4.25001Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/padding.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/paper-plane.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.20308 1.04312C1.00481 0.954998 0.772341 1.0048 0.627577 1.16641C0.482813 1.32802 0.458794 1.56455 0.568117 1.75196L3.92115 7.50002L0.568117 13.2481C0.458794 13.4355 0.482813 13.672 0.627577 13.8336C0.772341 13.9952 1.00481 14.045 1.20308 13.9569L14.7031 7.95693C14.8836 7.87668 15 7.69762 15 7.50002C15 7.30243 14.8836 7.12337 14.7031 7.04312L1.20308 1.04312ZM4.84553 7.10002L2.21234 2.586L13.2689 7.50002L2.21234 12.414L4.84552 7.90002H9C9.22092 7.90002 9.4 7.72094 9.4 7.50002C9.4 7.27911 9.22092 7.10002 9 7.10002H4.84553Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pause.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.04995 2.74998C6.04995 2.44623 5.80371 2.19998 5.49995 2.19998C5.19619 2.19998 4.94995 2.44623 4.94995 2.74998V12.25C4.94995 12.5537 5.19619 12.8 5.49995 12.8C5.80371 12.8 6.04995 12.5537 6.04995 12.25V2.74998ZM10.05 2.74998C10.05 2.44623 9.80371 2.19998 9.49995 2.19998C9.19619 2.19998 8.94995 2.44623 8.94995 2.74998V12.25C8.94995 12.5537 9.19619 12.8 9.49995 12.8C9.80371 12.8 10.05 12.5537 10.05 12.25V2.74998Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pencil-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.8536 1.14645C11.6583 0.951184 11.3417 0.951184 11.1465 1.14645L3.71455 8.57836C3.62459 8.66832 3.55263 8.77461 3.50251 8.89155L2.04044 12.303C1.9599 12.491 2.00189 12.709 2.14646 12.8536C2.29103 12.9981 2.50905 13.0401 2.69697 12.9596L6.10847 11.4975C6.2254 11.4474 6.3317 11.3754 6.42166 11.2855L13.8536 3.85355C14.0488 3.65829 14.0488 3.34171 13.8536 3.14645L11.8536 1.14645ZM4.42166 9.28547L11.5 2.20711L12.7929 3.5L5.71455 10.5784L4.21924 11.2192L3.78081 10.7808L4.42166 9.28547Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pencil-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/person.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 0.875C5.49797 0.875 3.875 2.49797 3.875 4.5C3.875 6.15288 4.98124 7.54738 6.49373 7.98351C5.2997 8.12901 4.27557 8.55134 3.50407 9.31167C2.52216 10.2794 2.02502 11.72 2.02502 13.5999C2.02502 13.8623 2.23769 14.0749 2.50002 14.0749C2.76236 14.0749 2.97502 13.8623 2.97502 13.5999C2.97502 11.8799 3.42786 10.7206 4.17091 9.9883C4.91536 9.25463 6.02674 8.87499 7.49995 8.87499C8.97317 8.87499 10.0846 9.25463 10.8291 9.98831C11.5721 10.7206 12.025 11.8799 12.025 13.5999C12.025 13.8623 12.2376 14.0749 12.5 14.0749C12.7623 14.075 12.975 13.8623 12.975 13.6C12.975 11.72 12.4778 10.2794 11.4959 9.31166C10.7244 8.55135 9.70025 8.12903 8.50625 7.98352C10.0187 7.5474 11.125 6.15289 11.125 4.5C11.125 2.49797 9.50203 0.875 7.5 0.875ZM4.825 4.5C4.825 3.02264 6.02264 1.825 7.5 1.825C8.97736 1.825 10.175 3.02264 10.175 4.5C10.175 5.97736 8.97736 7.175 7.5 7.175C6.02264 7.175 4.825 5.97736 4.825 4.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pie-chart.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.85001 7.50043C1.85001 4.37975 4.37963 1.85001 7.50001 1.85001C10.6204 1.85001 13.15 4.37975 13.15 7.50043C13.15 10.6211 10.6204 13.1509 7.50001 13.1509C4.37963 13.1509 1.85001 10.6211 1.85001 7.50043ZM7.50001 0.850006C3.82728 0.850006 0.850006 3.82753 0.850006 7.50043C0.850006 11.1733 3.82728 14.1509 7.50001 14.1509C11.1727 14.1509 14.15 11.1733 14.15 7.50043C14.15 3.82753 11.1727 0.850006 7.50001 0.850006ZM7.00001 8.00001V3.12811C7.16411 3.10954 7.33094 3.10001 7.50001 3.10001C9.93006 3.10001 11.9 5.07014 11.9 7.50043C11.9 7.66935 11.8905 7.83604 11.872 8.00001H7.00001Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pilcrow.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3 5.5C3 7.983 4.99169 9 7 9V12.5C7 12.7761 7.22386 13 7.5 13C7.77614 13 8 12.7761 8 12.5V9V3.1H9V12.5C9 12.7761 9.22386 13 9.5 13C9.77614 13 10 12.7761 10 12.5V3.1H11.5C11.8038 3.1 12.05 2.85376 12.05 2.55C12.05 2.24624 11.8038 2 11.5 2H9.5H8H7.5H7C4.99169 2 3 3.017 3 5.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pin-bottom.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.5 13.95C13.7485 13.95 13.95 13.7485 13.95 13.5C13.95 13.2514 13.7485 13.05 13.5 13.05L1.49995 13.05C1.25142 13.05 1.04995 13.2514 1.04995 13.5C1.04995 13.7485 1.25142 13.95 1.49995 13.95L13.5 13.95ZM11.0681 7.5683C11.2439 7.39257 11.2439 7.10764 11.0681 6.93191C10.8924 6.75617 10.6075 6.75617 10.4317 6.93191L7.94993 9.41371L7.94993 1.49998C7.94993 1.25146 7.74846 1.04998 7.49993 1.04998C7.2514 1.04998 7.04993 1.25146 7.04993 1.49998L7.04993 9.41371L4.56813 6.93191C4.39239 6.75617 4.10746 6.75617 3.93173 6.93191C3.75599 7.10764 3.75599 7.39257 3.93173 7.5683L7.18173 10.8183C7.35746 10.994 7.64239 10.994 7.81812 10.8183L11.0681 7.5683Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pin-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.05005 13.5C2.05005 13.7485 2.25152 13.95 2.50005 13.95C2.74858 13.95 2.95005 13.7485 2.95005 13.5L2.95005 1.49995C2.95005 1.25142 2.74858 1.04995 2.50005 1.04995C2.25152 1.04995 2.05005 1.25142 2.05005 1.49995L2.05005 13.5ZM8.4317 11.0681C8.60743 11.2439 8.89236 11.2439 9.06809 11.0681C9.24383 10.8924 9.24383 10.6075 9.06809 10.4317L6.58629 7.94993L14.5 7.94993C14.7485 7.94993 14.95 7.74846 14.95 7.49993C14.95 7.2514 14.7485 7.04993 14.5 7.04993L6.58629 7.04993L9.06809 4.56813C9.24383 4.39239 9.24383 4.10746 9.06809 3.93173C8.89236 3.75599 8.60743 3.75599 8.4317 3.93173L5.1817 7.18173C5.00596 7.35746 5.00596 7.64239 5.1817 7.81812L8.4317 11.0681Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pin-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.95 1.50005C12.95 1.25152 12.7485 1.05005 12.5 1.05005C12.2514 1.05005 12.05 1.25152 12.05 1.50005L12.05 13.5C12.05 13.7486 12.2514 13.95 12.5 13.95C12.7485 13.95 12.95 13.7486 12.95 13.5L12.95 1.50005ZM6.5683 3.93188C6.39257 3.75614 6.10764 3.75614 5.93191 3.93188C5.75617 4.10761 5.75617 4.39254 5.93191 4.56827L8.41371 7.05007L0.499984 7.05007C0.251456 7.05007 0.0499847 7.25155 0.0499847 7.50007C0.0499846 7.7486 0.251457 7.95007 0.499984 7.95007L8.41371 7.95007L5.93191 10.4319C5.75617 10.6076 5.75617 10.8925 5.93191 11.0683C6.10764 11.244 6.39257 11.244 6.56831 11.0683L9.8183 7.81827C9.99404 7.64254 9.99404 7.35761 9.8183 7.18188L6.5683 3.93188Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/pin-top.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.50005 1.05005C1.25152 1.05005 1.05005 1.25152 1.05005 1.50005C1.05005 1.74858 1.25152 1.95005 1.50005 1.95005L13.5 1.95005C13.7486 1.95005 13.95 1.74858 13.95 1.50005C13.95 1.25152 13.7486 1.05005 13.5 1.05005H1.50005ZM3.93188 7.43169C3.75614 7.60743 3.75614 7.89236 3.93188 8.06809C4.10761 8.24383 4.39254 8.24383 4.56827 8.06809L7.05007 5.58629V13.5C7.05007 13.7485 7.25155 13.95 7.50007 13.95C7.7486 13.95 7.95007 13.7485 7.95007 13.5L7.95007 5.58629L10.4319 8.06809C10.6076 8.24383 10.8925 8.24383 11.0683 8.06809C11.244 7.89235 11.244 7.60743 11.0683 7.43169L7.81827 4.18169C7.64254 4.00596 7.35761 4.00596 7.18188 4.18169L3.93188 7.43169Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/play.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.24182 2.32181C3.3919 2.23132 3.5784 2.22601 3.73338 2.30781L12.7334 7.05781C12.8974 7.14436 13 7.31457 13 7.5C13 7.68543 12.8974 7.85564 12.7334 7.94219L3.73338 12.6922C3.5784 12.774 3.3919 12.7687 3.24182 12.6782C3.09175 12.5877 3 12.4252 3 12.25V2.75C3 2.57476 3.09175 2.4123 3.24182 2.32181ZM4 3.57925V11.4207L11.4288 7.5L4 3.57925Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/plus-circled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM7.50003 4C7.77617 4 8.00003 4.22386 8.00003 4.5V7H10.5C10.7762 7 11 7.22386 11 7.5C11 7.77614 10.7762 8 10.5 8H8.00003V10.5C8.00003 10.7761 7.77617 11 7.50003 11C7.22389 11 7.00003 10.7761 7.00003 10.5V8H4.50003C4.22389 8 4.00003 7.77614 4.00003 7.5C4.00003 7.22386 4.22389 7 4.50003 7H7.00003V4.5C7.00003 4.22386 7.22389 4 7.50003 4Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/plus.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8 2.75C8 2.47386 7.77614 2.25 7.5 2.25C7.22386 2.25 7 2.47386 7 2.75V7H2.75C2.47386 7 2.25 7.22386 2.25 7.5C2.25 7.77614 2.47386 8 2.75 8H7V12.25C7 12.5261 7.22386 12.75 7.5 12.75C7.77614 12.75 8 12.5261 8 12.25V8H12.25C12.5261 8 12.75 7.77614 12.75 7.5C12.75 7.22386 12.5261 7 12.25 7H8V2.75Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/question-mark.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.07505 4.10001C5.07505 2.91103 6.25727 1.92502 7.50005 1.92502C8.74283 1.92502 9.92505 2.91103 9.92505 4.10001C9.92505 5.19861 9.36782 5.71436 8.61854 6.37884L8.58757 6.4063C7.84481 7.06467 6.92505 7.87995 6.92505 9.5C6.92505 9.81757 7.18248 10.075 7.50005 10.075C7.81761 10.075 8.07505 9.81757 8.07505 9.5C8.07505 8.41517 8.62945 7.90623 9.38156 7.23925L9.40238 7.22079C10.1496 6.55829 11.075 5.73775 11.075 4.10001C11.075 2.12757 9.21869 0.775024 7.50005 0.775024C5.7814 0.775024 3.92505 2.12757 3.92505 4.10001C3.92505 4.41758 4.18249 4.67501 4.50005 4.67501C4.81761 4.67501 5.07505 4.41758 5.07505 4.10001ZM7.50005 13.3575C7.9833 13.3575 8.37505 12.9657 8.37505 12.4825C8.37505 11.9992 7.9833 11.6075 7.50005 11.6075C7.0168 11.6075 6.62505 11.9992 6.62505 12.4825C6.62505 12.9657 7.0168 13.3575 7.50005 13.3575Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/quote.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/radiobutton.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49985 0.877045C3.84216 0.877045 0.877014 3.84219 0.877014 7.49988C0.877014 11.1575 3.84216 14.1227 7.49985 14.1227C11.1575 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 3.84219 11.1575 0.877045 7.49985 0.877045ZM1.82701 7.49988C1.82701 4.36686 4.36683 1.82704 7.49985 1.82704C10.6328 1.82704 13.1727 4.36686 13.1727 7.49988C13.1727 10.6329 10.6328 13.1727 7.49985 13.1727C4.36683 13.1727 1.82701 10.6329 1.82701 7.49988ZM7.49999 9.49999C8.60456 9.49999 9.49999 8.60456 9.49999 7.49999C9.49999 6.39542 8.60456 5.49999 7.49999 5.49999C6.39542 5.49999 5.49999 6.39542 5.49999 7.49999C5.49999 8.60456 6.39542 9.49999 7.49999 9.49999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/reader.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/reload.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.84998 7.49998C1.84998 4.66458 4.05979 1.84998 7.49998 1.84998C10.2783 1.84998 11.6515 3.9064 12.2367 5H10.5C10.2239 5 10 5.22386 10 5.5C10 5.77614 10.2239 6 10.5 6H13.5C13.7761 6 14 5.77614 14 5.5V2.5C14 2.22386 13.7761 2 13.5 2C13.2239 2 13 2.22386 13 2.5V4.31318C12.2955 3.07126 10.6659 0.849976 7.49998 0.849976C3.43716 0.849976 0.849976 4.18537 0.849976 7.49998C0.849976 10.8146 3.43716 14.15 7.49998 14.15C9.44382 14.15 11.0622 13.3808 12.2145 12.2084C12.8315 11.5806 13.3133 10.839 13.6418 10.0407C13.7469 9.78536 13.6251 9.49315 13.3698 9.38806C13.1144 9.28296 12.8222 9.40478 12.7171 9.66014C12.4363 10.3425 12.0251 10.9745 11.5013 11.5074C10.5295 12.4963 9.16504 13.15 7.49998 13.15C4.05979 13.15 1.84998 10.3354 1.84998 7.49998Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/reset.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.85355 2.14645C5.04882 2.34171 5.04882 2.65829 4.85355 2.85355L3.70711 4H9C11.4853 4 13.5 6.01472 13.5 8.5C13.5 10.9853 11.4853 13 9 13H5C4.72386 13 4.5 12.7761 4.5 12.5C4.5 12.2239 4.72386 12 5 12H9C10.933 12 12.5 10.433 12.5 8.5C12.5 6.567 10.933 5 9 5H3.70711L4.85355 6.14645C5.04882 6.34171 5.04882 6.65829 4.85355 6.85355C4.65829 7.04882 4.34171 7.04882 4.14645 6.85355L2.14645 4.85355C1.95118 4.65829 1.95118 4.34171 2.14645 4.14645L4.14645 2.14645C4.34171 1.95118 4.65829 1.95118 4.85355 2.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/resume.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.04995 2.74995C3.04995 2.44619 2.80371 2.19995 2.49995 2.19995C2.19619 2.19995 1.94995 2.44619 1.94995 2.74995V12.25C1.94995 12.5537 2.19619 12.8 2.49995 12.8C2.80371 12.8 3.04995 12.5537 3.04995 12.25V2.74995ZM5.73333 2.30776C5.57835 2.22596 5.39185 2.23127 5.24177 2.32176C5.0917 2.41225 4.99995 2.57471 4.99995 2.74995V12.25C4.99995 12.4252 5.0917 12.5877 5.24177 12.6781C5.39185 12.7686 5.57835 12.7739 5.73333 12.6921L14.7333 7.94214C14.8973 7.85559 15 7.68539 15 7.49995C15 7.31452 14.8973 7.14431 14.7333 7.05776L5.73333 2.30776ZM5.99995 11.4207V3.5792L13.4287 7.49995L5.99995 11.4207Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/rocket.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/rotate-counter-clockwise.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.59664 2.93628C7.76085 3.06401 8.00012 2.94698 8.00012 2.73895V1.99998C9.98143 2 11.1848 2.3637 11.9105 3.08945C12.6363 3.81522 13 5.0186 13 6.99998C13 7.27613 13.2239 7.49998 13.5 7.49998C13.7761 7.49998 14 7.27613 14 6.99998C14 4.9438 13.6325 3.39719 12.6176 2.38234C11.6028 1.36752 10.0562 0.999999 8.00012 0.999984V0.261266C8.00012 0.0532293 7.76085 -0.0637944 7.59664 0.063928L6.00384 1.30277C5.87516 1.40286 5.87516 1.59735 6.00384 1.69744L7.59664 2.93628ZM9.5 5H2.5C2.22386 5 2 5.22386 2 5.5V12.5C2 12.7761 2.22386 13 2.5 13H9.5C9.77614 13 10 12.7761 10 12.5V5.5C10 5.22386 9.77614 5 9.5 5ZM2.5 4C1.67157 4 1 4.67157 1 5.5V12.5C1 13.3284 1.67157 14 2.5 14H9.5C10.3284 14 11 13.3284 11 12.5V5.5C11 4.67157 10.3284 4 9.5 4H2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/row-spacing.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/rows.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14 12.85L1 12.85L1 14.15L14 14.15L14 12.85ZM14 8.85002L1 8.85002L1 10.15L14 10.15L14 8.85002ZM1 4.85003L14 4.85003L14 6.15003L1 6.15002L1 4.85003ZM14 0.850025L1 0.850025L1 2.15002L14 2.15002L14 0.850025Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/ruler-horizontal.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.5 4C0.223858 4 0 4.22386 0 4.5V10.5C0 10.7761 0.223858 11 0.5 11H14.5C14.7761 11 15 10.7761 15 10.5V4.5C15 4.22386 14.7761 4 14.5 4H0.5ZM1 10V5H2.075V7.5C2.075 7.73472 2.26528 7.925 2.5 7.925C2.73472 7.925 2.925 7.73472 2.925 7.5V5H4.075V6.5C4.075 6.73472 4.26528 6.925 4.5 6.925C4.73472 6.925 4.925 6.73472 4.925 6.5V5H6.075V6.5C6.075 6.73472 6.26528 6.925 6.5 6.925C6.73472 6.925 6.925 6.73472 6.925 6.5V5H8.075V7.5C8.075 7.73472 8.26528 7.925 8.5 7.925C8.73472 7.925 8.925 7.73472 8.925 7.5V5H10.075V6.5C10.075 6.73472 10.2653 6.925 10.5 6.925C10.7347 6.925 10.925 6.73472 10.925 6.5V5H12.075V6.5C12.075 6.73472 12.2653 6.925 12.5 6.925C12.7347 6.925 12.925 6.73472 12.925 6.5V5H14V10H1Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/scissors.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/section.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/sewing-pin-filled.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10 3.5C10 4.70948 9.14112 5.71836 8 5.94999V13.5C8 13.7761 7.77614 14 7.5 14C7.22386 14 7 13.7761 7 13.5V5.94999C5.85888 5.71836 5 4.70948 5 3.5C5 2.11929 6.11929 1 7.5 1C8.88071 1 10 2.11929 10 3.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/sewing-pin-solid.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10 3.5C10 4.70948 9.14112 5.71836 8 5.94999V13.5C8 13.7761 7.77614 14 7.5 14C7.22386 14 7 13.7761 7 13.5V5.94999C5.85888 5.71836 5 4.70948 5 3.5C5 2.11929 6.11929 1 7.5 1C8.88071 1 10 2.11929 10 3.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/sewing-pin.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6 3.5C6 2.67157 6.67157 2 7.5 2C8.32843 2 9 2.67157 9 3.5C9 4.32843 8.32843 5 7.5 5C6.67157 5 6 4.32843 6 3.5ZM8 5.94999C9.14112 5.71836 10 4.70948 10 3.5C10 2.11929 8.88071 1 7.5 1C6.11929 1 5 2.11929 5 3.5C5 4.70948 5.85888 5.71836 7 5.94999V13.5C7 13.7761 7.22386 14 7.5 14C7.77614 14 8 13.7761 8 13.5V5.94999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/shadow-inner.svg πŸ”—

@@ -0,0 +1,78 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    opacity=".05"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.1619 3.85182C8.35817 4.88918 4.88936 8.358 3.85199 12.1617L3.3696 12.0301C4.45356 8.05564 8.05581 4.45339 12.0303 3.36943L12.1619 3.85182Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".1"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.8807 3.42707C8.03441 4.50542 4.50561 8.03422 3.42726 11.8805L2.94582 11.7456C4.07129 7.73121 7.7314 4.0711 11.7458 2.94563L11.8807 3.42707Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".15"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.5201 3.02556C7.69092 4.16199 4.16779 7.68323 3.02805 11.512L2.54883 11.3694C3.73676 7.37869 7.38659 3.73076 11.3778 2.54623L11.5201 3.02556Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".2"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.0468 2.66169C7.31117 3.87664 3.87918 7.3079 2.66298 11.0434L2.18754 10.8886C3.45324 7.00109 7.00445 3.45062 10.8921 2.18621L11.0468 2.66169Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".25"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.5201 2.32365C6.92091 3.61447 3.62391 6.90876 2.32845 10.5073L1.858 10.338C3.20398 6.59909 6.61155 3.19424 10.3513 1.85301L10.5201 2.32365Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".3"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.90222 2.03122C6.50003 3.39465 3.39968 6.49367 2.03399 9.89551L1.56998 9.70924C2.98651 6.18076 6.18728 2.98133 9.71622 1.5671L9.90222 2.03122Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".35"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.20727 1.78873C6.06136 3.20349 3.21103 6.05203 1.79331 9.19738L1.33747 8.99192C2.80536 5.73528 5.74485 2.7976 9.0022 1.33272L9.20727 1.78873Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".4"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.40713 1.62085C5.59323 3.05117 3.05794 5.58509 1.62544 8.39847L1.17987 8.1716C2.66036 5.26397 5.27232 2.6534 8.18057 1.17513L8.40713 1.62085Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".45"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.46207 1.56747C5.08689 2.94695 2.95362 5.07912 1.57249 7.45379L1.14028 7.20241C2.56503 4.75273 4.7607 2.55818 7.21096 1.1351L7.46207 1.56747Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".5"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.30407 1.70487C4.51964 2.91063 2.90983 4.52061 1.7043 6.30513L1.28998 6.02524C2.5313 4.18773 4.18673 2.53214 6.02413 1.29059L6.30407 1.70487Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/shadow-none.svg πŸ”—

@@ -0,0 +1,78 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    opacity=".05"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.78296 13.376C8.73904 9.95284 8.73904 5.04719 6.78296 1.62405L7.21708 1.37598C9.261 4.95283 9.261 10.0472 7.21708 13.624L6.78296 13.376Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".1"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.28204 13.4775C9.23929 9.99523 9.23929 5.00475 7.28204 1.52248L7.71791 1.2775C9.76067 4.9119 9.76067 10.0881 7.71791 13.7225L7.28204 13.4775Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".15"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.82098 13.5064C9.72502 9.99523 9.72636 5.01411 7.82492 1.50084L8.26465 1.26285C10.2465 4.92466 10.2451 10.085 8.26052 13.7448L7.82098 13.5064Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".2"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.41284 13.429C10.1952 9.92842 10.1957 5.07537 8.41435 1.57402L8.85999 1.34729C10.7139 4.99113 10.7133 10.0128 8.85841 13.6559L8.41284 13.429Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".25"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.02441 13.2956C10.6567 9.8379 10.6586 5.17715 9.03005 1.71656L9.48245 1.50366C11.1745 5.09919 11.1726 9.91629 9.47657 13.5091L9.02441 13.2956Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".3"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.66809 13.0655C11.1097 9.69572 11.1107 5.3121 9.67088 1.94095L10.1307 1.74457C11.6241 5.24121 11.6231 9.76683 10.1278 13.2622L9.66809 13.0655Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".35"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.331 12.7456C11.5551 9.52073 11.5564 5.49103 10.3347 2.26444L10.8024 2.0874C12.0672 5.42815 12.0659 9.58394 10.7985 12.9231L10.331 12.7456Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".4"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.0155 12.2986C11.9938 9.29744 11.9948 5.71296 11.0184 2.71067L11.4939 2.55603C12.503 5.6589 12.502 9.35178 11.4909 12.4535L11.0155 12.2986Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".45"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.7214 11.668C12.4254 9.01303 12.4262 5.99691 11.7237 3.34116L12.2071 3.21329C12.9318 5.95292 12.931 9.05728 12.2047 11.7961L11.7214 11.668Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".5"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.4432 10.752C12.8524 8.63762 12.8523 6.36089 12.4429 4.2466L12.9338 4.15155C13.3553 6.32861 13.3554 8.66985 12.9341 10.847L12.4432 10.752Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49991 0.877045C3.84222 0.877045 0.877075 3.84219 0.877075 7.49988C0.877075 9.1488 1.47969 10.657 2.4767 11.8162L1.64647 12.6464C1.45121 12.8417 1.45121 13.1583 1.64647 13.3535C1.84173 13.5488 2.15832 13.5488 2.35358 13.3535L3.18383 12.5233C4.34302 13.5202 5.8511 14.1227 7.49991 14.1227C11.1576 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 5.85107 13.5202 4.34298 12.5233 3.1838L13.3536 2.35355C13.5488 2.15829 13.5488 1.8417 13.3536 1.64644C13.1583 1.45118 12.8417 1.45118 12.6465 1.64644L11.8162 2.47667C10.657 1.47966 9.14883 0.877045 7.49991 0.877045ZM11.1423 3.15065C10.1568 2.32449 8.88644 1.82704 7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 8.88641 2.32452 10.1568 3.15069 11.1422L11.1423 3.15065ZM3.85781 11.8493C4.84322 12.6753 6.11348 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 6.11345 12.6754 4.84319 11.8493 3.85778L3.85781 11.8493Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/shadow-outer.svg πŸ”—

@@ -0,0 +1,43 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    opacity=".05"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.1398 3.88617C13.8553 4.94159 15 6.83701 15 9.00001C15 12.3137 12.3137 15 9.00002 15C6.84351 15 4.95296 13.8621 3.89569 12.1552L4.32076 11.8919C5.29069 13.4578 7.02376 14.5 9.00002 14.5C12.0376 14.5 14.5 12.0375 14.5 9.00001C14.5 7.01781 13.4516 5.28027 11.8778 4.31203L12.1398 3.88617Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".2"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.851 5.07321C13.8684 6.07106 14.5 7.46199 14.5 9C14.5 12.0375 12.0376 14.5 9.00004 14.5C7.46215 14.5 6.07132 13.8685 5.07349 12.8513L5.43043 12.5011C6.3381 13.4264 7.60186 14 9.00004 14C11.7614 14 14 11.7614 14 9C14 7.60171 13.4264 6.33786 12.5009 5.43017L12.851 5.07321Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".35"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.3022 6.45071C13.7455 7.19737 14 8.06935 14 9.00001C14 11.7614 11.7614 14 9.00002 14C8.04868 14 7.15868 13.7341 6.40118 13.2724L6.66142 12.8454C7.34275 13.2607 8.14306 13.5 9.00002 13.5C11.4853 13.5 13.5 11.4853 13.5 9.00001C13.5 8.16165 13.271 7.37754 12.8722 6.70599L13.3022 6.45071Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".5"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.3745 7.94022C13.4566 8.28031 13.5 8.63525 13.5 9.00001C13.5 11.4853 11.4853 13.5 9.00003 13.5C8.61104 13.5 8.23323 13.4506 7.87268 13.3576L7.99759 12.8734C8.31768 12.956 8.65353 13 9.00003 13C11.2091 13 13 11.2091 13 9.00001C13 8.67509 12.9613 8.35953 12.8884 8.05757L13.3745 7.94022Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".65"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.9155 9.82133C12.5898 11.3813 11.3562 12.6072 9.79205 12.9215L9.69354 12.4313C11.0613 12.1565 12.1413 11.0834 12.4261 9.71915L12.9155 9.82133Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.2771 7.50253C1.2771 4.06456 4.06413 1.27753 7.5021 1.27753C10.94 1.27753 13.7271 4.06456 13.7271 7.50253C13.7271 10.9405 10.94 13.7275 7.5021 13.7275C4.06413 13.7275 1.2771 10.9405 1.2771 7.50253ZM7.5021 2.22753C4.5888 2.22753 2.2271 4.58923 2.2271 7.50253C2.2271 10.4158 4.5888 12.7775 7.5021 12.7775C10.4154 12.7775 12.7771 10.4158 12.7771 7.50253C12.7771 4.58923 10.4154 2.22753 7.5021 2.22753Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/shadow.svg πŸ”—

@@ -0,0 +1,78 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    opacity=".05"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.78296 13.376C8.73904 9.95284 8.73904 5.04719 6.78296 1.62405L7.21708 1.37598C9.261 4.95283 9.261 10.0472 7.21708 13.624L6.78296 13.376Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".1"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.28204 13.4775C9.23929 9.99523 9.23929 5.00475 7.28204 1.52248L7.71791 1.2775C9.76067 4.9119 9.76067 10.0881 7.71791 13.7225L7.28204 13.4775Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".15"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.82098 13.5064C9.72502 9.99523 9.72636 5.01411 7.82492 1.50084L8.26465 1.26285C10.2465 4.92466 10.2451 10.085 8.26052 13.7448L7.82098 13.5064Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".2"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8.41284 13.429C10.1952 9.92842 10.1957 5.07537 8.41435 1.57402L8.85999 1.34729C10.7139 4.99113 10.7133 10.0128 8.85841 13.6559L8.41284 13.429Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".25"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.02441 13.2956C10.6567 9.8379 10.6586 5.17715 9.03005 1.71656L9.48245 1.50366C11.1745 5.09919 11.1726 9.91629 9.47657 13.5091L9.02441 13.2956Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".3"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M9.66809 13.0655C11.1097 9.69572 11.1107 5.3121 9.67088 1.94095L10.1307 1.74457C11.6241 5.24121 11.6231 9.76683 10.1278 13.2622L9.66809 13.0655Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".35"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.331 12.7456C11.5551 9.52073 11.5564 5.49103 10.3347 2.26444L10.8024 2.0874C12.0672 5.42815 12.0659 9.58394 10.7985 12.9231L10.331 12.7456Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".4"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.0155 12.2986C11.9938 9.29744 11.9948 5.71296 11.0184 2.71067L11.4939 2.55603C12.503 5.6589 12.502 9.35178 11.4909 12.4535L11.0155 12.2986Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".45"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.7214 11.668C12.4254 9.01303 12.4262 5.99691 11.7237 3.34116L12.2071 3.21329C12.9318 5.95292 12.931 9.05728 12.2047 11.7961L11.7214 11.668Z"
+    fill="currentColor"
+  />
+  <path
+    opacity=".5"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M12.4432 10.752C12.8524 8.63762 12.8523 6.36089 12.4429 4.2466L12.9338 4.15155C13.3553 6.32861 13.3554 8.66985 12.9341 10.847L12.4432 10.752Z"
+    fill="currentColor"
+  />
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/share-1.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/share-2.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/shuffle.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/size.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M11.5 3.04999C11.7485 3.04999 11.95 3.25146 11.95 3.49999V7.49999C11.95 7.74852 11.7485 7.94999 11.5 7.94999C11.2515 7.94999 11.05 7.74852 11.05 7.49999V4.58639L4.58638 11.05H7.49999C7.74852 11.05 7.94999 11.2515 7.94999 11.5C7.94999 11.7485 7.74852 11.95 7.49999 11.95L3.49999 11.95C3.38064 11.95 3.26618 11.9026 3.18179 11.8182C3.0974 11.7338 3.04999 11.6193 3.04999 11.5L3.04999 7.49999C3.04999 7.25146 3.25146 7.04999 3.49999 7.04999C3.74852 7.04999 3.94999 7.25146 3.94999 7.49999L3.94999 10.4136L10.4136 3.94999L7.49999 3.94999C7.25146 3.94999 7.04999 3.74852 7.04999 3.49999C7.04999 3.25146 7.25146 3.04999 7.49999 3.04999L11.5 3.04999Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/sketch-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/slash.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.10876 14L9.46582 1H10.8178L5.46074 14H4.10876Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/slider.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.3004 7.49991C10.3004 8.4943 9.49426 9.30041 8.49988 9.30041C7.50549 9.30041 6.69938 8.4943 6.69938 7.49991C6.69938 6.50553 7.50549 5.69942 8.49988 5.69942C9.49426 5.69942 10.3004 6.50553 10.3004 7.49991ZM11.205 8C10.9699 9.28029 9.84816 10.2504 8.49988 10.2504C7.1516 10.2504 6.0299 9.28029 5.79473 8H0.5C0.223858 8 0 7.77614 0 7.5C0 7.22386 0.223858 7 0.5 7H5.7947C6.0298 5.71962 7.15154 4.74942 8.49988 4.74942C9.84822 4.74942 10.97 5.71962 11.2051 7H14.5C14.7761 7 15 7.22386 15 7.5C15 7.77614 14.7761 8 14.5 8H11.205Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/space-between-horizontally.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14.4999 0.999994C14.2237 0.999994 13.9999 1.22385 13.9999 1.49999L13.9999 5.99995L9.99992 5.99995C9.44764 5.99995 8.99993 6.44766 8.99993 6.99994L8.99993 7.99994C8.99993 8.55222 9.44764 8.99993 9.99992 8.99993L13.9999 8.99993L13.9999 13.4999C13.9999 13.776 14.2237 13.9999 14.4999 13.9999C14.776 13.9999 14.9999 13.776 14.9999 13.4999L14.9999 1.49999C14.9999 1.22385 14.776 0.999994 14.4999 0.999994ZM4.99996 5.99995L0.999992 5.99995L0.999992 1.49999C0.999992 1.22385 0.776136 0.999994 0.499996 0.999994C0.223856 0.999994 -9.7852e-09 1.22385 -2.18557e-08 1.49999L4.07279e-07 13.4999C3.95208e-07 13.776 0.223855 13.9999 0.499996 13.9999C0.776136 13.9999 0.999992 13.776 0.999992 13.4999L0.999992 8.99993L4.99996 8.99993C5.55224 8.99993 5.99995 8.55222 5.99995 7.99993L5.99995 6.99994C5.99995 6.44766 5.55224 5.99995 4.99996 5.99995Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/space-between-vertically.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.999878 0.5C0.999878 0.223858 1.22374 0 1.49988 0H13.4999C13.776 0 13.9999 0.223858 13.9999 0.5C13.9999 0.776142 13.776 1 13.4999 1L9 1V5C9 5.55228 8.55228 6 8 6H7C6.44772 6 6 5.55228 6 5V1H1.49988C1.22374 1 0.999878 0.776142 0.999878 0.5ZM7 9C6.44772 9 6 9.44771 6 10V14H1.49988C1.22374 14 0.999878 14.2239 0.999878 14.5C0.999878 14.7761 1.22374 15 1.49988 15H13.4999C13.776 15 13.9999 14.7761 13.9999 14.5C13.9999 14.2239 13.776 14 13.4999 14H9V10C9 9.44772 8.55228 9 8 9H7Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/space-evenly-vertically.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.999878 0.5C0.999878 0.223858 1.22374 0 1.49988 0H13.4999C13.776 0 13.9999 0.223858 13.9999 0.5C13.9999 0.776142 13.776 1 13.4999 1H1.49988C1.22374 1 0.999878 0.776142 0.999878 0.5ZM7 2C6.44772 2 6 2.44772 6 3V6C6 6.55228 6.44772 7 7 7H8C8.55228 7 9 6.55228 9 6V3C9 2.44772 8.55228 2 8 2H7ZM7 8C6.44772 8 6 8.44771 6 9V12C6 12.5523 6.44772 13 7 13H8C8.55228 13 9 12.5523 9 12V9C9 8.44772 8.55228 8 8 8H7ZM1.49988 14C1.22374 14 0.999878 14.2239 0.999878 14.5C0.999878 14.7761 1.22374 15 1.49988 15H13.4999C13.776 15 13.9999 14.7761 13.9999 14.5C13.9999 14.2239 13.776 14 13.4999 14H1.49988Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/speaker-moderate.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8 1.5C8 1.31062 7.893 1.13749 7.72361 1.05279C7.55421 0.968093 7.35151 0.986371 7.2 1.1L3.33333 4H1.5C0.671573 4 0 4.67158 0 5.5V9.5C0 10.3284 0.671573 11 1.5 11H3.33333L7.2 13.9C7.35151 14.0136 7.55421 14.0319 7.72361 13.9472C7.893 13.8625 8 13.6894 8 13.5V1.5ZM3.8 4.9L7 2.5V12.5L3.8 10.1C3.71345 10.0351 3.60819 10 3.5 10H1.5C1.22386 10 1 9.77614 1 9.5V5.5C1 5.22386 1.22386 5 1.5 5H3.5C3.60819 5 3.71345 4.96491 3.8 4.9ZM10.833 3.95949C10.7106 3.77557 10.4623 3.72567 10.2784 3.84804C10.0944 3.97041 10.0445 4.21871 10.1669 4.40264C11.4111 6.27268 11.4111 8.72728 10.1669 10.5973C10.0445 10.7813 10.0944 11.0296 10.2784 11.1519C10.4623 11.2743 10.7106 11.2244 10.833 11.0405C12.2558 8.90199 12.2558 6.09798 10.833 3.95949Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/speaker-off.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.72361 1.05279C7.893 1.13749 8 1.31062 8 1.5V13.5C8 13.6894 7.893 13.8625 7.72361 13.9472C7.55421 14.0319 7.35151 14.0136 7.2 13.9L3.33333 11H1.5C0.671573 11 0 10.3284 0 9.5V5.5C0 4.67158 0.671573 4 1.5 4H3.33333L7.2 1.1C7.35151 0.986371 7.55421 0.968093 7.72361 1.05279ZM7 2.5L3.8 4.9C3.71345 4.96491 3.60819 5 3.5 5H1.5C1.22386 5 1 5.22386 1 5.5V9.5C1 9.77614 1.22386 10 1.5 10H3.5C3.60819 10 3.71345 10.0351 3.8 10.1L7 12.5V2.5ZM14.8536 5.14645C15.0488 5.34171 15.0488 5.65829 14.8536 5.85355L13.2071 7.5L14.8536 9.14645C15.0488 9.34171 15.0488 9.65829 14.8536 9.85355C14.6583 10.0488 14.3417 10.0488 14.1464 9.85355L12.5 8.20711L10.8536 9.85355C10.6583 10.0488 10.3417 10.0488 10.1464 9.85355C9.95118 9.65829 9.95118 9.34171 10.1464 9.14645L11.7929 7.5L10.1464 5.85355C9.95118 5.65829 9.95118 5.34171 10.1464 5.14645C10.3417 4.95118 10.6583 4.95118 10.8536 5.14645L12.5 6.79289L14.1464 5.14645C14.3417 4.95118 14.6583 4.95118 14.8536 5.14645Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/speaker-quiet.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8 1.5C8 1.31062 7.893 1.13749 7.72361 1.05279C7.55421 0.968093 7.35151 0.986371 7.2 1.1L3.33333 4H1.5C0.671573 4 0 4.67158 0 5.5V9.5C0 10.3284 0.671573 11 1.5 11H3.33333L7.2 13.9C7.35151 14.0136 7.55421 14.0319 7.72361 13.9472C7.893 13.8625 8 13.6894 8 13.5V1.5ZM3.8 4.9L7 2.5V12.5L3.8 10.1C3.71345 10.0351 3.60819 10 3.5 10H1.5C1.22386 10 1 9.77614 1 9.5V5.5C1 5.22386 1.22386 5 1.5 5H3.5C3.60819 5 3.71345 4.96491 3.8 4.9ZM10.083 5.05577C9.96066 4.87185 9.71235 4.82195 9.52843 4.94432C9.3445 5.06669 9.2946 5.31499 9.41697 5.49892C10.2207 6.70693 10.2207 8.29303 9.41697 9.50104C9.2946 9.68496 9.3445 9.93326 9.52843 10.0556C9.71235 10.178 9.96066 10.1281 10.083 9.94418C11.0653 8.46773 11.0653 6.53222 10.083 5.05577Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/square.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 1H1.5H13.5H14V1.5V13.5V14H13.5H1.5H1V13.5V1.5V1ZM2 2V13H13V2H2Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/stack.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.75432 1.81954C7.59742 1.72682 7.4025 1.72682 7.24559 1.81954L1.74559 5.06954C1.59336 5.15949 1.49996 5.32317 1.49996 5.5C1.49996 5.67683 1.59336 5.84051 1.74559 5.93046L7.24559 9.18046C7.4025 9.27318 7.59742 9.27318 7.75432 9.18046L13.2543 5.93046C13.4066 5.84051 13.5 5.67683 13.5 5.5C13.5 5.32317 13.4066 5.15949 13.2543 5.06954L7.75432 1.81954ZM7.49996 8.16923L2.9828 5.5L7.49996 2.83077L12.0171 5.5L7.49996 8.16923ZM2.25432 8.31954C2.01658 8.17906 1.70998 8.2579 1.56949 8.49564C1.42901 8.73337 1.50785 9.03998 1.74559 9.18046L7.24559 12.4305C7.4025 12.5232 7.59742 12.5232 7.75432 12.4305L13.2543 9.18046C13.4921 9.03998 13.5709 8.73337 13.4304 8.49564C13.2899 8.2579 12.9833 8.17906 12.7456 8.31954L7.49996 11.4192L2.25432 8.31954Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/star-filled.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/star.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/stop.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 3C2 2.44772 2.44772 2 3 2H12C12.5523 2 13 2.44772 13 3V12C13 12.5523 12.5523 13 12 13H3C2.44772 13 2 12.5523 2 12V3ZM12 3H3V12H12V3Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/stopwatch.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.49998 0.5C5.49998 0.223858 5.72383 0 5.99998 0H7.49998H8.99998C9.27612 0 9.49998 0.223858 9.49998 0.5C9.49998 0.776142 9.27612 1 8.99998 1H7.99998V2.11922C9.09832 2.20409 10.119 2.56622 10.992 3.13572C11.0116 3.10851 11.0336 3.08252 11.058 3.05806L11.858 2.25806C12.1021 2.01398 12.4978 2.01398 12.7419 2.25806C12.986 2.50214 12.986 2.89786 12.7419 3.14194L11.967 3.91682C13.1595 5.07925 13.9 6.70314 13.9 8.49998C13.9 12.0346 11.0346 14.9 7.49998 14.9C3.96535 14.9 1.09998 12.0346 1.09998 8.49998C1.09998 5.13362 3.69904 2.3743 6.99998 2.11922V1H5.99998C5.72383 1 5.49998 0.776142 5.49998 0.5ZM2.09998 8.49998C2.09998 5.51764 4.51764 3.09998 7.49998 3.09998C10.4823 3.09998 12.9 5.51764 12.9 8.49998C12.9 11.4823 10.4823 13.9 7.49998 13.9C4.51764 13.9 2.09998 11.4823 2.09998 8.49998ZM7.99998 4.5C7.99998 4.22386 7.77612 4 7.49998 4C7.22383 4 6.99998 4.22386 6.99998 4.5V9.5C6.99998 9.77614 7.22383 10 7.49998 10C7.77612 10 7.99998 9.77614 7.99998 9.5V4.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/stretch-horizontally.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14.4999 0.999992C14.2237 0.999992 13.9999 1.22385 13.9999 1.49999L13.9999 5.99995L0.999992 5.99995L0.999992 1.49999C0.999992 1.22385 0.776136 0.999992 0.499996 0.999992C0.223856 0.999992 -9.78509e-09 1.22385 -2.18556e-08 1.49999L4.07279e-07 13.4999C3.95208e-07 13.776 0.223855 13.9999 0.499996 13.9999C0.776136 13.9999 0.999992 13.776 0.999992 13.4999L0.999992 8.99992L13.9999 8.99992L13.9999 13.4999C13.9999 13.776 14.2237 13.9999 14.4999 13.9999C14.776 13.9999 14.9999 13.776 14.9999 13.4999L14.9999 1.49999C14.9999 1.22385 14.776 0.999992 14.4999 0.999992Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/stretch-vertically.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.999878 0.5C0.999878 0.223858 1.22374 0 1.49988 0H13.4999C13.776 0 13.9999 0.223858 13.9999 0.5C13.9999 0.776142 13.776 1 13.4999 1H6H1.49988C1.22374 1 0.999878 0.776142 0.999878 0.5ZM9 14V1L6 1V14H1.49988C1.22374 14 0.999878 14.2239 0.999878 14.5C0.999878 14.7761 1.22374 15 1.49988 15H13.4999C13.776 15 13.9999 14.7761 13.9999 14.5C13.9999 14.2239 13.776 14 13.4999 14H9Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/strikethrough.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.00003 3.25C5.00003 2.97386 4.77617 2.75 4.50003 2.75C4.22389 2.75 4.00003 2.97386 4.00003 3.25V7.10003H2.49998C2.27906 7.10003 2.09998 7.27912 2.09998 7.50003C2.09998 7.72094 2.27906 7.90003 2.49998 7.90003H4.00003V8.55C4.00003 10.483 5.56703 12.05 7.50003 12.05C9.43303 12.05 11 10.483 11 8.55V7.90003H12.5C12.7209 7.90003 12.9 7.72094 12.9 7.50003C12.9 7.27912 12.7209 7.10003 12.5 7.10003H11V3.25C11 2.97386 10.7762 2.75 10.5 2.75C10.2239 2.75 10 2.97386 10 3.25V7.10003H5.00003V3.25ZM5.00003 7.90003V8.55C5.00003 9.93071 6.11932 11.05 7.50003 11.05C8.88074 11.05 10 9.93071 10 8.55V7.90003H5.00003Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/sun.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/switch.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10.5 4C8.567 4 7 5.567 7 7.5C7 9.433 8.567 11 10.5 11C12.433 11 14 9.433 14 7.5C14 5.567 12.433 4 10.5 4ZM7.67133 11C6.65183 10.175 6 8.91363 6 7.5C6 6.08637 6.65183 4.82498 7.67133 4H4.5C2.567 4 1 5.567 1 7.5C1 9.433 2.567 11 4.5 11H7.67133ZM0 7.5C0 5.01472 2.01472 3 4.5 3H10.5C12.9853 3 15 5.01472 15 7.5C15 9.98528 12.9853 12 10.5 12H4.5C2.01472 12 0 9.98528 0 7.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/symbol.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/table.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8 2H12.5C12.7761 2 13 2.22386 13 2.5V5H8V2ZM7 5V2H2.5C2.22386 2 2 2.22386 2 2.5V5H7ZM2 6V9H7V6H2ZM8 6H13V9H8V6ZM8 10H13V12.5C13 12.7761 12.7761 13 12.5 13H8V10ZM2 12.5V10H7V13H2.5C2.22386 13 2 12.7761 2 12.5ZM1 2.5C1 1.67157 1.67157 1 2.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/target.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/text-align-center.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 4.5C2 4.22386 2.22386 4 2.5 4H12.5C12.7761 4 13 4.22386 13 4.5C13 4.77614 12.7761 5 12.5 5H2.5C2.22386 5 2 4.77614 2 4.5ZM4 7.5C4 7.22386 4.22386 7 4.5 7H10.5C10.7761 7 11 7.22386 11 7.5C11 7.77614 10.7761 8 10.5 8H4.5C4.22386 8 4 7.77614 4 7.5ZM3 10.5C3 10.2239 3.22386 10 3.5 10H11.5C11.7761 10 12 10.2239 12 10.5C12 10.7761 11.7761 11 11.5 11H3.5C3.22386 11 3 10.7761 3 10.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/text-align-justify.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2.5 4C2.22386 4 2 4.22386 2 4.5C2 4.77614 2.22386 5 2.5 5H12.5C12.7761 5 13 4.77614 13 4.5C13 4.22386 12.7761 4 12.5 4H2.5ZM2 7.5C2 7.22386 2.22386 7 2.5 7H12.5C12.7761 7 13 7.22386 13 7.5C13 7.77614 12.7761 8 12.5 8H2.5C2.22386 8 2 7.77614 2 7.5ZM2 10.5C2 10.2239 2.22386 10 2.5 10H12.5C12.7761 10 13 10.2239 13 10.5C13 10.7761 12.7761 11 12.5 11H2.5C2.22386 11 2 10.7761 2 10.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/text-align-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 4.5C2 4.22386 2.22386 4 2.5 4H12.5C12.7761 4 13 4.22386 13 4.5C13 4.77614 12.7761 5 12.5 5H2.5C2.22386 5 2 4.77614 2 4.5ZM2 7.5C2 7.22386 2.22386 7 2.5 7H7.5C7.77614 7 8 7.22386 8 7.5C8 7.77614 7.77614 8 7.5 8H2.5C2.22386 8 2 7.77614 2 7.5ZM2 10.5C2 10.2239 2.22386 10 2.5 10H10.5C10.7761 10 11 10.2239 11 10.5C11 10.7761 10.7761 11 10.5 11H2.5C2.22386 11 2 10.7761 2 10.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/text-align-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M2 4.5C2 4.22386 2.22386 4 2.5 4H12.5C12.7761 4 13 4.22386 13 4.5C13 4.77614 12.7761 5 12.5 5H2.5C2.22386 5 2 4.77614 2 4.5ZM7 7.5C7 7.22386 7.22386 7 7.5 7H12.5C12.7761 7 13 7.22386 13 7.5C13 7.77614 12.7761 8 12.5 8H7.5C7.22386 8 7 7.77614 7 7.5ZM4 10.5C4 10.2239 4.22386 10 4.5 10H12.5C12.7761 10 13 10.2239 13 10.5C13 10.7761 12.7761 11 12.5 11H4.5C4.22386 11 4 10.7761 4 10.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/text-none.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.3536 2.35355C13.5488 2.15829 13.5488 1.84171 13.3536 1.64645C13.1583 1.45118 12.8417 1.45118 12.6464 1.64645L11.9291 2.36383C11.9159 2.32246 11.897 2.28368 11.8732 2.24845C11.7923 2.12875 11.6554 2.05005 11.5001 2.05005H3.50005C3.29909 2.05005 3.1289 2.18178 3.07111 2.3636C3.05743 2.40665 3.05005 2.45249 3.05005 2.50007V4.50001C3.05005 4.74854 3.25152 4.95001 3.50005 4.95001C3.74858 4.95001 3.95005 4.74854 3.95005 4.50001V2.95005H6.95006V7.34284L1.64645 12.6464C1.45118 12.8417 1.45118 13.1583 1.64645 13.3536C1.84171 13.5488 2.15829 13.5488 2.35355 13.3536L6.95006 8.75705V12.0501H5.7544C5.50587 12.0501 5.3044 12.2515 5.3044 12.5001C5.3044 12.7486 5.50587 12.9501 5.7544 12.9501H9.2544C9.50293 12.9501 9.7044 12.7486 9.7044 12.5001C9.7044 12.2515 9.50293 12.0501 9.2544 12.0501H8.05006V7.65705L13.3536 2.35355ZM8.05006 6.24284L11.0501 3.24283V2.95005H8.05006V6.24284Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/text.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M3.94993 2.95002L3.94993 4.49998C3.94993 4.74851 3.74845 4.94998 3.49993 4.94998C3.2514 4.94998 3.04993 4.74851 3.04993 4.49998V2.50004C3.04993 2.45246 3.05731 2.40661 3.07099 2.36357C3.12878 2.18175 3.29897 2.05002 3.49993 2.05002H11.4999C11.6553 2.05002 11.7922 2.12872 11.8731 2.24842C11.9216 2.32024 11.9499 2.40682 11.9499 2.50002L11.9499 2.50004V4.49998C11.9499 4.74851 11.7485 4.94998 11.4999 4.94998C11.2514 4.94998 11.0499 4.74851 11.0499 4.49998V2.95002H8.04993V12.05H9.25428C9.50281 12.05 9.70428 12.2515 9.70428 12.5C9.70428 12.7486 9.50281 12.95 9.25428 12.95H5.75428C5.50575 12.95 5.30428 12.7486 5.30428 12.5C5.30428 12.2515 5.50575 12.05 5.75428 12.05H6.94993V2.95002H3.94993Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/thick-arrow-down.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5 3.5C5 3.22386 5.22386 3 5.5 3H9.5C9.77614 3 10 3.22386 10 3.5V6H12.5C12.6873 6 12.8589 6.10467 12.9446 6.27121C13.0303 6.43774 13.0157 6.63821 12.9069 6.79062L7.90687 13.7906C7.81301 13.922 7.66148 14 7.5 14C7.33853 14 7.18699 13.922 7.09314 13.7906L2.09314 6.79062C1.98427 6.63821 1.96972 6.43774 2.05542 6.27121C2.14112 6.10467 2.31271 6 2.5 6H5V3.5ZM6 4V6.5C6 6.77614 5.77614 7 5.5 7H3.4716L7.5 12.6398L11.5284 7H9.5C9.22386 7 9 6.77614 9 6.5V4H6Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/thick-arrow-left.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1 7.5C1 7.66148 1.07798 7.81301 1.20938 7.90687L8.20938 12.9069C8.36179 13.0157 8.56226 13.0303 8.72879 12.9446C8.89533 12.8589 9 12.6873 9 12.5L9 10L11.5 10C11.7761 10 12 9.77614 12 9.5L12 5.5C12 5.22386 11.7761 5 11.5 5L9 5L9 2.5C9 2.31271 8.89533 2.14112 8.72879 2.05542C8.56226 1.96972 8.36179 1.98427 8.20938 2.09313L1.20938 7.09314C1.07798 7.18699 1 7.33853 1 7.5ZM8 3.4716L8 5.5C8 5.77614 8.22386 6 8.5 6L11 6L11 9L8.5 9C8.22386 9 8 9.22386 8 9.5L8 11.5284L2.36023 7.5L8 3.4716Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/thick-arrow-right.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14 7.5C14 7.66148 13.922 7.81301 13.7906 7.90687L6.79062 12.9069C6.63821 13.0157 6.43774 13.0303 6.27121 12.9446C6.10467 12.8589 6 12.6873 6 12.5L6 10L3.5 10C3.22386 10 3 9.77614 3 9.5L3 5.5C3 5.22386 3.22386 5 3.5 5L6 5L6 2.5C6 2.31271 6.10467 2.14112 6.27121 2.05542C6.43774 1.96972 6.63821 1.98427 6.79062 2.09313L13.7906 7.09314C13.922 7.18699 14 7.33853 14 7.5ZM7 3.4716L7 5.5C7 5.77614 6.77614 6 6.5 6L4 6L4 9L6.5 9C6.77614 9 7 9.22386 7 9.5L7 11.5284L12.6398 7.5L7 3.4716Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/thick-arrow-up.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.5 1C7.66148 1 7.81301 1.07798 7.90687 1.20938L12.9069 8.20938C13.0157 8.36179 13.0303 8.56226 12.9446 8.72879C12.8589 8.89533 12.6873 9 12.5 9H10V11.5C10 11.7761 9.77614 12 9.5 12H5.5C5.22386 12 5 11.7761 5 11.5V9H2.5C2.31271 9 2.14112 8.89533 2.05542 8.72879C1.96972 8.56226 1.98427 8.36179 2.09314 8.20938L7.09314 1.20938C7.18699 1.07798 7.33853 1 7.5 1ZM3.4716 8H5.5C5.77614 8 6 8.22386 6 8.5V11H9V8.5C9 8.22386 9.22386 8 9.5 8H11.5284L7.5 2.36023L3.4716 8Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/timer.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49998 0.849976C7.22383 0.849976 6.99998 1.07383 6.99998 1.34998V3.52234C6.99998 3.79848 7.22383 4.02234 7.49998 4.02234C7.77612 4.02234 7.99998 3.79848 7.99998 3.52234V1.8718C10.8862 2.12488 13.15 4.54806 13.15 7.49998C13.15 10.6204 10.6204 13.15 7.49998 13.15C4.37957 13.15 1.84998 10.6204 1.84998 7.49998C1.84998 6.10612 2.35407 4.83128 3.19049 3.8459C3.36919 3.63538 3.34339 3.31985 3.13286 3.14115C2.92234 2.96245 2.60681 2.98825 2.42811 3.19877C1.44405 4.35808 0.849976 5.86029 0.849976 7.49998C0.849976 11.1727 3.82728 14.15 7.49998 14.15C11.1727 14.15 14.15 11.1727 14.15 7.49998C14.15 3.82728 11.1727 0.849976 7.49998 0.849976ZM6.74049 8.08072L4.22363 4.57237C4.15231 4.47295 4.16346 4.33652 4.24998 4.25C4.33649 4.16348 4.47293 4.15233 4.57234 4.22365L8.08069 6.74051C8.56227 7.08599 8.61906 7.78091 8.19998 8.2C7.78089 8.61909 7.08597 8.56229 6.74049 8.08072Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/tokens.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.5 2C3.11929 2 2 3.11929 2 4.5C2 5.88072 3.11929 7 4.5 7C5.88072 7 7 5.88072 7 4.5C7 3.11929 5.88072 2 4.5 2ZM3 4.5C3 3.67157 3.67157 3 4.5 3C5.32843 3 6 3.67157 6 4.5C6 5.32843 5.32843 6 4.5 6C3.67157 6 3 5.32843 3 4.5ZM10.5 2C9.11929 2 8 3.11929 8 4.5C8 5.88072 9.11929 7 10.5 7C11.8807 7 13 5.88072 13 4.5C13 3.11929 11.8807 2 10.5 2ZM9 4.5C9 3.67157 9.67157 3 10.5 3C11.3284 3 12 3.67157 12 4.5C12 5.32843 11.3284 6 10.5 6C9.67157 6 9 5.32843 9 4.5ZM2 10.5C2 9.11929 3.11929 8 4.5 8C5.88072 8 7 9.11929 7 10.5C7 11.8807 5.88072 13 4.5 13C3.11929 13 2 11.8807 2 10.5ZM4.5 9C3.67157 9 3 9.67157 3 10.5C3 11.3284 3.67157 12 4.5 12C5.32843 12 6 11.3284 6 10.5C6 9.67157 5.32843 9 4.5 9ZM10.5 8C9.11929 8 8 9.11929 8 10.5C8 11.8807 9.11929 13 10.5 13C11.8807 13 13 11.8807 13 10.5C13 9.11929 11.8807 8 10.5 8ZM9 10.5C9 9.67157 9.67157 9 10.5 9C11.3284 9 12 9.67157 12 10.5C12 11.3284 11.3284 12 10.5 12C9.67157 12 9 11.3284 9 10.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/track-next.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M13.0502 2.74989C13.0502 2.44613 12.804 2.19989 12.5002 2.19989C12.1965 2.19989 11.9502 2.44613 11.9502 2.74989V7.2825C11.9046 7.18802 11.8295 7.10851 11.7334 7.05776L2.73338 2.30776C2.5784 2.22596 2.3919 2.23127 2.24182 2.32176C2.09175 2.41225 2 2.57471 2 2.74995V12.25C2 12.4252 2.09175 12.5877 2.24182 12.6781C2.3919 12.7686 2.5784 12.7739 2.73338 12.6921L11.7334 7.94214C11.8295 7.89139 11.9046 7.81188 11.9502 7.7174V12.2499C11.9502 12.5536 12.1965 12.7999 12.5002 12.7999C12.804 12.7999 13.0502 12.5536 13.0502 12.2499V2.74989ZM3 11.4207V3.5792L10.4288 7.49995L3 11.4207Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/track-previous.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.94976 2.74989C1.94976 2.44613 2.196 2.19989 2.49976 2.19989C2.80351 2.19989 3.04976 2.44613 3.04976 2.74989V7.2825C3.0954 7.18802 3.17046 7.10851 3.26662 7.05776L12.2666 2.30776C12.4216 2.22596 12.6081 2.23127 12.7582 2.32176C12.9083 2.41225 13 2.57471 13 2.74995V12.25C13 12.4252 12.9083 12.5877 12.7582 12.6781C12.6081 12.7686 12.4216 12.7739 12.2666 12.6921L3.26662 7.94214C3.17046 7.89139 3.0954 7.81188 3.04976 7.7174V12.2499C3.04976 12.5536 2.80351 12.7999 2.49976 12.7999C2.196 12.7999 1.94976 12.5536 1.94976 12.2499V2.74989ZM4.57122 7.49995L12 11.4207V3.5792L4.57122 7.49995Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/transform.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/transparency-grid.svg πŸ”—

@@ -0,0 +1,9 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    opacity=".25"
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0 0H3V3H0V0ZM6 3H3V6H0V9H3V12H0V15H3V12H6V15H9V12H12V15H15V12H12V9H15V6H12V3H15V0H12V3H9V0H6V3ZM6 6V3H9V6H6ZM6 9H3V6H6V9ZM9 9V6H12V9H9ZM9 9H6V12H9V9Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/trash.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.5 1C5.22386 1 5 1.22386 5 1.5C5 1.77614 5.22386 2 5.5 2H9.5C9.77614 2 10 1.77614 10 1.5C10 1.22386 9.77614 1 9.5 1H5.5ZM3 3.5C3 3.22386 3.22386 3 3.5 3H5H10H11.5C11.7761 3 12 3.22386 12 3.5C12 3.77614 11.7761 4 11.5 4H11V12C11 12.5523 10.5523 13 10 13H5C4.44772 13 4 12.5523 4 12V4L3.5 4C3.22386 4 3 3.77614 3 3.5ZM5 4H10V12H5V4Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/triangle-down.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M4 6H11L7.5 10.5L4 6Z" fill="currentColor" />
+</svg>

assets/icons/radix/triangle-left.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M9 4L9 11L4.5 7.5L9 4Z" fill="currentColor" />
+</svg>

assets/icons/radix/triangle-right.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M6 11L6 4L10.5 7.5L6 11Z" fill="currentColor" />
+</svg>

assets/icons/radix/triangle-up.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M4 9H11L7.5 4.5L4 9Z" fill="currentColor" />
+</svg>

assets/icons/radix/underline.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M5.00001 2.75C5.00001 2.47386 4.77615 2.25 4.50001 2.25C4.22387 2.25 4.00001 2.47386 4.00001 2.75V8.05C4.00001 9.983 5.56702 11.55 7.50001 11.55C9.43301 11.55 11 9.983 11 8.05V2.75C11 2.47386 10.7762 2.25 10.5 2.25C10.2239 2.25 10 2.47386 10 2.75V8.05C10 9.43071 8.88072 10.55 7.50001 10.55C6.1193 10.55 5.00001 9.43071 5.00001 8.05V2.75ZM3.49998 13.1001C3.27906 13.1001 3.09998 13.2791 3.09998 13.5001C3.09998 13.721 3.27906 13.9001 3.49998 13.9001H11.5C11.7209 13.9001 11.9 13.721 11.9 13.5001C11.9 13.2791 11.7209 13.1001 11.5 13.1001H3.49998Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/update.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/upload.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.81825 1.18188C7.64251 1.00615 7.35759 1.00615 7.18185 1.18188L4.18185 4.18188C4.00611 4.35762 4.00611 4.64254 4.18185 4.81828C4.35759 4.99401 4.64251 4.99401 4.81825 4.81828L7.05005 2.58648V9.49996C7.05005 9.74849 7.25152 9.94996 7.50005 9.94996C7.74858 9.94996 7.95005 9.74849 7.95005 9.49996V2.58648L10.1819 4.81828C10.3576 4.99401 10.6425 4.99401 10.8182 4.81828C10.994 4.64254 10.994 4.35762 10.8182 4.18188L7.81825 1.18188ZM2.5 9.99997C2.77614 9.99997 3 10.2238 3 10.5V12C3 12.5538 3.44565 13 3.99635 13H11.0012C11.5529 13 12 12.5528 12 12V10.5C12 10.2238 12.2239 9.99997 12.5 9.99997C12.7761 9.99997 13 10.2238 13 10.5V12C13 13.104 12.1062 14 11.0012 14H3.99635C2.89019 14 2 13.103 2 12V10.5C2 10.2238 2.22386 9.99997 2.5 9.99997Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/value-none.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49985 0.877045C3.84216 0.877045 0.877014 3.84219 0.877014 7.49988C0.877014 9.1488 1.47963 10.657 2.47665 11.8162L1.64643 12.6464C1.45117 12.8417 1.45117 13.1583 1.64643 13.3535C1.8417 13.5488 2.15828 13.5488 2.35354 13.3535L3.18377 12.5233C4.34296 13.5202 5.85104 14.1227 7.49985 14.1227C11.1575 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 5.85107 13.5202 4.34299 12.5233 3.1838L13.3535 2.35354C13.5488 2.15827 13.5488 1.84169 13.3535 1.64643C13.1583 1.45117 12.8417 1.45117 12.6464 1.64643L11.8162 2.47668C10.657 1.47966 9.14877 0.877045 7.49985 0.877045ZM11.1422 3.15066C10.1567 2.32449 8.88639 1.82704 7.49985 1.82704C4.36683 1.82704 1.82701 4.36686 1.82701 7.49988C1.82701 8.88642 2.32446 10.1568 3.15063 11.1422L11.1422 3.15066ZM3.85776 11.8493C4.84317 12.6753 6.11343 13.1727 7.49985 13.1727C10.6328 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 6.11346 12.6753 4.8432 11.8493 3.85779L3.85776 11.8493Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/value.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M0.877075 7.49988C0.877075 3.84219 3.84222 0.877045 7.49991 0.877045C11.1576 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1576 14.1227 7.49991 14.1227C3.84222 14.1227 0.877075 11.1575 0.877075 7.49988ZM7.49991 1.82704C4.36689 1.82704 1.82708 4.36686 1.82708 7.49988C1.82708 10.6329 4.36689 13.1727 7.49991 13.1727C10.6329 13.1727 13.1727 10.6329 13.1727 7.49988C13.1727 4.36686 10.6329 1.82704 7.49991 1.82704Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/vercel-logo.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7.49998 1L6.92321 2.00307L1.17498 12L0.599976 13H1.7535H13.2464H14.4L13.825 12L8.07674 2.00307L7.49998 1ZM7.49998 3.00613L2.3285 12H12.6714L7.49998 3.00613Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/video.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"

assets/icons/radix/view-grid.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M7 2H1.5C1.22386 2 1 2.22386 1 2.5V7H7V2ZM8 2V7H14V2.5C14 2.22386 13.7761 2 13.5 2H8ZM7 8H1V12.5C1 12.7761 1.22386 13 1.5 13H7V8ZM8 13V8H14V12.5C14 12.7761 13.7761 13 13.5 13H8ZM1.5 1C0.671573 1 0 1.67157 0 2.5V12.5C0 13.3284 0.671573 14 1.5 14H13.5C14.3284 14 15 13.3284 15 12.5V2.5C15 1.67157 14.3284 1 13.5 1H1.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/view-horizontal.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M1.5 2H13.5C13.7761 2 14 2.22386 14 2.5V7H1V2.5C1 2.22386 1.22386 2 1.5 2ZM1 8V12.5C1 12.7761 1.22386 13 1.5 13H13.5C13.7761 13 14 12.7761 14 12.5V8H1ZM0 2.5C0 1.67157 0.671573 1 1.5 1H13.5C14.3284 1 15 1.67157 15 2.5V12.5C15 13.3284 14.3284 14 13.5 14H1.5C0.671573 14 0 13.3284 0 12.5V2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/view-none.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M14 2.58711L1.85163 13H13.5C13.7761 13 14 12.7761 14 12.5V2.58711ZM0.762879 13.8067L0.825396 13.8796L0.854717 13.8545C1.05017 13.9478 1.26899 14 1.5 14H13.5C14.3284 14 15 13.3284 15 12.5V2.5C15 1.93949 14.6926 1.45078 14.2371 1.19331L14.1746 1.12037L14.1453 1.1455C13.9498 1.05222 13.731 1 13.5 1H1.5C0.671573 1 0 1.67157 0 2.5V12.5C0 13.0605 0.307435 13.5492 0.762879 13.8067ZM1 12.4129L13.1484 2H1.5C1.22386 2 1 2.22386 1 2.5V12.4129Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/view-vertical.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M8 2H13.5C13.7761 2 14 2.22386 14 2.5V12.5C14 12.7761 13.7761 13 13.5 13H8V2ZM7 2H1.5C1.22386 2 1 2.22386 1 2.5V12.5C1 12.7761 1.22386 13 1.5 13H7V2ZM0 2.5C0 1.67157 0.671573 1 1.5 1H13.5C14.3284 1 15 1.67157 15 2.5V12.5C15 13.3284 14.3284 14 13.5 14H1.5C0.671573 14 0 13.3284 0 12.5V2.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/width.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M4.81812 4.68161C4.99386 4.85734 4.99386 5.14227 4.81812 5.318L3.08632 7.0498H11.9135L10.1817 5.318C10.006 5.14227 10.006 4.85734 10.1817 4.68161C10.3575 4.50587 10.6424 4.50587 10.8181 4.68161L13.3181 7.18161C13.4939 7.35734 13.4939 7.64227 13.3181 7.818L10.8181 10.318C10.6424 10.4937 10.3575 10.4937 10.1817 10.318C10.006 10.1423 10.006 9.85734 10.1817 9.68161L11.9135 7.9498H3.08632L4.81812 9.68161C4.99386 9.85734 4.99386 10.1423 4.81812 10.318C4.64239 10.4937 4.35746 10.4937 4.18173 10.318L1.68173 7.818C1.50599 7.64227 1.50599 7.35734 1.68173 7.18161L4.18173 4.68161C4.35746 4.50587 4.64239 4.50587 4.81812 4.68161Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/zoom-in.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M10 6.5C10 8.433 8.433 10 6.5 10C4.567 10 3 8.433 3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5ZM9.30884 10.0159C8.53901 10.6318 7.56251 11 6.5 11C4.01472 11 2 8.98528 2 6.5C2 4.01472 4.01472 2 6.5 2C8.98528 2 11 4.01472 11 6.5C11 7.56251 10.6318 8.53901 10.0159 9.30884L12.8536 12.1464C13.0488 12.3417 13.0488 12.6583 12.8536 12.8536C12.6583 13.0488 12.3417 13.0488 12.1464 12.8536L9.30884 10.0159ZM4.25 6.5C4.25 6.22386 4.47386 6 4.75 6H6V4.75C6 4.47386 6.22386 4.25 6.5 4.25C6.77614 4.25 7 4.47386 7 4.75V6H8.25C8.52614 6 8.75 6.22386 8.75 6.5C8.75 6.77614 8.52614 7 8.25 7H7V8.25C7 8.52614 6.77614 8.75 6.5 8.75C6.22386 8.75 6 8.52614 6 8.25V7H4.75C4.47386 7 4.25 6.77614 4.25 6.5Z"
+    fill="currentColor"
+  />
+</svg>

assets/icons/radix/zoom-out.svg πŸ”—

@@ -0,0 +1,8 @@
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    fill-rule="evenodd"
+    clip-rule="evenodd"
+    d="M6.5 10C8.433 10 10 8.433 10 6.5C10 4.567 8.433 3 6.5 3C4.567 3 3 4.567 3 6.5C3 8.433 4.567 10 6.5 10ZM6.5 11C7.56251 11 8.53901 10.6318 9.30884 10.0159L12.1464 12.8536C12.3417 13.0488 12.6583 13.0488 12.8536 12.8536C13.0488 12.6583 13.0488 12.3417 12.8536 12.1464L10.0159 9.30884C10.6318 8.53901 11 7.56251 11 6.5C11 4.01472 8.98528 2 6.5 2C4.01472 2 2 4.01472 2 6.5C2 8.98528 4.01472 11 6.5 11ZM4.75 6C4.47386 6 4.25 6.22386 4.25 6.5C4.25 6.77614 4.47386 7 4.75 7H8.25C8.52614 7 8.75 6.77614 8.75 6.5C8.75 6.22386 8.52614 6 8.25 6H4.75Z"
+    fill="currentColor"
+  />
+</svg>

assets/keymaps/atom.json πŸ”—

@@ -24,9 +24,7 @@
       ],
       "ctrl-shift-down": "editor::AddSelectionBelow",
       "ctrl-shift-up": "editor::AddSelectionAbove",
-      "cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
-      "cmd-shift-enter": "editor::NewlineAbove",
-      "cmd-enter": "editor::NewlineBelow"
+      "cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
     }
   },
   {
@@ -55,7 +53,40 @@
     "context": "Pane",
     "bindings": {
       "alt-cmd-/": "search::ToggleRegex",
-      "ctrl-0": "project_panel::ToggleFocus"
+      "ctrl-0": "project_panel::ToggleFocus",
+      "cmd-1": [
+        "pane::ActivateItem",
+        0
+      ],
+      "cmd-2": [
+        "pane::ActivateItem",
+        1
+      ],
+      "cmd-3": [
+        "pane::ActivateItem",
+        2
+      ],
+      "cmd-4": [
+        "pane::ActivateItem",
+        3
+      ],
+      "cmd-5": [
+        "pane::ActivateItem",
+        4
+      ],
+      "cmd-6": [
+        "pane::ActivateItem",
+        5
+      ],
+      "cmd-7": [
+        "pane::ActivateItem",
+        6
+      ],
+      "cmd-8": [
+        "pane::ActivateItem",
+        7
+      ],
+      "cmd-9": "pane::ActivateLastItem"
     }
   },
   {

assets/keymaps/default.json πŸ”—

@@ -40,7 +40,8 @@
       "cmd-o": "workspace::Open",
       "alt-cmd-o": "projects::OpenRecent",
       "ctrl-~": "workspace::NewTerminal",
-      "ctrl-`": "terminal_panel::ToggleFocus"
+      "ctrl-`": "terminal_panel::ToggleFocus",
+      "shift-escape": "workspace::ToggleZoom"
     }
   },
   {
@@ -197,10 +198,20 @@
     }
   },
   {
-    "context": "AssistantEditor > Editor",
+    "context": "AssistantPanel",
+    "bindings": {
+      "cmd-g": "search::SelectNextMatch",
+      "cmd-shift-g": "search::SelectPrevMatch"
+    }
+  },
+  {
+    "context": "ConversationEditor > Editor",
     "bindings": {
       "cmd-enter": "assistant::Assist",
-      "cmd->": "assistant::QuoteSelection"
+      "cmd-s": "workspace::Save",
+      "cmd->": "assistant::QuoteSelection",
+      "shift-enter": "assistant::Split",
+      "ctrl-r": "assistant::CycleMessageRole"
     }
   },
   {
@@ -232,8 +243,7 @@
       "cmd-shift-g": "search::SelectPrevMatch",
       "alt-cmd-c": "search::ToggleCaseSensitive",
       "alt-cmd-w": "search::ToggleWholeWord",
-      "alt-cmd-r": "search::ToggleRegex",
-      "shift-escape": "workspace::ToggleZoom"
+      "alt-cmd-r": "search::ToggleRegex"
     }
   },
   // Bindings from VS Code
@@ -398,6 +408,7 @@
       "cmd-shift-p": "command_palette::Toggle",
       "cmd-shift-m": "diagnostics::Deploy",
       "cmd-shift-e": "project_panel::ToggleFocus",
+      "cmd-?": "assistant::ToggleFocus",
       "cmd-alt-s": "workspace::SaveAll",
       "cmd-k m": "language_selector::Toggle"
     }
@@ -409,6 +420,7 @@
       "ctrl-shift-k": "editor::DeleteLine",
       "cmd-shift-d": "editor::DuplicateLine",
       "cmd-shift-l": "editor::SplitSelectionIntoLines",
+      "ctrl-j": "editor::JoinLines",
       "ctrl-cmd-up": "editor::MoveLineUp",
       "ctrl-cmd-down": "editor::MoveLineDown",
       "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",

assets/keymaps/sublime_text.json πŸ”—

@@ -24,9 +24,7 @@
       "ctrl-.": "editor::GoToHunk",
       "ctrl-,": "editor::GoToPrevHunk",
       "ctrl-backspace": "editor::DeleteToPreviousWordStart",
-      "ctrl-delete": "editor::DeleteToNextWordEnd",
-      "cmd-shift-enter": "editor::NewlineAbove",
-      "cmd-enter": "editor::NewlineBelow"
+      "ctrl-delete": "editor::DeleteToNextWordEnd"
     }
   },
   {

assets/keymaps/textmate.json πŸ”—

@@ -12,8 +12,6 @@
       "ctrl-shift-d": "editor::DuplicateLine",
       "cmd-b": "editor::GoToDefinition",
       "cmd-j": "editor::ScrollCursorCenter",
-      "cmd-alt-enter": "editor::NewlineAbove",
-      "cmd-enter": "editor::NewlineBelow",
       "cmd-shift-l": "editor::SelectLine",
       "cmd-shift-t": "outline::Toggle",
       "alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -56,7 +54,9 @@
   },
   {
     "context": "Editor && mode == full",
-    "bindings": {}
+    "bindings": {
+      "cmd-alt-enter": "editor::NewlineAbove"
+    }
   },
   {
     "context": "BufferSearchBar",

assets/keymaps/vim.json πŸ”—

@@ -1,6 +1,6 @@
 [
   {
-    "context": "Editor && VimControl && !VimWaiting",
+    "context": "Editor && VimControl && !VimWaiting && !menu",
     "bindings": {
       "g": [
         "vim::PushOperator",
@@ -25,11 +25,15 @@
         }
       ],
       "h": "vim::Left",
+      "left": "vim::Left",
       "backspace": "vim::Backspace",
       "j": "vim::Down",
+      "down": "vim::Down",
       "enter": "vim::NextLineStart",
       "k": "vim::Up",
+      "up": "vim::Up",
       "l": "vim::Right",
+      "right": "vim::Right",
       "$": "vim::EndOfLine",
       "shift-g": "vim::EndOfDocument",
       "w": "vim::NextWordStart",
@@ -54,10 +58,6 @@
         }
       ],
       "%": "vim::Matching",
-      "ctrl-y": [
-        "vim::Scroll",
-        "LineUp"
-      ],
       "f": [
         "vim::PushOperator",
         {
@@ -90,6 +90,8 @@
           }
         }
       ],
+      "ctrl-o": "pane::GoBack",
+      "ctrl-]": "editor::GoToDefinition",
       "escape": "editor::Cancel",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
       "1": [
@@ -131,7 +133,7 @@
     }
   },
   {
-    "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
+    "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
     "bindings": {
       "c": [
         "vim::PushOperator",
@@ -143,6 +145,7 @@
         "Delete"
       ],
       "shift-d": "vim::DeleteToEndOfLine",
+      "shift-j": "editor::JoinLines",
       "y": [
         "vim::PushOperator",
         "Yank"
@@ -165,6 +168,7 @@
       "^": "vim::FirstNonWhitespace",
       "o": "vim::InsertLineBelow",
       "shift-o": "vim::InsertLineAbove",
+      "~": "vim::ChangeCase",
       "v": [
         "vim::SwitchMode",
         {
@@ -184,37 +188,29 @@
       "p": "vim::Paste",
       "u": "editor::Undo",
       "ctrl-r": "editor::Redo",
-      "ctrl-o": "pane::GoBack",
       "/": [
         "buffer_search::Deploy",
         {
           "focus": true
         }
       ],
-      "ctrl-f": [
-        "vim::Scroll",
-        "PageDown"
-      ],
-      "ctrl-b": [
-        "vim::Scroll",
-        "PageUp"
-      ],
-      "ctrl-d": [
-        "vim::Scroll",
-        "HalfPageDown"
-      ],
-      "ctrl-u": [
-        "vim::Scroll",
-        "HalfPageUp"
-      ],
-      "ctrl-e": [
-        "vim::Scroll",
-        "LineDown"
-      ],
+      "ctrl-f": "vim::PageDown",
+      "pagedown": "vim::PageDown",
+      "ctrl-b": "vim::PageUp",
+      "pageup": "vim::PageUp",
+      "ctrl-d": "vim::ScrollDown",
+      "ctrl-u": "vim::ScrollUp",
+      "ctrl-e": "vim::LineDown",
+      "ctrl-y": "vim::LineUp",
       "r": [
         "vim::PushOperator",
         "Replace"
-      ]
+      ],
+      "s": "vim::Substitute",
+      "> >": "editor::Indent",
+      "< <": "editor::Outdent",
+      "ctrl-pagedown": "pane::ActivateNextItem",
+      "ctrl-pageup": "pane::ActivatePrevItem"
     }
   },
   {
@@ -231,6 +227,8 @@
     "bindings": {
       "g": "vim::StartOfDocument",
       "h": "editor::Hover",
+      "t": "pane::ActivateNextItem",
+      "shift-t": "pane::ActivatePrevItem",
       "escape": [
         "vim::SwitchMode",
         "Normal"
@@ -301,10 +299,14 @@
       "x": "vim::VisualDelete",
       "y": "vim::VisualYank",
       "p": "vim::VisualPaste",
+      "s": "vim::Substitute",
+      "~": "vim::ChangeCase",
       "r": [
         "vim::PushOperator",
         "Replace"
-      ]
+      ],
+      "> >": "editor::Indent",
+      "< <": "editor::Outdent"
     }
   },
   {

assets/settings/default.json πŸ”—

@@ -57,39 +57,49 @@
   "show_whitespaces": "selection",
   // Scrollbar related settings
   "scrollbar": {
-      // When to show the scrollbar in the editor.
-      // This setting can take four values:
-      //
-      // 1. Show the scrollbar if there's important information or
-      //    follow the system's configured behavior (default):
-      //   "auto"
-      // 2. Match the system's configured behavior:
-      //    "system"
-      // 3. Always show the scrollbar:
-      //    "always"
-      // 4. Never show the scrollbar:
-      //    "never"
-      "show": "auto",
-      // Whether to show git diff indicators in the scrollbar.
-      "git_diff": true,
-      // Whether to show selections in the scrollbar.
-      "selections": true
+    // When to show the scrollbar in the editor.
+    // This setting can take four values:
+    //
+    // 1. Show the scrollbar if there's important information or
+    //    follow the system's configured behavior (default):
+    //   "auto"
+    // 2. Match the system's configured behavior:
+    //    "system"
+    // 3. Always show the scrollbar:
+    //    "always"
+    // 4. Never show the scrollbar:
+    //    "never"
+    "show": "auto",
+    // Whether to show git diff indicators in the scrollbar.
+    "git_diff": true,
+    // Whether to show selections in the scrollbar.
+    "selections": true
+  },
+  // Inlay hint related settings
+  "inlay_hints": {
+    // Global switch to toggle hints on and off, switched off by default.
+    "enabled": false,
+    // Toggle certain types of hints on and off, all switched on by default.
+    "show_type_hints": true,
+    "show_parameter_hints": true,
+    // Corresponds to null/None LSP hint type value.
+    "show_other_hints": true
   },
   "project_panel": {
-      // Whether to show the git status in the project panel.
-      "git_status": true,
-      // Where to dock project panel. Can be 'left' or 'right'.
-      "dock": "left",
-      // Default width of the project panel.
-      "default_width": 240
+    // Whether to show the git status in the project panel.
+    "git_status": true,
+    // Where to dock project panel. Can be 'left' or 'right'.
+    "dock": "left",
+    // Default width of the project panel.
+    "default_width": 240
   },
   "assistant": {
-      // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
-      "dock": "right",
-      // Default width when the assistant is docked to the left or right.
-      "default_width": 450,
-      // Default height when the assistant is docked to the bottom.
-      "default_height": 320
+    // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
+    "dock": "right",
+    // Default width when the assistant is docked to the left or right.
+    "default_width": 640,
+    // Default height when the assistant is docked to the bottom.
+    "default_height": 320
   },
   // Whether the screen sharing icon is shown in the os status bar.
   "show_call_status_icon": true,

crates/activity_indicator/src/activity_indicator.rs πŸ”—

@@ -207,16 +207,11 @@ impl ActivityIndicator {
         let mut checking_for_update = SmallVec::<[_; 3]>::new();
         let mut failed = SmallVec::<[_; 3]>::new();
         for status in &self.statuses {
+            let name = status.name.clone();
             match status.status {
-                LanguageServerBinaryStatus::CheckingForUpdate => {
-                    checking_for_update.push(status.name.clone());
-                }
-                LanguageServerBinaryStatus::Downloading => {
-                    downloading.push(status.name.clone());
-                }
-                LanguageServerBinaryStatus::Failed { .. } => {
-                    failed.push(status.name.clone());
-                }
+                LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
+                LanguageServerBinaryStatus::Downloading => downloading.push(name),
+                LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
                 LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
             }
         }
@@ -326,7 +321,7 @@ impl View for ActivityIndicator {
         let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
             let theme = &theme::current(cx).workspace.status_bar.lsp_status;
             let style = if state.hovered() && on_click.is_some() {
-                theme.hover.as_ref().unwrap_or(&theme.default)
+                theme.hovered.as_ref().unwrap_or(&theme.default)
             } else {
                 &theme.default
             };

crates/ai/Cargo.toml πŸ”—

@@ -22,13 +22,16 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 
 anyhow.workspace = true
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
 futures.workspace = true
 isahc.workspace = true
+regex.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+smol.workspace = true
 tiktoken-rs = "0.4"
 
 [dev-dependencies]
 editor = { path = "../editor", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }

crates/ai/src/ai.rs πŸ”—

@@ -1,19 +1,109 @@
 pub mod assistant;
 mod assistant_settings;
 
+use anyhow::Result;
 pub use assistant::AssistantPanel;
+use chrono::{DateTime, Local};
+use collections::HashMap;
+use fs::Fs;
+use futures::StreamExt;
 use gpui::AppContext;
+use regex::Regex;
 use serde::{Deserialize, Serialize};
-use std::fmt::{self, Display};
+use std::{
+    cmp::Reverse,
+    fmt::{self, Display},
+    path::PathBuf,
+    sync::Arc,
+};
+use util::paths::CONVERSATIONS_DIR;
 
 // Data types for chat completion requests
-#[derive(Serialize)]
+#[derive(Debug, Serialize)]
 struct OpenAIRequest {
     model: String,
     messages: Vec<RequestMessage>,
     stream: bool,
 }
 
+#[derive(
+    Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
+)]
+struct MessageId(usize);
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+struct MessageMetadata {
+    role: Role,
+    sent_at: DateTime<Local>,
+    status: MessageStatus,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+enum MessageStatus {
+    Pending,
+    Done,
+    Error(Arc<str>),
+}
+
+#[derive(Serialize, Deserialize)]
+struct SavedMessage {
+    id: MessageId,
+    start: usize,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SavedConversation {
+    zed: String,
+    version: String,
+    text: String,
+    messages: Vec<SavedMessage>,
+    message_metadata: HashMap<MessageId, MessageMetadata>,
+    summary: String,
+    model: String,
+}
+
+impl SavedConversation {
+    const VERSION: &'static str = "0.1.0";
+}
+
+struct SavedConversationMetadata {
+    title: String,
+    path: PathBuf,
+    mtime: chrono::DateTime<chrono::Local>,
+}
+
+impl SavedConversationMetadata {
+    pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
+        fs.create_dir(&CONVERSATIONS_DIR).await?;
+
+        let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
+        let mut conversations = Vec::<SavedConversationMetadata>::new();
+        while let Some(path) = paths.next().await {
+            let path = path?;
+
+            let pattern = r" - \d+.zed.json$";
+            let re = Regex::new(pattern).unwrap();
+
+            let metadata = fs.metadata(&path).await?;
+            if let Some((file_name, metadata)) = path
+                .file_name()
+                .and_then(|name| name.to_str())
+                .zip(metadata)
+            {
+                let title = re.replace(file_name, "");
+                conversations.push(Self {
+                    title: title.into_owned(),
+                    path,
+                    mtime: metadata.mtime.into(),
+                });
+            }
+        }
+        conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
+
+        Ok(conversations)
+    }
+}
+
 #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
 struct RequestMessage {
     role: Role,

crates/ai/src/assistant.rs πŸ”—

@@ -1,6 +1,7 @@
 use crate::{
     assistant_settings::{AssistantDockPosition, AssistantSettings},
-    OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role,
+    MessageId, MessageMetadata, MessageStatus, OpenAIRequest, OpenAIResponseStreamEvent,
+    RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
@@ -8,7 +9,7 @@ use collections::{HashMap, HashSet};
 use editor::{
     display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint},
     scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
-    Anchor, Editor, ToOffset as _,
+    Anchor, Editor, ToOffset,
 };
 use fs::Fs;
 use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
@@ -23,49 +24,66 @@ use gpui::{
 };
 use isahc::{http::StatusCode, Request, RequestExt};
 use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
+use search::BufferSearchBar;
 use serde::Deserialize;
 use settings::SettingsStore;
 use std::{
-    borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc,
+    cell::RefCell,
+    cmp, env,
+    fmt::Write,
+    io, iter,
+    ops::Range,
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::Arc,
     time::Duration,
 };
-use util::{channel::ReleaseChannel, post_inc, truncate_and_trailoff, ResultExt, TryFutureExt};
+use theme::AssistantStyle;
+use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
-    item::Item,
-    pane, Pane, Workspace,
+    searchable::Direction,
+    Save, ToggleZoom, Toolbar, Workspace,
 };
 
 const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
 
 actions!(
     assistant,
-    [NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey]
+    [
+        NewConversation,
+        Assist,
+        Split,
+        CycleMessageRole,
+        QuoteSelection,
+        ToggleFocus,
+        ResetKey,
+    ]
 );
 
 pub fn init(cx: &mut AppContext) {
-    if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
-        cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
-            filter.filtered_namespaces.insert("assistant");
-        });
-    }
-
     settings::register::<AssistantSettings>(cx);
     cx.add_action(
-        |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
-            if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
-                this.update(cx, |this, cx| this.add_context(cx))
-            }
-
-            workspace.focus_panel::<AssistantPanel>(cx);
+        |this: &mut AssistantPanel,
+         _: &workspace::NewFile,
+         cx: &mut ViewContext<AssistantPanel>| {
+            this.new_conversation(cx);
         },
     );
-    cx.add_action(AssistantEditor::assist);
-    cx.capture_action(AssistantEditor::cancel_last_assist);
-    cx.add_action(AssistantEditor::quote_selection);
-    cx.capture_action(AssistantEditor::copy);
+    cx.add_action(ConversationEditor::assist);
+    cx.capture_action(ConversationEditor::cancel_last_assist);
+    cx.capture_action(ConversationEditor::save);
+    cx.add_action(ConversationEditor::quote_selection);
+    cx.capture_action(ConversationEditor::copy);
+    cx.add_action(ConversationEditor::split);
+    cx.capture_action(ConversationEditor::cycle_message_role);
     cx.add_action(AssistantPanel::save_api_key);
     cx.add_action(AssistantPanel::reset_api_key);
+    cx.add_action(AssistantPanel::toggle_zoom);
+    cx.add_action(AssistantPanel::deploy);
+    cx.add_action(AssistantPanel::select_next_match);
+    cx.add_action(AssistantPanel::select_prev_match);
+    cx.add_action(AssistantPanel::handle_editor_cancel);
     cx.add_action(
         |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
             workspace.toggle_panel_focus::<AssistantPanel>(cx);
@@ -73,6 +91,7 @@ pub fn init(cx: &mut AppContext) {
     );
 }
 
+#[derive(Debug)]
 pub enum AssistantPanelEvent {
     ZoomIn,
     ZoomOut,
@@ -82,15 +101,24 @@ pub enum AssistantPanelEvent {
 }
 
 pub struct AssistantPanel {
+    workspace: WeakViewHandle<Workspace>,
     width: Option<f32>,
     height: Option<f32>,
-    pane: ViewHandle<Pane>,
+    active_editor_index: Option<usize>,
+    prev_active_editor_index: Option<usize>,
+    editors: Vec<ViewHandle<ConversationEditor>>,
+    saved_conversations: Vec<SavedConversationMetadata>,
+    saved_conversations_list_state: UniformListState,
+    zoomed: bool,
+    has_focus: bool,
+    toolbar: ViewHandle<Toolbar>,
     api_key: Rc<RefCell<Option<String>>>,
     api_key_editor: Option<ViewHandle<Editor>>,
     has_read_credentials: bool,
     languages: Arc<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     subscriptions: Vec<Subscription>,
+    _watch_saved_conversations: Task<Result<()>>,
 }
 
 impl AssistantPanel {
@@ -99,66 +127,52 @@ impl AssistantPanel {
         cx: AsyncAppContext,
     ) -> Task<Result<ViewHandle<Self>>> {
         cx.spawn(|mut cx| async move {
+            let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
+            let saved_conversations = SavedConversationMetadata::list(fs.clone())
+                .await
+                .log_err()
+                .unwrap_or_default();
+
             // TODO: deserialize state.
+            let workspace_handle = workspace.clone();
             workspace.update(&mut cx, |workspace, cx| {
                 cx.add_view::<Self, _>(|cx| {
-                    let weak_self = cx.weak_handle();
-                    let pane = cx.add_view(|cx| {
-                        let mut pane = Pane::new(
-                            workspace.weak_handle(),
-                            workspace.project().clone(),
-                            workspace.app_state().background_actions,
-                            Default::default(),
-                            cx,
-                        );
-                        pane.set_can_split(false, cx);
-                        pane.set_can_navigate(false, cx);
-                        pane.on_can_drop(move |_, _| false);
-                        pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
-                            let weak_self = weak_self.clone();
-                            Flex::row()
-                                .with_child(Pane::render_tab_bar_button(
-                                    0,
-                                    "icons/plus_12.svg",
-                                    false,
-                                    Some(("New Context".into(), Some(Box::new(NewContext)))),
-                                    cx,
-                                    move |_, cx| {
-                                        let weak_self = weak_self.clone();
-                                        cx.window_context().defer(move |cx| {
-                                            if let Some(this) = weak_self.upgrade(cx) {
-                                                this.update(cx, |this, cx| this.add_context(cx));
-                                            }
-                                        })
-                                    },
-                                    None,
-                                ))
-                                .with_child(Pane::render_tab_bar_button(
-                                    1,
-                                    if pane.is_zoomed() {
-                                        "icons/minimize_8.svg"
-                                    } else {
-                                        "icons/maximize_8.svg"
-                                    },
-                                    pane.is_zoomed(),
-                                    Some((
-                                        "Toggle Zoom".into(),
-                                        Some(Box::new(workspace::ToggleZoom)),
-                                    )),
-                                    cx,
-                                    move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
-                                    None,
-                                ))
-                                .into_any()
-                        });
-                        let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
-                        pane.toolbar()
-                            .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
-                        pane
+                    const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
+                    let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
+                        let mut events = fs
+                            .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
+                            .await;
+                        while events.next().await.is_some() {
+                            let saved_conversations = SavedConversationMetadata::list(fs.clone())
+                                .await
+                                .log_err()
+                                .unwrap_or_default();
+                            this.update(&mut cx, |this, cx| {
+                                this.saved_conversations = saved_conversations;
+                                cx.notify();
+                            })
+                            .ok();
+                        }
+
+                        anyhow::Ok(())
                     });
 
+                    let toolbar = cx.add_view(|cx| {
+                        let mut toolbar = Toolbar::new(None);
+                        toolbar.set_can_navigate(false, cx);
+                        toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
+                        toolbar
+                    });
                     let mut this = Self {
-                        pane,
+                        workspace: workspace_handle,
+                        active_editor_index: Default::default(),
+                        prev_active_editor_index: Default::default(),
+                        editors: Default::default(),
+                        saved_conversations,
+                        saved_conversations_list_state: Default::default(),
+                        zoomed: false,
+                        has_focus: false,
+                        toolbar,
                         api_key: Rc::new(RefCell::new(None)),
                         api_key_editor: None,
                         has_read_credentials: false,
@@ -167,20 +181,18 @@ impl AssistantPanel {
                         width: None,
                         height: None,
                         subscriptions: Default::default(),
+                        _watch_saved_conversations,
                     };
 
                     let mut old_dock_position = this.position(cx);
-                    this.subscriptions = vec![
-                        cx.observe(&this.pane, |_, _, cx| cx.notify()),
-                        cx.subscribe(&this.pane, Self::handle_pane_event),
-                        cx.observe_global::<SettingsStore, _>(move |this, cx| {
+                    this.subscriptions =
+                        vec![cx.observe_global::<SettingsStore, _>(move |this, cx| {
                             let new_dock_position = this.position(cx);
                             if new_dock_position != old_dock_position {
                                 old_dock_position = new_dock_position;
                                 cx.emit(AssistantPanelEvent::DockPositionChanged);
                             }
-                        }),
-                    ];
+                        })];
 
                     this
                 })
@@ -188,40 +200,64 @@ impl AssistantPanel {
         })
     }
 
-    fn handle_pane_event(
+    fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<ConversationEditor> {
+        let editor = cx.add_view(|cx| {
+            ConversationEditor::new(
+                self.api_key.clone(),
+                self.languages.clone(),
+                self.fs.clone(),
+                cx,
+            )
+        });
+        self.add_conversation(editor.clone(), cx);
+        editor
+    }
+
+    fn add_conversation(
         &mut self,
-        _pane: ViewHandle<Pane>,
-        event: &pane::Event,
+        editor: ViewHandle<ConversationEditor>,
         cx: &mut ViewContext<Self>,
     ) {
-        match event {
-            pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn),
-            pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut),
-            pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus),
-            pane::Event::Remove => cx.emit(AssistantPanelEvent::Close),
-            _ => {}
-        }
-    }
+        self.subscriptions
+            .push(cx.subscribe(&editor, Self::handle_conversation_editor_event));
 
-    fn add_context(&mut self, cx: &mut ViewContext<Self>) {
-        let focus = self.has_focus(cx);
-        let editor = cx
-            .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx));
+        let conversation = editor.read(cx).conversation.clone();
         self.subscriptions
-            .push(cx.subscribe(&editor, Self::handle_assistant_editor_event));
-        self.pane.update(cx, |pane, cx| {
-            pane.add_item(Box::new(editor), true, focus, None, cx)
-        });
+            .push(cx.observe(&conversation, |_, _, cx| cx.notify()));
+
+        let index = self.editors.len();
+        self.editors.push(editor);
+        self.set_active_editor_index(Some(index), cx);
     }
 
-    fn handle_assistant_editor_event(
+    fn set_active_editor_index(&mut self, index: Option<usize>, cx: &mut ViewContext<Self>) {
+        self.prev_active_editor_index = self.active_editor_index;
+        self.active_editor_index = index;
+        if let Some(editor) = self.active_editor() {
+            let editor = editor.read(cx).editor.clone();
+            self.toolbar.update(cx, |toolbar, cx| {
+                toolbar.set_active_item(Some(&editor), cx);
+            });
+            if self.has_focus(cx) {
+                cx.focus(&editor);
+            }
+        } else {
+            self.toolbar.update(cx, |toolbar, cx| {
+                toolbar.set_active_item(None, cx);
+            });
+        }
+
+        cx.notify();
+    }
+
+    fn handle_conversation_editor_event(
         &mut self,
-        _: ViewHandle<AssistantEditor>,
-        event: &AssistantEditorEvent,
+        _: ViewHandle<ConversationEditor>,
+        event: &ConversationEditorEvent,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()),
+            ConversationEditorEvent::TabContentChanged => cx.notify(),
         }
     }
 
@@ -252,6 +288,287 @@ impl AssistantPanel {
         cx.focus_self();
         cx.notify();
     }
+
+    fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
+        if self.zoomed {
+            cx.emit(AssistantPanelEvent::ZoomOut)
+        } else {
+            cx.emit(AssistantPanelEvent::ZoomIn)
+        }
+    }
+
+    fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
+        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
+            if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
+                return;
+            }
+        }
+        cx.propagate_action();
+    }
+
+    fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
+        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
+            if !search_bar.read(cx).is_dismissed() {
+                search_bar.update(cx, |search_bar, cx| {
+                    search_bar.dismiss(&Default::default(), cx)
+                });
+                return;
+            }
+        }
+        cx.propagate_action();
+    }
+
+    fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
+        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx));
+        }
+    }
+
+    fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
+        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx));
+        }
+    }
+
+    fn active_editor(&self) -> Option<&ViewHandle<ConversationEditor>> {
+        self.editors.get(self.active_editor_index?)
+    }
+
+    fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        enum History {}
+        let theme = theme::current(cx);
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        MouseEventHandler::<History, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.hamburger_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if this.active_editor().is_some() {
+                this.set_active_editor_index(None, cx);
+            } else {
+                this.set_active_editor_index(this.prev_active_editor_index, cx);
+            }
+        })
+        .with_tooltip::<History>(1, "History".into(), None, tooltip_style, cx)
+    }
+
+    fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement<Self>> {
+        if self.active_editor().is_some() {
+            vec![
+                Self::render_split_button(cx).into_any(),
+                Self::render_quote_button(cx).into_any(),
+                Self::render_assist_button(cx).into_any(),
+            ]
+        } else {
+            Default::default()
+        }
+    }
+
+    fn render_split_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        MouseEventHandler::<Split, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.split_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if let Some(active_editor) = this.active_editor() {
+                active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
+            }
+        })
+        .with_tooltip::<Split>(
+            1,
+            "Split Message".into(),
+            Some(Box::new(Split)),
+            tooltip_style,
+            cx,
+        )
+    }
+
+    fn render_assist_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        MouseEventHandler::<Assist, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.assist_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if let Some(active_editor) = this.active_editor() {
+                active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
+            }
+        })
+        .with_tooltip::<Assist>(
+            1,
+            "Assist".into(),
+            Some(Box::new(Assist)),
+            tooltip_style,
+            cx,
+        )
+    }
+
+    fn render_quote_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        MouseEventHandler::<QuoteSelection, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.quote_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            if let Some(workspace) = this.workspace.upgrade(cx) {
+                cx.window_context().defer(move |cx| {
+                    workspace.update(cx, |workspace, cx| {
+                        ConversationEditor::quote_selection(workspace, &Default::default(), cx)
+                    });
+                });
+            }
+        })
+        .with_tooltip::<QuoteSelection>(
+            1,
+            "Quote Selection".into(),
+            Some(Box::new(QuoteSelection)),
+            tooltip_style,
+            cx,
+        )
+    }
+
+    fn render_plus_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        let theme = theme::current(cx);
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        MouseEventHandler::<NewConversation, _>::new(0, cx, |state, _| {
+            let style = theme.assistant.plus_button.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
+            this.new_conversation(cx);
+        })
+        .with_tooltip::<NewConversation>(
+            1,
+            "New Conversation".into(),
+            Some(Box::new(NewConversation)),
+            tooltip_style,
+            cx,
+        )
+    }
+
+    fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
+        enum ToggleZoomButton {}
+
+        let theme = theme::current(cx);
+        let tooltip_style = theme::current(cx).tooltip.clone();
+        let style = if self.zoomed {
+            &theme.assistant.zoom_out_button
+        } else {
+            &theme.assistant.zoom_in_button
+        };
+
+        MouseEventHandler::<ToggleZoomButton, _>::new(0, cx, |state, _| {
+            let style = style.style_for(state);
+            Svg::for_style(style.icon.clone())
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, |_, this, cx| {
+            this.toggle_zoom(&ToggleZoom, cx);
+        })
+        .with_tooltip::<ToggleZoom>(
+            0,
+            if self.zoomed {
+                "Zoom Out".into()
+            } else {
+                "Zoom In".into()
+            },
+            Some(Box::new(ToggleZoom)),
+            tooltip_style,
+            cx,
+        )
+    }
+
+    fn render_saved_conversation(
+        &mut self,
+        index: usize,
+        cx: &mut ViewContext<Self>,
+    ) -> impl Element<Self> {
+        let conversation = &self.saved_conversations[index];
+        let path = conversation.path.clone();
+        MouseEventHandler::<SavedConversationMetadata, _>::new(index, cx, move |state, cx| {
+            let style = &theme::current(cx).assistant.saved_conversation;
+            Flex::row()
+                .with_child(
+                    Label::new(
+                        conversation.mtime.format("%F %I:%M%p").to_string(),
+                        style.saved_at.text.clone(),
+                    )
+                    .aligned()
+                    .contained()
+                    .with_style(style.saved_at.container),
+                )
+                .with_child(
+                    Label::new(conversation.title.clone(), style.title.text.clone())
+                        .aligned()
+                        .contained()
+                        .with_style(style.title.container),
+                )
+                .contained()
+                .with_style(*style.container.style_for(state))
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            this.open_conversation(path.clone(), cx)
+                .detach_and_log_err(cx)
+        })
+    }
+
+    fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+        if let Some(ix) = self.editor_index_for_path(&path, cx) {
+            self.set_active_editor_index(Some(ix), cx);
+            return Task::ready(Ok(()));
+        }
+
+        let fs = self.fs.clone();
+        let api_key = self.api_key.clone();
+        let languages = self.languages.clone();
+        cx.spawn(|this, mut cx| async move {
+            let saved_conversation = fs.load(&path).await?;
+            let saved_conversation = serde_json::from_str(&saved_conversation)?;
+            let conversation = cx.add_model(|cx| {
+                Conversation::deserialize(saved_conversation, path.clone(), api_key, languages, cx)
+            });
+            this.update(&mut cx, |this, cx| {
+                // If, by the time we've loaded the conversation, the user has already opened
+                // the same conversation, we don't want to open it again.
+                if let Some(ix) = this.editor_index_for_path(&path, cx) {
+                    this.set_active_editor_index(Some(ix), cx);
+                } else {
+                    let editor = cx
+                        .add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx));
+                    this.add_conversation(editor, cx);
+                }
+            })?;
+            Ok(())
+        })
+    }
+
+    fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option<usize> {
+        self.editors
+            .iter()
+            .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path))
+    }
 }
 
 fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
@@ -275,7 +592,8 @@ impl View for AssistantPanel {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let style = &theme::current(cx).assistant;
+        let theme = &theme::current(cx);
+        let style = &theme.assistant;
         if let Some(api_key_editor) = self.api_key_editor.as_ref() {
             Flex::column()
                 .with_child(
@@ -296,19 +614,81 @@ impl View for AssistantPanel {
                 .aligned()
                 .into_any()
         } else {
-            ChildView::new(&self.pane, cx).into_any()
+            let title = self.active_editor().map(|editor| {
+                Label::new(editor.read(cx).title(cx), style.title.text.clone())
+                    .contained()
+                    .with_style(style.title.container)
+                    .aligned()
+                    .left()
+                    .flex(1., false)
+            });
+            let mut header = Flex::row()
+                .with_child(Self::render_hamburger_button(cx).aligned())
+                .with_children(title);
+            if self.has_focus {
+                header.add_children(
+                    self.render_editor_tools(cx)
+                        .into_iter()
+                        .map(|tool| tool.aligned().flex_float()),
+                );
+                header.add_child(Self::render_plus_button(cx).aligned().flex_float());
+                header.add_child(self.render_zoom_button(cx).aligned());
+            }
+
+            Flex::column()
+                .with_child(
+                    header
+                        .contained()
+                        .with_style(theme.workspace.tab_bar.container)
+                        .expanded()
+                        .constrained()
+                        .with_height(theme.workspace.tab_bar.height),
+                )
+                .with_children(if self.toolbar.read(cx).hidden() {
+                    None
+                } else {
+                    Some(ChildView::new(&self.toolbar, cx).expanded())
+                })
+                .with_child(if let Some(editor) = self.active_editor() {
+                    ChildView::new(editor, cx).flex(1., true).into_any()
+                } else {
+                    UniformList::new(
+                        self.saved_conversations_list_state.clone(),
+                        self.saved_conversations.len(),
+                        cx,
+                        |this, range, items, cx| {
+                            for ix in range {
+                                items.push(this.render_saved_conversation(ix, cx).into_any());
+                            }
+                        },
+                    )
+                    .flex(1., true)
+                    .into_any()
+                })
+                .into_any()
         }
     }
 
     fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
+        self.toolbar
+            .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
+        cx.notify();
         if cx.is_self_focused() {
-            if let Some(api_key_editor) = self.api_key_editor.as_ref() {
+            if let Some(editor) = self.active_editor() {
+                cx.focus(editor);
+            } else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
                 cx.focus(api_key_editor);
-            } else {
-                cx.focus(&self.pane);
             }
         }
     }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = false;
+        self.toolbar
+            .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
+        cx.notify();
+    }
 }
 
 impl Panel for AssistantPanel {
@@ -361,19 +741,22 @@ impl Panel for AssistantPanel {
         matches!(event, AssistantPanelEvent::ZoomOut)
     }
 
-    fn is_zoomed(&self, cx: &WindowContext) -> bool {
-        self.pane.read(cx).is_zoomed()
+    fn is_zoomed(&self, _: &WindowContext) -> bool {
+        self.zoomed
     }
 
     fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
-        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
+        self.zoomed = zoomed;
+        cx.notify();
     }
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active {
             if self.api_key.borrow().is_none() && !self.has_read_credentials {
                 self.has_read_credentials = true;
-                let api_key = if let Some((_, api_key)) = cx
+                let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
+                    Some(api_key)
+                } else if let Some((_, api_key)) = cx
                     .platform()
                     .read_credentials(OPENAI_API_URL)
                     .log_err()
@@ -391,8 +774,8 @@ impl Panel for AssistantPanel {
                 }
             }
 
-            if self.pane.read(cx).items_len() == 0 {
-                self.add_context(cx);
+            if self.editors.is_empty() {
+                self.new_conversation(cx);
             }
         }
     }
@@ -417,12 +800,8 @@ impl Panel for AssistantPanel {
         matches!(event, AssistantPanelEvent::Close)
     }
 
-    fn has_focus(&self, cx: &WindowContext) -> bool {
-        self.pane.read(cx).has_focus()
-            || self
-                .api_key_editor
-                .as_ref()
-                .map_or(false, |editor| editor.is_focused(cx))
+    fn has_focus(&self, _: &WindowContext) -> bool {
+        self.has_focus
     }
 
     fn is_focus_event(event: &Self::Event) -> bool {
@@ -430,18 +809,24 @@ impl Panel for AssistantPanel {
     }
 }
 
-enum AssistantEvent {
+enum ConversationEvent {
     MessagesEdited,
     SummaryChanged,
     StreamedCompletion,
 }
 
-struct Assistant {
+#[derive(Default)]
+struct Summary {
+    text: String,
+    done: bool,
+}
+
+struct Conversation {
     buffer: ModelHandle<Buffer>,
-    messages: Vec<Message>,
+    message_anchors: Vec<MessageAnchor>,
     messages_metadata: HashMap<MessageId, MessageMetadata>,
     next_message_id: MessageId,
-    summary: Option<String>,
+    summary: Option<Summary>,
     pending_summary: Task<Option<()>>,
     completion_count: usize,
     pending_completions: Vec<PendingCompletion>,
@@ -450,20 +835,22 @@ struct Assistant {
     max_token_count: usize,
     pending_token_count: Task<Option<()>>,
     api_key: Rc<RefCell<Option<String>>>,
+    pending_save: Task<Result<()>>,
+    path: Option<PathBuf>,
     _subscriptions: Vec<Subscription>,
 }
 
-impl Entity for Assistant {
-    type Event = AssistantEvent;
+impl Entity for Conversation {
+    type Event = ConversationEvent;
 }
 
-impl Assistant {
+impl Conversation {
     fn new(
         api_key: Rc<RefCell<Option<String>>>,
         language_registry: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let model = "gpt-3.5-turbo";
+        let model = "gpt-3.5-turbo-0613";
         let markdown = language_registry.language_for_name("Markdown");
         let buffer = cx.add_model(|cx| {
             let mut buffer = Buffer::new(0, "", cx);
@@ -483,7 +870,7 @@ impl Assistant {
         });
 
         let mut this = Self {
-            messages: Default::default(),
+            message_anchors: Default::default(),
             messages_metadata: Default::default(),
             next_message_id: Default::default(),
             summary: None,
@@ -495,20 +882,22 @@ impl Assistant {
             pending_token_count: Task::ready(None),
             model: model.into(),
             _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
+            pending_save: Task::ready(Ok(())),
+            path: None,
             api_key,
             buffer,
         };
-        let message = Message {
+        let message = MessageAnchor {
             id: MessageId(post_inc(&mut this.next_message_id.0)),
             start: language::Anchor::MIN,
         };
-        this.messages.push(message.clone());
+        this.message_anchors.push(message.clone());
         this.messages_metadata.insert(
             message.id,
             MessageMetadata {
                 role: Role::User,
                 sent_at: Local::now(),
-                error: None,
+                status: MessageStatus::Done,
             },
         );
 
@@ -516,6 +905,88 @@ impl Assistant {
         this
     }
 
+    fn serialize(&self, cx: &AppContext) -> SavedConversation {
+        SavedConversation {
+            zed: "conversation".into(),
+            version: SavedConversation::VERSION.into(),
+            text: self.buffer.read(cx).text(),
+            message_metadata: self.messages_metadata.clone(),
+            messages: self
+                .messages(cx)
+                .map(|message| SavedMessage {
+                    id: message.id,
+                    start: message.offset_range.start,
+                })
+                .collect(),
+            summary: self
+                .summary
+                .as_ref()
+                .map(|summary| summary.text.clone())
+                .unwrap_or_default(),
+            model: self.model.clone(),
+        }
+    }
+
+    fn deserialize(
+        saved_conversation: SavedConversation,
+        path: PathBuf,
+        api_key: Rc<RefCell<Option<String>>>,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        let model = saved_conversation.model;
+        let markdown = language_registry.language_for_name("Markdown");
+        let mut message_anchors = Vec::new();
+        let mut next_message_id = MessageId(0);
+        let buffer = cx.add_model(|cx| {
+            let mut buffer = Buffer::new(0, saved_conversation.text, cx);
+            for message in saved_conversation.messages {
+                message_anchors.push(MessageAnchor {
+                    id: message.id,
+                    start: buffer.anchor_before(message.start),
+                });
+                next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
+            }
+            buffer.set_language_registry(language_registry);
+            cx.spawn_weak(|buffer, mut cx| async move {
+                let markdown = markdown.await?;
+                let buffer = buffer
+                    .upgrade(&cx)
+                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
+                buffer.update(&mut cx, |buffer, cx| {
+                    buffer.set_language(Some(markdown), cx)
+                });
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+            buffer
+        });
+
+        let mut this = Self {
+            message_anchors,
+            messages_metadata: saved_conversation.message_metadata,
+            next_message_id,
+            summary: Some(Summary {
+                text: saved_conversation.summary,
+                done: true,
+            }),
+            pending_summary: Task::ready(None),
+            completion_count: Default::default(),
+            pending_completions: Default::default(),
+            token_count: None,
+            max_token_count: tiktoken_rs::model::get_context_size(&model),
+            pending_token_count: Task::ready(None),
+            model,
+            _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
+            pending_save: Task::ready(Ok(())),
+            path: Some(path),
+            api_key,
+            buffer,
+        };
+        this.count_remaining_tokens(cx);
+        this
+    }
+
     fn handle_buffer_event(
         &mut self,
         _: ModelHandle<Buffer>,
@@ -525,7 +996,7 @@ impl Assistant {
         match event {
             language::Event::Edited => {
                 self.count_remaining_tokens(cx);
-                cx.emit(AssistantEvent::MessagesEdited);
+                cx.emit(ConversationEvent::MessagesEdited);
             }
             _ => {}
         }
@@ -533,7 +1004,7 @@ impl Assistant {
 
     fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
         let messages = self
-            .open_ai_request_messages(cx)
+            .messages(cx)
             .into_iter()
             .filter_map(|message| {
                 Some(tiktoken_rs::ChatCompletionRequestMessage {
@@ -542,7 +1013,11 @@ impl Assistant {
                         Role::Assistant => "assistant".into(),
                         Role::System => "system".into(),
                     },
-                    content: message.content,
+                    content: self
+                        .buffer
+                        .read(cx)
+                        .text_for_range(message.offset_range)
+                        .collect(),
                     name: None,
                 })
             })

crates/audio/Cargo.toml πŸ”—

@@ -0,0 +1,23 @@
+[package]
+name = "audio"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/audio.rs"
+doctest = false
+
+[dependencies]
+gpui = { path = "../gpui" }
+collections = { path = "../collections" }
+util = { path = "../util" }
+
+rodio = "0.17.1"
+
+log.workspace = true
+
+anyhow.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]

crates/audio/src/assets.rs πŸ”—

@@ -0,0 +1,44 @@
+use std::{io::Cursor, sync::Arc};
+
+use anyhow::Result;
+use collections::HashMap;
+use gpui::{AppContext, AssetSource};
+use rodio::{
+    source::{Buffered, SamplesConverter},
+    Decoder, Source,
+};
+
+type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
+
+pub struct SoundRegistry {
+    cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
+    assets: Box<dyn AssetSource>,
+}
+
+impl SoundRegistry {
+    pub fn new(source: impl AssetSource) -> Arc<Self> {
+        Arc::new(Self {
+            cache: Default::default(),
+            assets: Box::new(source),
+        })
+    }
+
+    pub fn global(cx: &AppContext) -> Arc<Self> {
+        cx.global::<Arc<Self>>().clone()
+    }
+
+    pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
+        if let Some(wav) = self.cache.lock().get(name) {
+            return Ok(wav.clone());
+        }
+
+        let path = format!("sounds/{}.wav", name);
+        let bytes = self.assets.load(&path)?.into_owned();
+        let cursor = Cursor::new(bytes);
+        let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
+
+        self.cache.lock().insert(name.to_string(), source.clone());
+
+        Ok(source)
+    }
+}

crates/audio/src/audio.rs πŸ”—

@@ -0,0 +1,67 @@
+use assets::SoundRegistry;
+use gpui::{AppContext, AssetSource};
+use rodio::{OutputStream, OutputStreamHandle};
+use util::ResultExt;
+
+mod assets;
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(SoundRegistry::new(source));
+    cx.set_global(Audio::new());
+}
+
+pub enum Sound {
+    Joined,
+    Leave,
+    Mute,
+    Unmute,
+    StartScreenshare,
+    StopScreenshare,
+}
+
+impl Sound {
+    fn file(&self) -> &'static str {
+        match self {
+            Self::Joined => "joined_call",
+            Self::Leave => "leave_call",
+            Self::Mute => "mute",
+            Self::Unmute => "unmute",
+            Self::StartScreenshare => "start_screenshare",
+            Self::StopScreenshare => "stop_screenshare",
+        }
+    }
+}
+
+pub struct Audio {
+    _output_stream: Option<OutputStream>,
+    output_handle: Option<OutputStreamHandle>,
+}
+
+impl Audio {
+    pub fn new() -> Self {
+        let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+
+        Self {
+            _output_stream,
+            output_handle,
+        }
+    }
+
+    pub fn play_sound(sound: Sound, cx: &AppContext) {
+        if !cx.has_global::<Self>() {
+            return;
+        }
+
+        let this = cx.global::<Self>();
+
+        let Some(output_handle) = this.output_handle.as_ref() else {
+            return;
+        };
+
+        let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
+        return;
+    };
+
+        output_handle.play_raw(source).log_err();
+    }
+}

crates/auto_update/src/update_notification.rs πŸ”—

@@ -49,7 +49,7 @@ impl View for UpdateNotification {
                         )
                         .with_child(
                             MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
-                                let style = theme.dismiss_button.style_for(state, false);
+                                let style = theme.dismiss_button.style_for(state);
                                 Svg::new("icons/x_mark_8.svg")
                                     .with_color(style.color)
                                     .constrained()
@@ -74,7 +74,7 @@ impl View for UpdateNotification {
                         ),
                 )
                 .with_child({
-                    let style = theme.action_message.style_for(state, false);
+                    let style = theme.action_message.style_for(state);
                     Text::new("View the release notes", style.text.clone())
                         .contained()
                         .with_style(style.container)

crates/breadcrumbs/src/breadcrumbs.rs πŸ”—

@@ -83,7 +83,7 @@ impl View for Breadcrumbs {
         }
 
         MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
-            let style = style.style_for(state, false);
+            let style = style.style_for(state);
             crumbs.with_style(style.container)
         })
         .on_click(MouseButton::Left, |_, this, cx| {

crates/call/Cargo.toml πŸ”—

@@ -19,6 +19,7 @@ test-support = [
 ]
 
 [dependencies]
+audio = { path = "../audio" }
 client = { path = "../client" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }

crates/call/src/participant.rs πŸ”—

@@ -3,6 +3,7 @@ use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModelHandle;
 pub use live_kit_client::Frame;
+use live_kit_client::RemoteAudioTrack;
 use project::Project;
 use std::{fmt, sync::Arc};
 
@@ -42,7 +43,10 @@ pub struct RemoteParticipant {
     pub peer_id: proto::PeerId,
     pub projects: Vec<proto::ParticipantProject>,
     pub location: ParticipantLocation,
-    pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
+    pub muted: bool,
+    pub speaking: bool,
+    pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
+    pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
 }
 
 #[derive(Clone)]

crates/call/src/room.rs πŸ”—

@@ -3,6 +3,7 @@ use crate::{
     IncomingCall,
 };
 use anyhow::{anyhow, Result};
+use audio::{Audio, Sound};
 use client::{
     proto::{self, PeerId},
     Client, TypedEnvelope, User, UserStore,
@@ -12,7 +13,10 @@ use fs::Fs;
 use futures::{FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use language::LanguageRegistry;
-use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
+use live_kit_client::{
+    LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
+    RemoteVideoTrackUpdate,
+};
 use postage::stream::Stream;
 use project::Project;
 use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
@@ -28,6 +32,9 @@ pub enum Event {
     RemoteVideoTracksChanged {
         participant_id: proto::PeerId,
     },
+    RemoteAudioTracksChanged {
+        participant_id: proto::PeerId,
+    },
     RemoteProjectShared {
         owner: Arc<User>,
         project_id: u64,
@@ -112,9 +119,9 @@ impl Room {
                 }
             });
 
-            let mut track_changes = room.remote_video_track_updates();
-            let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move {
-                while let Some(track_change) = track_changes.next().await {
+            let mut track_video_changes = room.remote_video_track_updates();
+            let _maintain_video_tracks = cx.spawn_weak(|this, mut cx| async move {
+                while let Some(track_change) = track_video_changes.next().await {
                     let this = if let Some(this) = this.upgrade(&cx) {
                         this
                     } else {
@@ -127,16 +134,42 @@ impl Room {
                 }
             });
 
-            cx.foreground()
-                .spawn(room.connect(&connection_info.server_url, &connection_info.token))
-                .detach_and_log_err(cx);
+            let mut track_audio_changes = room.remote_audio_track_updates();
+            let _maintain_audio_tracks = cx.spawn_weak(|this, mut cx| async move {
+                while let Some(track_change) = track_audio_changes.next().await {
+                    let this = if let Some(this) = this.upgrade(&cx) {
+                        this
+                    } else {
+                        break;
+                    };
+
+                    this.update(&mut cx, |this, cx| {
+                        this.remote_audio_track_updated(track_change, cx).log_err()
+                    });
+                }
+            });
+
+            let connect = room.connect(&connection_info.server_url, &connection_info.token);
+            cx.spawn(|this, mut cx| async move {
+                connect.await?;
+
+                this.update(&mut cx, |this, cx| this.share_microphone(cx))
+                    .await?;
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
 
             Some(LiveKitRoom {
                 room,
-                screen_track: ScreenTrack::None,
+                screen_track: LocalTrack::None,
+                microphone_track: LocalTrack::None,
                 next_publish_id: 0,
+                muted_by_user: false,
+                deafened: false,
+                speaking: false,
                 _maintain_room,
-                _maintain_tracks,
+                _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],
             })
         } else {
             None
@@ -145,6 +178,8 @@ impl Room {
         let maintain_connection =
             cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
 
+        Audio::play_sound(Sound::Joined, cx);
+
         Self {
             id,
             live_kit: live_kit_room,
@@ -234,6 +269,7 @@ impl Room {
                 room.apply_room_update(room_proto, cx)?;
                 anyhow::Ok(())
             })?;
+
             Ok(room)
         })
     }
@@ -275,6 +311,8 @@ impl Room {
             }
         }
 
+        Audio::play_sound(Sound::Leave, cx);
+
         self.status = RoomStatus::Offline;
         self.remote_participants.clear();
         self.pending_participants.clear();
@@ -618,20 +656,34 @@ impl Room {
                                     peer_id,
                                     projects: participant.projects,
                                     location,
-                                    tracks: Default::default(),
+                                    muted: false,
+                                    speaking: false,
+                                    video_tracks: Default::default(),
+                                    audio_tracks: Default::default(),
                                 },
                             );
 
+                            Audio::play_sound(Sound::Joined, cx);
+
                             if let Some(live_kit) = this.live_kit.as_ref() {
-                                let tracks =
+                                let video_tracks =
                                     live_kit.room.remote_video_tracks(&user.id.to_string());
-                                for track in tracks {
+                                let audio_tracks =
+                                    live_kit.room.remote_audio_tracks(&user.id.to_string());
+                                for track in video_tracks {
                                     this.remote_video_track_updated(
                                         RemoteVideoTrackUpdate::Subscribed(track),
                                         cx,
                                     )
                                     .log_err();
                                 }
+                                for track in audio_tracks {
+                                    this.remote_audio_track_updated(
+                                        RemoteAudioTrackUpdate::Subscribed(track),
+                                        cx,
+                                    )
+                                    .log_err();
+                                }
                             }
                         }
                     }
@@ -706,7 +758,7 @@ impl Room {
                     .remote_participants
                     .get_mut(&user_id)
                     .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
-                participant.tracks.insert(
+                participant.video_tracks.insert(
                     track_id.clone(),
                     Arc::new(RemoteVideoTrack {
                         live_kit_track: track,
@@ -725,7 +777,7 @@ impl Room {
                     .remote_participants
                     .get_mut(&user_id)
                     .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
-                participant.tracks.remove(&track_id);
+                participant.video_tracks.remove(&track_id);
                 cx.emit(Event::RemoteVideoTracksChanged {
                     participant_id: participant.peer_id,
                 });
@@ -736,6 +788,84 @@ impl Room {
         Ok(())
     }
 
+    fn remote_audio_track_updated(
+        &mut self,
+        change: RemoteAudioTrackUpdate,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        match change {
+            RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => {
+                let mut speaker_ids = speakers
+                    .into_iter()
+                    .filter_map(|speaker_sid| speaker_sid.parse().ok())
+                    .collect::<Vec<u64>>();
+                speaker_ids.sort_unstable();
+                for (sid, participant) in &mut self.remote_participants {
+                    if let Ok(_) = speaker_ids.binary_search(sid) {
+                        participant.speaking = true;
+                    } else {
+                        participant.speaking = false;
+                    }
+                }
+                if let Some(id) = self.client.user_id() {
+                    if let Some(room) = &mut self.live_kit {
+                        if let Ok(_) = speaker_ids.binary_search(&id) {
+                            room.speaking = true;
+                        } else {
+                            room.speaking = false;
+                        }
+                    }
+                }
+                cx.notify();
+            }
+            RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
+                for participant in &mut self.remote_participants.values_mut() {
+                    let mut found = false;
+                    for track in participant.audio_tracks.values() {
+                        if track.sid() == track_id {
+                            found = true;
+                            break;
+                        }
+                    }
+                    if found {
+                        participant.muted = muted;
+                        break;
+                    }
+                }
+                cx.notify();
+            }
+            RemoteAudioTrackUpdate::Subscribed(track) => {
+                let user_id = track.publisher_id().parse()?;
+                let track_id = track.sid().to_string();
+                let participant = self
+                    .remote_participants
+                    .get_mut(&user_id)
+                    .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+                participant.audio_tracks.insert(track_id.clone(), track);
+                cx.emit(Event::RemoteAudioTracksChanged {
+                    participant_id: participant.peer_id,
+                });
+            }
+            RemoteAudioTrackUpdate::Unsubscribed {
+                publisher_id,
+                track_id,
+            } => {
+                let user_id = publisher_id.parse()?;
+                let participant = self
+                    .remote_participants
+                    .get_mut(&user_id)
+                    .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
+                participant.audio_tracks.remove(&track_id);
+                cx.emit(Event::RemoteAudioTracksChanged {
+                    participant_id: participant.peer_id,
+                });
+            }
+        }
+
+        cx.notify();
+        Ok(())
+    }
+
     fn check_invariants(&self) {
         #[cfg(any(test, feature = "test-support"))]
         {
@@ -801,6 +931,7 @@ impl Room {
         cx.spawn(|this, mut cx| async move {
             let project =
                 Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
+
             this.update(&mut cx, |this, cx| {
                 this.joined_projects.retain(|project| {
                     if let Some(project) = project.upgrade(cx) {
@@ -908,7 +1039,116 @@ impl Room {
 
     pub fn is_screen_sharing(&self) -> bool {
         self.live_kit.as_ref().map_or(false, |live_kit| {
-            !matches!(live_kit.screen_track, ScreenTrack::None)
+            !matches!(live_kit.screen_track, LocalTrack::None)
+        })
+    }
+
+    pub fn is_sharing_mic(&self) -> bool {
+        self.live_kit.as_ref().map_or(false, |live_kit| {
+            !matches!(live_kit.microphone_track, LocalTrack::None)
+        })
+    }
+
+    pub fn is_muted(&self) -> bool {
+        self.live_kit
+            .as_ref()
+            .and_then(|live_kit| match &live_kit.microphone_track {
+                LocalTrack::None => None,
+                LocalTrack::Pending { muted, .. } => Some(*muted),
+                LocalTrack::Published { muted, .. } => Some(*muted),
+            })
+            .unwrap_or(false)
+    }
+
+    pub fn is_speaking(&self) -> bool {
+        self.live_kit
+            .as_ref()
+            .map_or(false, |live_kit| live_kit.speaking)
+    }
+
+    pub fn is_deafened(&self) -> Option<bool> {
+        self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
+    }
+
+    pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        if self.status.is_offline() {
+            return Task::ready(Err(anyhow!("room is offline")));
+        } else if self.is_sharing_mic() {
+            return Task::ready(Err(anyhow!("microphone was already shared")));
+        }
+
+        let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
+            let publish_id = post_inc(&mut live_kit.next_publish_id);
+            live_kit.microphone_track = LocalTrack::Pending {
+                publish_id,
+                muted: false,
+            };
+            cx.notify();
+            publish_id
+        } else {
+            return Task::ready(Err(anyhow!("live-kit was not initialized")));
+        };
+
+        cx.spawn_weak(|this, mut cx| async move {
+            let publish_track = async {
+                let track = LocalAudioTrack::create();
+                this.upgrade(&cx)
+                    .ok_or_else(|| anyhow!("room was dropped"))?
+                    .read_with(&cx, |this, _| {
+                        this.live_kit
+                            .as_ref()
+                            .map(|live_kit| live_kit.room.publish_audio_track(&track))
+                    })
+                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?
+                    .await
+            };
+
+            let publication = publish_track.await;
+            this.upgrade(&cx)
+                .ok_or_else(|| anyhow!("room was dropped"))?
+                .update(&mut cx, |this, cx| {
+                    let live_kit = this
+                        .live_kit
+                        .as_mut()
+                        .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+
+                    let (canceled, muted) = if let LocalTrack::Pending {
+                        publish_id: cur_publish_id,
+                        muted,
+                    } = &live_kit.microphone_track
+                    {
+                        (*cur_publish_id != publish_id, *muted)
+                    } else {
+                        (true, false)
+                    };
+
+                    match publication {
+                        Ok(publication) => {
+                            if canceled {
+                                live_kit.room.unpublish_track(publication);
+                            } else {
+                                if muted {
+                                    cx.background().spawn(publication.set_mute(muted)).detach();
+                                }
+                                live_kit.microphone_track = LocalTrack::Published {
+                                    track_publication: publication,
+                                    muted,
+                                };
+                                cx.notify();
+                            }
+                            Ok(())
+                        }
+                        Err(error) => {
+                            if canceled {
+                                Ok(())
+                            } else {
+                                live_kit.microphone_track = LocalTrack::None;
+                                cx.notify();
+                                Err(error)
+                            }
+                        }
+                    }
+                })
         })
     }
 
@@ -921,7 +1161,10 @@ impl Room {
 
         let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
             let publish_id = post_inc(&mut live_kit.next_publish_id);
-            live_kit.screen_track = ScreenTrack::Pending { publish_id };
+            live_kit.screen_track = LocalTrack::Pending {
+                publish_id,
+                muted: false,
+            };
             cx.notify();
             (live_kit.room.display_sources(), publish_id)
         } else {
@@ -955,13 +1198,14 @@ impl Room {
                         .as_mut()
                         .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
 
-                    let canceled = if let ScreenTrack::Pending {
+                    let (canceled, muted) = if let LocalTrack::Pending {
                         publish_id: cur_publish_id,
+                        muted,
                     } = &live_kit.screen_track
                     {
-                        *cur_publish_id != publish_id
+                        (*cur_publish_id != publish_id, *muted)
                     } else {
-                        true
+                        (true, false)
                     };
 
                     match publication {
@@ -969,16 +1213,25 @@ impl Room {
                             if canceled {
                                 live_kit.room.unpublish_track(publication);
                             } else {
-                                live_kit.screen_track = ScreenTrack::Published(publication);
+                                if muted {
+                                    cx.background().spawn(publication.set_mute(muted)).detach();
+                                }
+                                live_kit.screen_track = LocalTrack::Published {
+                                    track_publication: publication,
+                                    muted,
+                                };
                                 cx.notify();
                             }
+
+                            Audio::play_sound(Sound::StartScreenshare, cx);
+
                             Ok(())
                         }
                         Err(error) => {
                             if canceled {
                                 Ok(())
                             } else {
-                                live_kit.screen_track = ScreenTrack::None;
+                                live_kit.screen_track = LocalTrack::None;
                                 cx.notify();
                                 Err(error)
                             }
@@ -988,6 +1241,59 @@ impl Room {
         })
     }
 
+    pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
+        let should_mute = !self.is_muted();
+        if let Some(live_kit) = self.live_kit.as_mut() {
+            let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
+            live_kit.muted_by_user = should_mute;
+
+            if old_muted == true && live_kit.deafened == true {
+                if let Some(task) = self.toggle_deafen(cx).ok() {
+                    task.detach();
+                }
+            }
+
+            Ok(ret_task)
+        } else {
+            Err(anyhow!("LiveKit not started"))
+        }
+    }
+
+    pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
+        if let Some(live_kit) = self.live_kit.as_mut() {
+            (*live_kit).deafened = !live_kit.deafened;
+
+            let mut tasks = Vec::with_capacity(self.remote_participants.len());
+            // Context notification is sent within set_mute itself.
+            let mut mute_task = None;
+            // When deafening, mute user's mic as well.
+            // When undeafening, unmute user's mic unless it was manually muted prior to deafening.
+            if live_kit.deafened || !live_kit.muted_by_user {
+                mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
+            };
+            for participant in self.remote_participants.values() {
+                for track in live_kit
+                    .room
+                    .remote_audio_track_publications(&participant.user.id.to_string())
+                {
+                    tasks.push(cx.foreground().spawn(track.set_enabled(!live_kit.deafened)));
+                }
+            }
+
+            Ok(cx.foreground().spawn(async move {
+                if let Some(mute_task) = mute_task {
+                    mute_task.await?;
+                }
+                for task in tasks {
+                    task.await?;
+                }
+                Ok(())
+            }))
+        } else {
+            Err(anyhow!("LiveKit not started"))
+        }
+    }
+
     pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
         if self.status.is_offline() {
             return Err(anyhow!("room is offline"));
@@ -998,14 +1304,18 @@ impl Room {
             .as_mut()
             .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
         match mem::take(&mut live_kit.screen_track) {
-            ScreenTrack::None => Err(anyhow!("screen was not shared")),
-            ScreenTrack::Pending { .. } => {
+            LocalTrack::None => Err(anyhow!("screen was not shared")),
+            LocalTrack::Pending { .. } => {
                 cx.notify();
                 Ok(())
             }
-            ScreenTrack::Published(track) => {
-                live_kit.room.unpublish_track(track);
+            LocalTrack::Published {
+                track_publication, ..
+            } => {
+                live_kit.room.unpublish_track(track_publication);
                 cx.notify();
+
+                Audio::play_sound(Sound::StopScreenshare, cx);
                 Ok(())
             }
         }
@@ -1023,19 +1333,75 @@ impl Room {
 
 struct LiveKitRoom {
     room: Arc<live_kit_client::Room>,
-    screen_track: ScreenTrack,
+    screen_track: LocalTrack,
+    microphone_track: LocalTrack,
+    /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
+    muted_by_user: bool,
+    deafened: bool,
+    speaking: bool,
     next_publish_id: usize,
     _maintain_room: Task<()>,
-    _maintain_tracks: Task<()>,
+    _maintain_tracks: [Task<()>; 2],
+}
+
+impl LiveKitRoom {
+    fn set_mute(
+        self: &mut LiveKitRoom,
+        should_mute: bool,
+        cx: &mut ModelContext<Room>,
+    ) -> Result<(Task<Result<()>>, bool)> {
+        if !should_mute {
+            // clear user muting state.
+            self.muted_by_user = false;
+        }
+
+        let (result, old_muted) = match &mut self.microphone_track {
+            LocalTrack::None => Err(anyhow!("microphone was not shared")),
+            LocalTrack::Pending { muted, .. } => {
+                let old_muted = *muted;
+                *muted = should_mute;
+                cx.notify();
+                Ok((Task::Ready(Some(Ok(()))), old_muted))
+            }
+            LocalTrack::Published {
+                track_publication,
+                muted,
+            } => {
+                let old_muted = *muted;
+                *muted = should_mute;
+                cx.notify();
+                Ok((
+                    cx.background().spawn(track_publication.set_mute(*muted)),
+                    old_muted,
+                ))
+            }
+        }?;
+
+        if old_muted != should_mute {
+            if should_mute {
+                Audio::play_sound(Sound::Mute, cx);
+            } else {
+                Audio::play_sound(Sound::Unmute, cx);
+            }
+        }
+
+        Ok((result, old_muted))
+    }
 }
 
-enum ScreenTrack {
+enum LocalTrack {
     None,
-    Pending { publish_id: usize },
-    Published(LocalTrackPublication),
+    Pending {
+        publish_id: usize,
+        muted: bool,
+    },
+    Published {
+        track_publication: LocalTrackPublication,
+        muted: bool,
+    },
 }
 
-impl Default for ScreenTrack {
+impl Default for LocalTrack {
     fn default() -> Self {
         Self::None
     }

crates/client/src/telemetry.rs πŸ”—

@@ -1,5 +1,4 @@
 use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
-use db::kvp::KEY_VALUE_STORE;
 use gpui::{executor::Background, serde_json, AppContext, Task};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
@@ -8,7 +7,6 @@ use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
 use tempfile::NamedTempFile;
 use util::http::HttpClient;
 use util::{channel::ReleaseChannel, TryFutureExt};
-use uuid::Uuid;
 
 pub struct Telemetry {
     http_client: Arc<dyn HttpClient>,
@@ -120,39 +118,15 @@ impl Telemetry {
         Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
     }
 
-    pub fn start(self: &Arc<Self>) {
-        let this = self.clone();
-        self.executor
-            .spawn(
-                async move {
-                    let installation_id =
-                        if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
-                            installation_id
-                        } else {
-                            let installation_id = Uuid::new_v4().to_string();
-                            KEY_VALUE_STORE
-                                .write_kvp("device_id".to_string(), installation_id.clone())
-                                .await?;
-                            installation_id
-                        };
-
-                    let installation_id: Arc<str> = installation_id.into();
-                    let mut state = this.state.lock();
-                    state.installation_id = Some(installation_id.clone());
-
-                    let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
-
-                    drop(state);
-
-                    if has_clickhouse_events {
-                        this.flush_clickhouse_events();
-                    }
+    pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
+        let mut state = self.state.lock();
+        state.installation_id = installation_id.map(|id| id.into());
+        let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
+        drop(state);
 
-                    anyhow::Ok(())
-                }
-                .log_err(),
-            )
-            .detach();
+        if has_clickhouse_events {
+            self.flush_clickhouse_events();
+        }
     }
 
     /// This method takes the entire TelemetrySettings struct in order to force client code

crates/collab/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.14.2"
+version = "0.16.0"
 publish = false
 
 [[bin]]
@@ -57,6 +57,7 @@ tracing-log = "0.1.3"
 tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
 
 [dev-dependencies]
+audio = { path = "../audio" }
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 call = { path = "../call", features = ["test-support"] }
@@ -67,7 +68,7 @@ fs = { path = "../fs", features = ["test-support"] }
 git = { path = "../git", features = ["test-support"] }
 live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }

crates/collab/src/db.rs πŸ”—

@@ -1539,6 +1539,7 @@ impl Database {
                                     }),
                                     is_symlink: db_entry.is_symlink,
                                     is_ignored: db_entry.is_ignored,
+                                    is_external: db_entry.is_external,
                                     git_status: db_entry.git_status.map(|status| status as i32),
                                 });
                             }
@@ -2349,6 +2350,7 @@ impl Database {
                         mtime_nanos: ActiveValue::set(mtime.nanos as i32),
                         is_symlink: ActiveValue::set(entry.is_symlink),
                         is_ignored: ActiveValue::set(entry.is_ignored),
+                        is_external: ActiveValue::set(entry.is_external),
                         git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
                         is_deleted: ActiveValue::set(false),
                         scan_id: ActiveValue::set(update.scan_id as i64),
@@ -2705,6 +2707,7 @@ impl Database {
                             }),
                             is_symlink: db_entry.is_symlink,
                             is_ignored: db_entry.is_ignored,
+                            is_external: db_entry.is_external,
                             git_status: db_entry.git_status.map(|status| status as i32),
                         });
                     }

crates/collab/src/rpc.rs πŸ”—

@@ -201,6 +201,7 @@ impl Server {
             .add_message_handler(update_language_server)
             .add_message_handler(update_diagnostic_summary)
             .add_message_handler(update_worktree_settings)
+            .add_message_handler(refresh_inlay_hints)
             .add_request_handler(forward_project_request::<proto::GetHover>)
             .add_request_handler(forward_project_request::<proto::GetDefinition>)
             .add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -224,7 +225,9 @@ impl Server {
             .add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
             .add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
             .add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
+            .add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
             .add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
+            .add_request_handler(forward_project_request::<proto::InlayHints>)
             .add_message_handler(create_buffer_for_peer)
             .add_request_handler(update_buffer)
             .add_message_handler(update_buffer_file)
@@ -1573,6 +1576,10 @@ async fn update_worktree_settings(
     Ok(())
 }
 
+async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
+    broadcast_project_message(request.project_id, request, session).await
+}
+
 async fn start_language_server(
     request: proto::StartLanguageServer,
     session: Session,
@@ -1749,7 +1756,15 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
 }
 
 async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
+    broadcast_project_message(request.project_id, request, session).await
+}
+
+async fn broadcast_project_message<T: EnvelopedMessage>(
+    project_id: u64,
+    request: T,
+    session: Session,
+) -> Result<()> {
+    let project_id = ProjectId::from_proto(project_id);
     let project_connection_ids = session
         .db()
         .await

crates/collab/src/tests.rs πŸ”—

@@ -203,6 +203,7 @@ impl TestServer {
             language::init(cx);
             editor::init_settings(cx);
             workspace::init(app_state.clone(), cx);
+            audio::init((), cx);
             call::init(client.clone(), user_store.clone(), cx);
         });
 

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -18,7 +18,7 @@ use gpui::{
 };
 use indoc::indoc;
 use language::{
-    language_settings::{AllLanguageSettings, Formatter},
+    language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
     tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
     LanguageConfig, OffsetRangeExt, Point, Rope,
 };
@@ -34,12 +34,17 @@ use std::{
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
         Arc,
     },
 };
 use unindent::Unindent as _;
-use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace};
+use workspace::{
+    dock::{test::TestPanel, DockPosition},
+    item::{test::TestItem, ItemHandle as _},
+    shared_screen::SharedScreen,
+    SplitDirection, Workspace,
+};
 
 #[ctor::ctor]
 fn init_logger() {
@@ -252,7 +257,7 @@ async fn test_basic_calls(
         room_b.read_with(cx_b, |room, _| {
             assert_eq!(
                 room.remote_participants()[&client_a.user_id().unwrap()]
-                    .tracks
+                    .video_tracks
                     .len(),
                 1
             );
@@ -269,7 +274,7 @@ async fn test_basic_calls(
         room_c.read_with(cx_c, |room, _| {
             assert_eq!(
                 room.remote_participants()[&client_a.user_id().unwrap()]
-                    .tracks
+                    .video_tracks
                     .len(),
                 1
             );
@@ -1261,6 +1266,27 @@ async fn test_share_project(
         let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
         assert_eq!(client_b_collaborator.replica_id, replica_id_b);
     });
+    project_b.read_with(cx_b, |project, cx| {
+        let worktree = project.worktrees(cx).next().unwrap().read(cx);
+        assert_eq!(
+            worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
+            [
+                Path::new(".gitignore"),
+                Path::new("a.txt"),
+                Path::new("b.txt"),
+                Path::new("ignored-dir"),
+            ]
+        );
+    });
+
+    project_b
+        .update(cx_b, |project, cx| {
+            let worktree = project.worktrees(cx).next().unwrap();
+            let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
+            project.expand_entry(worktree_id, entry.id, cx).unwrap()
+        })
+        .await
+        .unwrap();
     project_b.read_with(cx_b, |project, cx| {
         let worktree = project.worktrees(cx).next().unwrap().read(cx);
         assert_eq!(
@@ -6847,12 +6873,43 @@ async fn test_basic_following(
         )
     });
 
-    // Client B activates an external window again, and the previously-opened screen-sharing item
-    // gets activated.
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(None, cx))
-        .await
-        .unwrap();
+    // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
+    let panel = cx_b.add_view(workspace_b.window_id(), |_| {
+        TestPanel::new(DockPosition::Left)
+    });
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.add_panel(panel, cx);
+        workspace.toggle_panel_focus::<TestPanel>(cx);
+    });
+    deterministic.run_until_parked();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        shared_screen.id()
+    );
+
+    // Toggling the focus back to the pane causes client A to return to the multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.toggle_panel_focus::<TestPanel>(cx);
+    });
+    deterministic.run_until_parked();
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().id(),
+            multibuffer_editor_a.id()
+        )
+    });
+
+    // Client B activates an item that doesn't implement following,
+    // so the previously-opened screen-sharing item gets activated.
+    let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new());
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
+        })
+    });
     deterministic.run_until_parked();
     assert_eq!(
         workspace_a.read_with(cx_a, |workspace, cx| workspace
@@ -6957,7 +7014,7 @@ async fn test_join_call_after_screen_was_shared(
             room.remote_participants()
                 .get(&client_a.user_id().unwrap())
                 .unwrap()
-                .tracks
+                .video_tracks
                 .len(),
             1
         );
@@ -7743,6 +7800,572 @@ async fn test_on_input_format_from_guest_to_host(
     });
 }
 
+#[gpui::test]
+async fn test_mutual_editor_inlay_hint_cache_update(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    cx_a.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    cx_b.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+    let language = Arc::new(language);
+    client_a.language_registry.add(Arc::clone(&language));
+    client_b.language_registry.add(language);
+
+    client_a
+        .fs
+        .insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    cx_a.foreground().start_waiting();
+
+    let _buffer_a = project_a
+        .update(cx_a, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    let next_call_id = Arc::new(AtomicU32::new(0));
+    let editor_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    fake_language_server
+        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+            let task_next_call_id = Arc::clone(&next_call_id);
+            async move {
+                assert_eq!(
+                    params.text_document.uri,
+                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                );
+                let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+                let mut new_hints = Vec::with_capacity(current_call_id as usize);
+                loop {
+                    new_hints.push(lsp::InlayHint {
+                        position: lsp::Position::new(0, current_call_id),
+                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    });
+                    if current_call_id == 0 {
+                        break;
+                    }
+                    current_call_id -= 1;
+                }
+                Ok(Some(new_hints))
+            }
+        })
+        .next()
+        .await
+        .unwrap();
+
+    cx_a.foreground().finish_waiting();
+    cx_a.foreground().run_until_parked();
+
+    let mut edits_made = 1;
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string()],
+            extract_hint_labels(editor),
+            "Host should get its first hints when opens an editor"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Cache should use editor settings to get the allowed hint kinds"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Host editor update the cache version after every cache/view change",
+        );
+    });
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    cx_b.foreground().run_until_parked();
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string(), "1".to_string()],
+            extract_hint_labels(editor),
+            "Client should get its first hints when opens an editor"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Cache should use editor settings to get the allowed hint kinds"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Guest editor update the cache version after every cache/view change"
+        );
+    });
+
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
+        editor.handle_input(":", cx);
+        cx.focus(&editor_b);
+        edits_made += 1;
+    });
+    cx_a.foreground().run_until_parked();
+    cx_b.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string(), "1".to_string(), "2".to_string()],
+            extract_hint_labels(editor),
+            "Host should get hints from the 1st edit and 1st LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string()
+            ],
+            extract_hint_labels(editor),
+            "Guest should get hints the 1st edit and 2nd LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+
+    editor_a.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+        editor.handle_input("a change to increment both buffers' versions", cx);
+        cx.focus(&editor_a);
+        edits_made += 1;
+    });
+    cx_a.foreground().run_until_parked();
+    cx_b.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string()
+            ],
+            extract_hint_labels(editor),
+            "Host should get hints from 3rd edit, 5th LSP query: \
+4th query was made by guest (but not applied) due to cache invalidation logic"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+                "5".to_string(),
+            ],
+            extract_hint_labels(editor),
+            "Guest should get hints from 3rd edit, 6th LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+
+    fake_language_server
+        .request::<lsp::request::InlayHintRefreshRequest>(())
+        .await
+        .expect("inlay refresh request failed");
+    edits_made += 1;
+    cx_a.foreground().run_until_parked();
+    cx_b.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+                "5".to_string(),
+                "6".to_string(),
+            ],
+            extract_hint_labels(editor),
+            "Host should react to /refresh LSP request and get new hints from 7th LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Host should accepted all edits and bump its cache version every time"
+        );
+    });
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+                "5".to_string(),
+                "6".to_string(),
+                "7".to_string(),
+            ],
+            extract_hint_labels(editor),
+            "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(
+            inlay_cache.version,
+            edits_made,
+            "Guest should accepted all edits and bump its cache version every time"
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_inlay_hint_refresh_is_forwarded(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    cx_a.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: false,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    cx_b.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+    let language = Arc::new(language);
+    client_a.language_registry.add(Arc::clone(&language));
+    client_b.language_registry.add(language);
+
+    client_a
+        .fs
+        .insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    cx_a.foreground().start_waiting();
+    cx_b.foreground().start_waiting();
+
+    let editor_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    let next_call_id = Arc::new(AtomicU32::new(0));
+    fake_language_server
+        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+            let task_next_call_id = Arc::clone(&next_call_id);
+            async move {
+                assert_eq!(
+                    params.text_document.uri,
+                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                );
+                let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+                let mut new_hints = Vec::with_capacity(current_call_id as usize);
+                loop {
+                    new_hints.push(lsp::InlayHint {
+                        position: lsp::Position::new(0, current_call_id),
+                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    });
+                    if current_call_id == 0 {
+                        break;
+                    }
+                    current_call_id -= 1;
+                }
+                Ok(Some(new_hints))
+            }
+        })
+        .next()
+        .await
+        .unwrap();
+    cx_a.foreground().finish_waiting();
+    cx_b.foreground().finish_waiting();
+
+    cx_a.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert!(
+            extract_hint_labels(editor).is_empty(),
+            "Host should get no hints due to them turned off"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Host should have allowed hint kinds set despite hints are off"
+        );
+        assert_eq!(
+            inlay_cache.version, 0,
+            "Host should not increment its cache version due to no changes",
+        );
+    });
+
+    let mut edits_made = 1;
+    cx_b.foreground().run_until_parked();
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string()],
+            extract_hint_labels(editor),
+            "Client should get its first hints when opens an editor"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Cache should use editor settings to get the allowed hint kinds"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Guest editor update the cache version after every cache/view change"
+        );
+    });
+
+    fake_language_server
+        .request::<lsp::request::InlayHintRefreshRequest>(())
+        .await
+        .expect("inlay refresh request failed");
+    cx_a.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert!(
+            extract_hint_labels(editor).is_empty(),
+            "Host should get nop hints due to them turned off, even after the /refresh"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+        assert_eq!(
+            inlay_cache.version, 0,
+            "Host should not increment its cache version due to no changes",
+        );
+    });
+
+    edits_made += 1;
+    cx_b.foreground().run_until_parked();
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string(), "1".to_string(),],
+            extract_hint_labels(editor),
+            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Guest should accepted all edits and bump its cache version every time"
+        );
+    });
+}
+
 #[derive(Debug, Eq, PartialEq)]
 struct RoomParticipants {
     remote: Vec<String>,
@@ -7766,3 +8389,17 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
         RoomParticipants { remote, pending }
     })
 }
+
+fn extract_hint_labels(editor: &Editor) -> Vec<String> {
+    let mut labels = Vec::new();
+    for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+        let excerpt_hints = excerpt_hints.read();
+        for (_, inlay) in excerpt_hints.hints.iter() {
+            match &inlay.label {
+                project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+                _ => unreachable!(),
+            }
+        }
+    }
+    labels
+}

crates/collab_ui/Cargo.toml πŸ”—

@@ -35,10 +35,14 @@ gpui = { path = "../gpui" }
 menu = { path = "../menu" }
 picker = { path = "../picker" }
 project = { path = "../project" }
+recent_projects = {path = "../recent_projects"}
 settings = { path = "../settings" }
 theme = { path = "../theme" }
+theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
+zed-actions = {path = "../zed-actions"}
+
 
 anyhow.workspace = true
 futures.workspace = true

crates/collab_ui/src/branch_list.rs πŸ”—

@@ -0,0 +1,238 @@
+use anyhow::{anyhow, bail};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::{ops::Not, sync::Arc};
+use util::ResultExt;
+use workspace::{Toast, Workspace};
+
+pub fn init(cx: &mut AppContext) {
+    Picker::<BranchListDelegate>::init(cx);
+}
+
+pub type BranchList = Picker<BranchListDelegate>;
+
+pub fn build_branch_list(
+    workspace: ViewHandle<Workspace>,
+    cx: &mut ViewContext<BranchList>,
+) -> BranchList {
+    Picker::new(
+        BranchListDelegate {
+            matches: vec![],
+            workspace,
+            selected_index: 0,
+            last_query: String::default(),
+        },
+        cx,
+    )
+    .with_theme(|theme| theme.picker.clone())
+}
+
+pub struct BranchListDelegate {
+    matches: Vec<StringMatch>,
+    workspace: ViewHandle<Workspace>,
+    selected_index: usize,
+    last_query: String,
+}
+
+impl PickerDelegate for BranchListDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select branch...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        cx.spawn(move |picker, mut cx| async move {
+            let Some(candidates) = picker
+                .read_with(&mut cx, |view, cx| {
+                    let delegate = view.delegate();
+                    let project = delegate.workspace.read(cx).project().read(&cx);
+                    let mut cwd =
+                    project
+                        .visible_worktrees(cx)
+                        .next()
+                        .unwrap()
+                        .read(cx)
+                        .abs_path()
+                        .to_path_buf();
+                    cwd.push(".git");
+                    let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
+                    let mut branches = repo
+                        .lock()
+                        .branches()?;
+                    const RECENT_BRANCHES_COUNT: usize = 10;
+                    if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+                        // Truncate list of recent branches
+                        // Do a partial sort to show recent-ish branches first.
+                        branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+                            rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+                        });
+                        branches.truncate(RECENT_BRANCHES_COUNT);
+                        branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+                    }
+                    Ok(branches
+                        .iter()
+                        .cloned()
+                        .enumerate()
+                        .map(|(ix, command)| StringMatchCandidate {
+                            id: ix,
+                            char_bag: command.name.chars().collect(),
+                            string: command.name.into(),
+                        })
+                        .collect::<Vec<_>>())
+                })
+                .log_err() else { return; };
+            let Some(candidates) = candidates.log_err() else {return;};
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                fuzzy::match_strings(
+                    &candidates,
+                    &query,
+                    true,
+                    10000,
+                    &Default::default(),
+                    cx.background(),
+                )
+                .await
+            };
+            picker
+                .update(&mut cx, |picker, _| {
+                    let delegate = picker.delegate_mut();
+                    delegate.matches = matches;
+                    if delegate.matches.is_empty() {
+                        delegate.selected_index = 0;
+                    } else {
+                        delegate.selected_index =
+                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+                    }
+                    delegate.last_query = query;
+                })
+                .log_err();
+        })
+    }
+
+    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        let current_pick = self.selected_index();
+        let current_pick = self.matches[current_pick].string.clone();
+        cx.spawn(|picker, mut cx| async move {
+            picker.update(&mut cx, |this, cx| {
+                let project = this.delegate().workspace.read(cx).project().read(cx);
+                let mut cwd = project
+                .visible_worktrees(cx)
+                .next()
+                .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+                .read(cx)
+                .abs_path()
+                .to_path_buf();
+                cwd.push(".git");
+                let status = project
+                    .fs()
+                    .open_repo(&cwd)
+                    .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
+                    .lock()
+                    .change_branch(&current_pick);
+                if status.is_err() {
+                    const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+                    this.delegate().workspace.update(cx, |model, ctx| {
+                        model.show_toast(
+                            Toast::new(
+                                GIT_CHECKOUT_FAILURE_ID,
+                                format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
+                            ),
+                            ctx,
+                        )
+                    });
+                    status?;
+                }
+                cx.emit(PickerEvent::Dismiss);
+
+                Ok::<(), anyhow::Error>(())
+            }).log_err();
+        }).detach();
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        cx.emit(PickerEvent::Dismiss);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> AnyElement<Picker<Self>> {
+        const DISPLAYED_MATCH_LEN: usize = 29;
+        let theme = &theme::current(cx);
+        let hit = &self.matches[ix];
+        let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
+        let highlights = hit
+            .positions
+            .iter()
+            .copied()
+            .filter(|index| index < &DISPLAYED_MATCH_LEN)
+            .collect();
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+        Flex::row()
+            .with_child(
+                Label::new(shortened_branch_name.clone(), style.label.clone())
+                    .with_highlights(highlights)
+                    .contained()
+                    .aligned()
+                    .left(),
+            )
+            .contained()
+            .with_style(style.container)
+            .constrained()
+            .with_height(theme.contact_finder.row_height)
+            .into_any()
+    }
+    fn render_header(
+        &self,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
+        let theme = &theme::current(cx);
+        let style = theme.picker.header.clone();
+        let label = if self.last_query.is_empty() {
+            Flex::row()
+                .with_child(Label::new("Recent branches", style.label.clone()))
+                .contained()
+                .with_style(style.container)
+        } else {
+            Flex::row()
+                .with_child(Label::new("Branches", style.label.clone()))
+                .with_children(self.matches.is_empty().not().then(|| {
+                    let suffix = if self.matches.len() == 1 { "" } else { "es" };
+                    Label::new(
+                        format!("{} match{}", self.matches.len(), suffix),
+                        style.label,
+                    )
+                    .flex_float()
+                }))
+                .contained()
+                .with_style(style.container)
+        };
+        Some(label.into_any())
+    }
+}

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -1,6 +1,10 @@
 use crate::{
-    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
-    toggle_screen_sharing, ToggleScreenSharing,
+    branch_list::{build_branch_list, BranchList},
+    contact_notification::ContactNotification,
+    contacts_popover,
+    face_pile::FacePile,
+    toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
+    ToggleScreenSharing,
 };
 use call::{ActiveCall, ParticipantLocation, Room};
 use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
@@ -17,19 +21,25 @@ use gpui::{
     AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
-use project::Project;
+use picker::PickerEvent;
+use project::{Project, RepositoryEntry};
+use recent_projects::{build_recent_projects, RecentProjects};
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
-use workspace::{FollowNextCollaborator, Workspace};
+use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
 
-const MAX_TITLE_LENGTH: usize = 75;
+const MAX_PROJECT_NAME_LENGTH: usize = 40;
+const MAX_BRANCH_NAME_LENGTH: usize = 40;
 
 actions!(
     collab,
     [
         ToggleContactsMenu,
         ToggleUserMenu,
+        ToggleVcsMenu,
+        ToggleProjectMenu,
+        SwitchBranch,
         ShareProject,
         UnshareProject,
     ]
@@ -40,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(CollabTitlebarItem::share_project);
     cx.add_action(CollabTitlebarItem::unshare_project);
     cx.add_action(CollabTitlebarItem::toggle_user_menu);
+    cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+    cx.add_action(CollabTitlebarItem::toggle_project_menu);
 }
 
 pub struct CollabTitlebarItem {
@@ -48,6 +60,8 @@ pub struct CollabTitlebarItem {
     client: Arc<Client>,
     workspace: WeakViewHandle<Workspace>,
     contacts_popover: Option<ViewHandle<ContactsPopover>>,
+    branch_popover: Option<ViewHandle<BranchList>>,
+    project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
     user_menu: ViewHandle<ContextMenu>,
     _subscriptions: Vec<Subscription>,
 }
@@ -68,37 +82,43 @@ impl View for CollabTitlebarItem {
             return Empty::new().into_any();
         };
 
-        let project = self.project.read(cx);
         let theme = theme::current(cx).clone();
         let mut left_container = Flex::row();
         let mut right_container = Flex::row().align_children_center();
 
-        left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
+        left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
 
         let user = self.user_store.read(cx).current_user();
         let peer_id = self.client.peer_id();
         if let Some(((user, peer_id), room)) = user
+            .as_ref()
             .zip(peer_id)
             .zip(ActiveCall::global(cx).read(cx).room().cloned())
         {
-            left_container
-                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
-
-            right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
             right_container
-                .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
+                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
+            right_container.add_child(self.render_leave_call(&theme, cx));
+            let muted = room.read(cx).is_muted();
+            let speaking = room.read(cx).is_speaking();
+            left_container.add_child(
+                self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
+            );
+            left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
+            right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
+            right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
             right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
         }
 
         let status = workspace.read(cx).client().status();
         let status = &*status.borrow();
-
         if matches!(status, client::Status::Connected { .. }) {
             right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
-            right_container.add_child(self.render_user_menu_button(&theme, cx));
+            let avatar = user.as_ref().and_then(|user| user.avatar.clone());
+            right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
         } else {
             right_container.add_children(self.render_connection_status(status, cx));
             right_container.add_child(self.render_sign_in_button(&theme, cx));
+            right_container.add_child(self.render_user_menu_button(&theme, None, cx));
         }
 
         Stack::new()
@@ -108,7 +128,6 @@ impl View for CollabTitlebarItem {
                     .with_child(
                         right_container.contained().with_background_color(
                             theme
-                                .workspace
                                 .titlebar
                                 .container
                                 .background_color
@@ -163,7 +182,6 @@ impl CollabTitlebarItem {
             }),
         );
 
-        let view_id = cx.view_id();
         Self {
             workspace: workspace.weak_handle(),
             project,
@@ -171,69 +189,105 @@ impl CollabTitlebarItem {
             client,
             contacts_popover: None,
             user_menu: cx.add_view(|cx| {
+                let view_id = cx.view_id();
                 let mut menu = ContextMenu::new(view_id, cx);
                 menu.set_position_mode(OverlayPositionMode::Local);
                 menu
             }),
+            branch_popover: None,
+            project_popover: None,
             _subscriptions: subscriptions,
         }
     }
 
     fn collect_title_root_names(
         &self,
-        project: &Project,
         theme: Arc<Theme>,
-        cx: &ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
-            let worktree = worktree.read(cx);
-            (worktree.root_name(), worktree.root_git_entry())
-        });
-
-        fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
-            buffer.push_str(str);
-            *index += str.chars().count();
-        }
-
-        let mut indices = Vec::new();
-        let mut index = 0;
-        let mut title = String::new();
-        let mut names_and_branches = names_and_branches.peekable();
-        while let Some((name, entry)) = names_and_branches.next() {
-            let pre_index = index;
-            push_str(&mut title, &mut index, name);
-            indices.extend((pre_index..index).into_iter());
-            if let Some(branch) = entry.and_then(|entry| entry.branch()) {
-                push_str(&mut title, &mut index, "/");
-                push_str(&mut title, &mut index, &branch);
-            }
-            if names_and_branches.peek().is_some() {
-                push_str(&mut title, &mut index, ", ");
-                if index >= MAX_TITLE_LENGTH {
-                    title.push_str(" …");
-                    break;
-                }
-            }
-        }
-
-        let text_style = theme.workspace.titlebar.title.clone();
-        let item_spacing = theme.workspace.titlebar.item_spacing;
+        let project = self.project.read(cx);
 
-        let mut highlight = text_style.clone();
-        highlight.color = theme.workspace.titlebar.highlight_color;
+        let (name, entry) = {
+            let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+                let worktree = worktree.read(cx);
+                (worktree.root_name(), worktree.root_git_entry())
+            });
 
-        let style = LabelStyle {
-            text: text_style,
-            highlight_text: Some(highlight),
+            names_and_branches.next().unwrap_or(("", None))
         };
 
-        Label::new(title, style)
-            .with_highlights(indices)
-            .contained()
-            .with_margin_right(item_spacing)
-            .aligned()
-            .left()
-            .into_any_named("title-with-git-information")
+        let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
+        let branch_prepended = entry
+            .as_ref()
+            .and_then(RepositoryEntry::branch)
+            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+        let project_style = theme.titlebar.project_menu_button.clone();
+        let git_style = theme.titlebar.git_menu_button.clone();
+        let divider_style = theme.titlebar.project_name_divider.clone();
+        let item_spacing = theme.titlebar.item_spacing;
+
+        let mut ret = Flex::row().with_child(
+            Stack::new()
+                .with_child(
+                    MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
+                        let style = project_style
+                            .in_state(self.project_popover.is_some())
+                            .style_for(mouse_state);
+                        Label::new(name, style.text.clone())
+                            .contained()
+                            .with_style(style.container)
+                            .aligned()
+                            .left()
+                            .into_any_named("title-project-name")
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_down(MouseButton::Left, move |_, this, cx| {
+                        this.toggle_project_menu(&Default::default(), cx)
+                    })
+                    .on_click(MouseButton::Left, move |_, _, _| {}),
+                )
+                .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
+        );
+        if let Some(git_branch) = branch_prepended {
+            ret = ret.with_child(
+                Flex::row()
+                    .with_child(
+                        Label::new("/", divider_style.text)
+                            .contained()
+                            .with_style(divider_style.container)
+                            .aligned()
+                            .left(),
+                    )
+                    .with_child(
+                        Stack::new()
+                            .with_child(
+                                MouseEventHandler::<ToggleVcsMenu, Self>::new(
+                                    0,
+                                    cx,
+                                    |mouse_state, _| {
+                                        let style = git_style
+                                            .in_state(self.branch_popover.is_some())
+                                            .style_for(mouse_state);
+                                        Label::new(git_branch, style.text.clone())
+                                            .contained()
+                                            .with_style(style.container.clone())
+                                            .with_margin_right(item_spacing)
+                                            .aligned()
+                                            .left()
+                                            .into_any_named("title-project-branch")
+                                    },
+                                )
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_down(MouseButton::Left, move |_, this, cx| {
+                                    this.toggle_vcs_menu(&Default::default(), cx)
+                                })
+                                .on_click(MouseButton::Left, move |_, _, _| {}),
+                            )
+                            .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+                    ),
+            )
+        }
+        ret.into_any()
     }
 
     fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
@@ -297,55 +351,167 @@ impl CollabTitlebarItem {
     }
 
     pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
-        let theme = theme::current(cx).clone();
-        let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
-        let item_style = theme.context_menu.item.disabled_style().clone();
         self.user_menu.update(cx, |user_menu, cx| {
-            let items = if let Some(user) = self.user_store.read(cx).current_user() {
+            let items = if let Some(_) = self.user_store.read(cx).current_user() {
                 vec![
-                    ContextMenuItem::Static(Box::new(move |_| {
-                        Flex::row()
-                            .with_children(user.avatar.clone().map(|avatar| {
-                                Self::render_face(
-                                    avatar,
-                                    avatar_style.clone(),
-                                    Color::transparent_black(),
-                                )
-                            }))
-                            .with_child(Label::new(
-                                user.github_login.clone(),
-                                item_style.label.clone(),
-                            ))
-                            .contained()
-                            .with_style(item_style.container)
-                            .into_any()
-                    })),
-                    ContextMenuItem::action("Sign out", SignOut),
+                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+                    ContextMenuItem::action("Theme", theme_selector::Toggle),
+                    ContextMenuItem::separator(),
                     ContextMenuItem::action(
-                        "Send Feedback",
+                        "Share Feedback",
                         feedback::feedback_editor::GiveFeedback,
                     ),
+                    ContextMenuItem::action("Sign out", SignOut),
                 ]
             } else {
                 vec![
-                    ContextMenuItem::action("Sign in", SignIn),
+                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
+                    ContextMenuItem::action("Theme", theme_selector::Toggle),
+                    ContextMenuItem::separator(),
                     ContextMenuItem::action(
-                        "Send Feedback",
+                        "Share Feedback",
                         feedback::feedback_editor::GiveFeedback,
                     ),
                 ]
             };
-
-            user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
+            user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
         });
     }
+    fn render_branches_popover_host<'a>(
+        &'a self,
+        _theme: &'a theme::Titlebar,
+        cx: &'a mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        self.branch_popover.as_ref().map(|child| {
+            let theme = theme::current(cx).clone();
+            let child = ChildView::new(child, cx);
+            let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
+                child
+                    .flex(1., true)
+                    .contained()
+                    .constrained()
+                    .with_width(theme.contacts_popover.width)
+                    .with_height(theme.contacts_popover.height)
+            })
+            .on_click(MouseButton::Left, |_, _, _| {})
+            .on_down_out(MouseButton::Left, move |_, this, cx| {
+                this.branch_popover.take();
+                cx.emit(());
+                cx.notify();
+            })
+            .contained()
+            .into_any();
+
+            Overlay::new(child)
+                .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                .with_anchor_corner(AnchorCorner::TopLeft)
+                .with_z_index(999)
+                .aligned()
+                .bottom()
+                .left()
+                .into_any()
+        })
+    }
+    fn render_project_popover_host<'a>(
+        &'a self,
+        _theme: &'a theme::Titlebar,
+        cx: &'a mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        self.project_popover.as_ref().map(|child| {
+            let theme = theme::current(cx).clone();
+            let child = ChildView::new(child, cx);
+            let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
+                child
+                    .flex(1., true)
+                    .contained()
+                    .constrained()
+                    .with_width(theme.contacts_popover.width)
+                    .with_height(theme.contacts_popover.height)
+            })
+            .on_click(MouseButton::Left, |_, _, _| {})
+            .on_down_out(MouseButton::Left, move |_, this, cx| {
+                this.project_popover.take();
+                cx.emit(());
+                cx.notify();
+            })
+            .into_any();
+
+            Overlay::new(child)
+                .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                .with_anchor_corner(AnchorCorner::TopLeft)
+                .with_z_index(999)
+                .aligned()
+                .bottom()
+                .left()
+                .into_any()
+        })
+    }
+    pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+        if self.branch_popover.take().is_none() {
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                let view = cx.add_view(|cx| build_branch_list(workspace, cx));
+                cx.subscribe(&view, |this, _, event, cx| {
+                    match event {
+                        PickerEvent::Dismiss => {
+                            this.branch_popover = None;
+                        }
+                    }
+
+                    cx.notify();
+                })
+                .detach();
+                self.project_popover.take();
+                cx.focus(&view);
+                self.branch_popover = Some(view);
+            }
+        }
+
+        cx.notify();
+    }
+
+    pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+        let workspace = self.workspace.clone();
+        if self.project_popover.take().is_none() {
+            cx.spawn(|this, mut cx| async move {
+                let workspaces = WORKSPACE_DB
+                    .recent_workspaces_on_disk()
+                    .await
+                    .unwrap_or_default()
+                    .into_iter()
+                    .map(|(_, location)| location)
+                    .collect();
+
+                let workspace = workspace.clone();
+                this.update(&mut cx, move |this, cx| {
+                    let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+                    cx.subscribe(&view, |this, _, event, cx| {
+                        match event {
+                            PickerEvent::Dismiss => {
+                                this.project_popover = None;
+                            }
+                        }
 
+                        cx.notify();
+                    })
+                    .detach();
+                    cx.focus(&view);
+                    this.branch_popover.take();
+                    this.project_popover = Some(view);
+                    cx.notify();
+                })
+                .log_err();
+            })
+            .detach();
+        }
+        cx.notify();
+    }
     fn render_toggle_contacts_button(
         &self,
         theme: &Theme,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        let titlebar = &theme.workspace.titlebar;
+        let titlebar = &theme.titlebar;
 
         let badge = if self
             .user_store
@@ -361,8 +527,20 @@ impl CollabTitlebarItem {
                     .contained()
                     .with_style(titlebar.toggle_contacts_badge)
                     .contained()
-                    .with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
-                    .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
+                    .with_margin_left(
+                        titlebar
+                            .toggle_contacts_button
+                            .inactive_state()
+                            .default
+                            .icon_width,
+                    )
+                    .with_margin_top(
+                        titlebar
+                            .toggle_contacts_button
+                            .inactive_state()
+                            .default
+                            .icon_width,
+                    )
                     .aligned(),
             )
         };
@@ -372,8 +550,9 @@ impl CollabTitlebarItem {
                 MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
                     let style = titlebar
                         .toggle_contacts_button
-                        .style_for(state, self.contacts_popover.is_some());
-                    Svg::new("icons/user_plus_16.svg")
+                        .in_state(self.contacts_popover.is_some())
+                        .style_for(state);
+                    Svg::new("icons/radix/person.svg")
                         .with_color(style.color)
                         .constrained()
                         .with_width(style.icon_width)
@@ -400,7 +579,6 @@ impl CollabTitlebarItem {
             .with_children(self.render_contacts_popover_host(titlebar, cx))
             .into_any()
     }
-
     fn render_toggle_screen_sharing_button(
         &self,
         theme: &Theme,
@@ -410,16 +588,21 @@ impl CollabTitlebarItem {
         let icon;
         let tooltip;
         if room.read(cx).is_screen_sharing() {
-            icon = "icons/enable_screen_sharing_12.svg";
+            icon = "icons/radix/desktop.svg";
             tooltip = "Stop Sharing Screen"
         } else {
-            icon = "icons/disable_screen_sharing_12.svg";
+            icon = "icons/radix/desktop.svg";
             tooltip = "Share Screen";
         }
 
-        let titlebar = &theme.workspace.titlebar;
+        let active = room.read(cx).is_screen_sharing();
+        let titlebar = &theme.titlebar;
         MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
-            let style = titlebar.call_control.style_for(state, false);
+            let style = titlebar
+                .screen_share_button
+                .in_state(active)
+                .style_for(state);
+
             Svg::new(icon)
                 .with_color(style.color)
                 .constrained()
@@ -445,7 +628,141 @@ impl CollabTitlebarItem {
         .aligned()
         .into_any()
     }
+    fn render_toggle_mute(
+        &self,
+        theme: &Theme,
+        room: &ModelHandle<Room>,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let icon;
+        let tooltip;
+        let is_muted = room.read(cx).is_muted();
+        if is_muted {
+            icon = "icons/radix/mic-mute.svg";
+            tooltip = "Unmute microphone\nRight click for options";
+        } else {
+            icon = "icons/radix/mic.svg";
+            tooltip = "Mute microphone\nRight click for options";
+        }
+
+        let titlebar = &theme.titlebar;
+        MouseEventHandler::<ToggleMute, Self>::new(0, cx, |state, _| {
+            let style = titlebar
+                .toggle_microphone_button
+                .in_state(is_muted)
+                .style_for(state);
+            let image = Svg::new(icon)
+                .with_color(style.color)
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .with_height(style.button_width)
+                .contained()
+                .with_style(style.container);
+            if let Some(color) = style.container.background_color {
+                image.with_background_color(color)
+            } else {
+                image
+            }
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, _, cx| {
+            toggle_mute(&Default::default(), cx)
+        })
+        .with_tooltip::<ToggleMute>(
+            0,
+            tooltip.into(),
+            Some(Box::new(ToggleMute)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .into_any()
+    }
+    fn render_toggle_deafen(
+        &self,
+        theme: &Theme,
+        room: &ModelHandle<Room>,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let icon;
+        let tooltip;
+        let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
+        if is_deafened {
+            icon = "icons/radix/speaker-off.svg";
+            tooltip = "Unmute speakers\nRight click for options";
+        } else {
+            icon = "icons/radix/speaker-loud.svg";
+            tooltip = "Mute speakers\nRight click for options";
+        }
 
+        let titlebar = &theme.titlebar;
+        MouseEventHandler::<ToggleDeafen, Self>::new(0, cx, |state, _| {
+            let style = titlebar
+                .toggle_speakers_button
+                .in_state(is_deafened)
+                .style_for(state);
+            Svg::new(icon)
+                .with_color(style.color)
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .with_height(style.button_width)
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, _, cx| {
+            toggle_deafen(&Default::default(), cx)
+        })
+        .with_tooltip::<ToggleDeafen>(
+            0,
+            tooltip.into(),
+            Some(Box::new(ToggleDeafen)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .into_any()
+    }
+    fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let icon = "icons/radix/exit.svg";
+        let tooltip = "Leave call";
+
+        let titlebar = &theme.titlebar;
+        MouseEventHandler::<LeaveCall, Self>::new(0, cx, |state, _| {
+            let style = titlebar.leave_call_button.style_for(state);
+            Svg::new(icon)
+                .with_color(style.color)
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .with_height(style.button_width)
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, _, cx| {
+            ActiveCall::global(cx)
+                .update(cx, |call, cx| call.hang_up(cx))
+                .detach_and_log_err(cx);
+        })
+        .with_tooltip::<LeaveCall>(
+            0,
+            tooltip.into(),
+            Some(Box::new(LeaveCall)),
+            theme.tooltip.clone(),
+            cx,
+        )
+        .aligned()
+        .into_any()
+    }
     fn render_in_call_share_unshare_button(
         &self,
         workspace: &ViewHandle<Workspace>,
@@ -458,14 +775,14 @@ impl CollabTitlebarItem {
         }
 
         let is_shared = project.read(cx).is_shared();
-        let label = if is_shared { "Unshare" } else { "Share" };
+        let label = if is_shared { "Stop Sharing" } else { "Share" };
         let tooltip = if is_shared {
-            "Unshare project from call participants"
+            "Stop sharing project with call participants"
         } else {
             "Share project with call participants"
         };
 
-        let titlebar = &theme.workspace.titlebar;
+        let titlebar = &theme.titlebar;
 
         enum ShareUnshare {}
         Some(
@@ -473,7 +790,7 @@ impl CollabTitlebarItem {
                 .with_child(
                     MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
                         //TODO: Ensure this button has consistent width for both text variations
-                        let style = titlebar.share_button.style_for(state, false);
+                        let style = titlebar.share_button.inactive_state().style_for(state);
                         Label::new(label, style.text.clone())
                             .contained()
                             .with_style(style.container)
@@ -496,7 +813,7 @@ impl CollabTitlebarItem {
                 )
                 .aligned()
                 .contained()
-                .with_margin_left(theme.workspace.titlebar.item_spacing)
+                .with_margin_left(theme.titlebar.item_spacing)
                 .into_any(),
         )
     }
@@ -504,26 +821,56 @@ impl CollabTitlebarItem {
     fn render_user_menu_button(
         &self,
         theme: &Theme,
+        avatar: Option<Arc<ImageData>>,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        let titlebar = &theme.workspace.titlebar;
+        let tooltip = theme.tooltip.clone();
+        let user_menu_button_style = if avatar.is_some() {
+            &theme.titlebar.user_menu.user_menu_button_online
+        } else {
+            &theme.titlebar.user_menu.user_menu_button_offline
+        };
 
+        let avatar_style = &user_menu_button_style.avatar;
         Stack::new()
             .with_child(
                 MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
-                    let style = titlebar.call_control.style_for(state, false);
-                    Svg::new("icons/ellipsis_14.svg")
-                        .with_color(style.color)
-                        .constrained()
-                        .with_width(style.icon_width)
+                    let style = user_menu_button_style
+                        .user_menu
+                        .inactive_state()
+                        .style_for(state);
+
+                    let mut dropdown = Flex::row().align_children_center();
+
+                    if let Some(avatar_img) = avatar {
+                        dropdown = dropdown.with_child(Self::render_face(
+                            avatar_img,
+                            *avatar_style,
+                            Color::transparent_black(),
+                            None,
+                        ));
+                    };
+
+                    dropdown
+                        .with_child(
+                            Svg::new("icons/caret_down_8.svg")
+                                .with_color(user_menu_button_style.icon.color)
+                                .constrained()
+                                .with_width(user_menu_button_style.icon.width)
+                                .contained()
+                                .into_any(),
+                        )
                         .aligned()
                         .constrained()
-                        .with_width(style.button_width)
-                        .with_height(style.button_width)
+                        .with_height(style.width)
                         .contained()
                         .with_style(style.container)
+                        .into_any()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
+                .on_down(MouseButton::Left, move |_, this, cx| {
+                    this.user_menu.update(cx, |menu, _| menu.delay_cancel());
+                })
                 .on_click(MouseButton::Left, move |_, this, cx| {
                     this.toggle_user_menu(&Default::default(), cx)
                 })
@@ -531,11 +878,10 @@ impl CollabTitlebarItem {
                     0,
                     "Toggle user menu".to_owned(),
                     Some(Box::new(ToggleUserMenu)),
-                    theme.tooltip.clone(),
+                    tooltip,
                     cx,
                 )
-                .contained()
-                .with_margin_left(theme.workspace.titlebar.item_spacing),
+                .contained(),
             )
             .with_child(
                 ChildView::new(&self.user_menu, cx)
@@ -547,9 +893,9 @@ impl CollabTitlebarItem {
     }
 
     fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let titlebar = &theme.workspace.titlebar;
+        let titlebar = &theme.titlebar;
         MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
-            let style = titlebar.sign_in_prompt.style_for(state, false);
+            let style = titlebar.sign_in_button.inactive_state().style_for(state);
             Label::new("Sign In", style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -572,7 +918,7 @@ impl CollabTitlebarItem {
         self.contacts_popover.as_ref().map(|popover| {
             Overlay::new(ChildView::new(popover, cx))
                 .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopRight)
+                .with_anchor_corner(AnchorCorner::TopLeft)
                 .with_z_index(999)
                 .aligned()
                 .bottom()
@@ -611,11 +957,13 @@ impl CollabTitlebarItem {
                         replica_id,
                         participant.peer_id,
                         Some(participant.location),
+                        participant.muted,
+                        participant.speaking,
                         workspace,
                         theme,
                         cx,
                     ))
-                    .with_margin_right(theme.workspace.titlebar.face_pile_spacing),
+                    .with_margin_right(theme.titlebar.face_pile_spacing),
                 )
             })
             .collect()
@@ -627,19 +975,24 @@ impl CollabTitlebarItem {
         theme: &Theme,
         user: &Arc<User>,
         peer_id: PeerId,
+        muted: bool,
+        speaking: bool,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         let replica_id = workspace.read(cx).project().read(cx).replica_id();
+
         Container::new(self.render_face_pile(
             user,
             Some(replica_id),
             peer_id,
             None,
+            muted,
+            speaking,
             workspace,
             theme,
             cx,
         ))
-        .with_margin_right(theme.workspace.titlebar.item_spacing)
+        .with_margin_right(theme.titlebar.item_spacing)
         .into_any()
     }
 
@@ -649,6 +1002,8 @@ impl CollabTitlebarItem {
         replica_id: Option<ReplicaId>,
         peer_id: PeerId,
         location: Option<ParticipantLocation>,
+        muted: bool,
+        speaking: bool,
         workspace: &ViewHandle<Workspace>,
         theme: &Theme,
         cx: &mut ViewContext<Self>,
@@ -671,15 +1026,23 @@ impl CollabTitlebarItem {
             })
             .unwrap_or(false);
 
-        let leader_style = theme.workspace.titlebar.leader_avatar;
-        let follower_style = theme.workspace.titlebar.follower_avatar;
+        let leader_style = theme.titlebar.leader_avatar;
+        let follower_style = theme.titlebar.follower_avatar;
+
+        let microphone_state = if muted {
+            Some(theme.titlebar.muted)
+        } else if speaking {
+            Some(theme.titlebar.speaking)
+        } else {
+            None
+        };
 
         let mut background_color = theme
-            .workspace
             .titlebar
             .container
             .background_color
             .unwrap_or_default();
+
         if let Some(replica_id) = replica_id {
             if followed_by_self {
                 let selection = theme.editor.replica_selection_style(replica_id).selection;
@@ -690,11 +1053,12 @@ impl CollabTitlebarItem {
 
         let mut content = Stack::new()
             .with_children(user.avatar.as_ref().map(|avatar| {
-                let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
+                let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
                     .with_child(Self::render_face(
                         avatar.clone(),
                         Self::location_style(workspace, location, leader_style, cx),
                         background_color,
+                        microphone_state,
                     ))
                     .with_children(
                         (|| {
@@ -726,6 +1090,7 @@ impl CollabTitlebarItem {
                                     avatar.clone(),
                                     follower_style,
                                     background_color,
+                                    None,
                                 ))
                             }))
                         })()
@@ -735,7 +1100,7 @@ impl CollabTitlebarItem {
 
                 let mut container = face_pile
                     .contained()
-                    .with_style(theme.workspace.titlebar.leader_selection);
+                    .with_style(theme.titlebar.leader_selection);
 
                 if let Some(replica_id) = replica_id {
                     if followed_by_self {
@@ -752,8 +1117,8 @@ impl CollabTitlebarItem {
                 Some(
                     AvatarRibbon::new(color)
                         .constrained()
-                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
-                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
+                        .with_width(theme.titlebar.avatar_ribbon.width)
+                        .with_height(theme.titlebar.avatar_ribbon.height)
                         .aligned()
                         .bottom(),
                 )
@@ -844,12 +1209,13 @@ impl CollabTitlebarItem {
         avatar: Arc<ImageData>,
         avatar_style: AvatarStyle,
         background_color: Color,
+        microphone_state: Option<Color>,
     ) -> AnyElement<V> {
         Image::from_data(avatar)
             .with_style(avatar_style.image)
             .aligned()
             .contained()
-            .with_background_color(background_color)
+            .with_background_color(microphone_state.unwrap_or(background_color))
             .with_corner_radius(avatar_style.outer_corner_radius)
             .constrained()
             .with_width(avatar_style.outer_width)
@@ -873,22 +1239,22 @@ impl CollabTitlebarItem {
             | client::Status::Reconnecting { .. }
             | client::Status::ReconnectionError { .. } => Some(
                 Svg::new("icons/cloud_slash_12.svg")
-                    .with_color(theme.workspace.titlebar.offline_icon.color)
+                    .with_color(theme.titlebar.offline_icon.color)
                     .constrained()
-                    .with_width(theme.workspace.titlebar.offline_icon.width)
+                    .with_width(theme.titlebar.offline_icon.width)
                     .aligned()
                     .contained()
-                    .with_style(theme.workspace.titlebar.offline_icon.container)
+                    .with_style(theme.titlebar.offline_icon.container)
                     .into_any(),
             ),
             client::Status::UpgradeRequired => Some(
                 MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
                     Label::new(
                         "Please update Zed to collaborate",
-                        theme.workspace.titlebar.outdated_warning.text.clone(),
+                        theme.titlebar.outdated_warning.text.clone(),
                     )
                     .contained()
-                    .with_style(theme.workspace.titlebar.outdated_warning.container)
+                    .with_style(theme.titlebar.outdated_warning.container)
                     .aligned()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)

crates/collab_ui/src/collab_ui.rs πŸ”—

@@ -1,3 +1,4 @@
+mod branch_list;
 mod collab_titlebar_item;
 mod contact_finder;
 mod contact_list;
@@ -9,15 +10,26 @@ mod notifications;
 mod project_shared_notification;
 mod sharing_status_indicator;
 
-use call::ActiveCall;
+use call::{ActiveCall, Room};
 pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
 use gpui::{actions, AppContext, Task};
 use std::sync::Arc;
+use util::ResultExt;
 use workspace::AppState;
 
-actions!(collab, [ToggleScreenSharing]);
+actions!(
+    collab,
+    [
+        ToggleScreenSharing,
+        ToggleMute,
+        ToggleDeafen,
+        LeaveCall,
+        ShareMicrophone
+    ]
+);
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    branch_list::init(cx);
     collab_titlebar_item::init(cx);
     contact_list::init(cx);
     contact_finder::init(cx);
@@ -27,6 +39,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     sharing_status_indicator::init(cx);
 
     cx.add_global_action(toggle_screen_sharing);
+    cx.add_global_action(toggle_mute);
+    cx.add_global_action(toggle_deafen);
+    cx.add_global_action(share_microphone);
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -41,3 +56,26 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
         toggle_screen_sharing.detach_and_log_err(cx);
     }
 }
+
+pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
+    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+        room.update(cx, Room::toggle_mute)
+            .map(|task| task.detach_and_log_err(cx))
+            .log_err();
+    }
+}
+
+pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+        room.update(cx, Room::toggle_deafen)
+            .map(|task| task.detach_and_log_err(cx))
+            .log_err();
+    }
+}
+
+pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) {
+    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+        room.update(cx, Room::share_microphone)
+            .detach_and_log_err(cx)
+    }
+}

crates/collab_ui/src/contact_finder.rs πŸ”—

@@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate {
             .contact_finder
             .picker
             .item
-            .style_for(mouse_state, selected);
+            .in_state(selected)
+            .style_for(mouse_state);
         Flex::row()
             .with_children(user.avatar.clone().map(|avatar| {
                 Image::from_data(avatar)

crates/collab_ui/src/contact_list.rs πŸ”—

@@ -514,10 +514,10 @@ impl ContactList {
                         project_id: project.id,
                         worktree_root_names: project.worktree_root_names.clone(),
                         host_user_id: participant.user.id,
-                        is_last: projects.peek().is_none() && participant.tracks.is_empty(),
+                        is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
                     });
                 }
-                if !participant.tracks.is_empty() {
+                if !participant.video_tracks.is_empty() {
                     participant_entries.push(ContactEntry::ParticipantScreen {
                         peer_id: participant.peer_id,
                         is_last: true,
@@ -774,7 +774,8 @@ impl ContactList {
             .with_style(
                 *theme
                     .contact_row
-                    .style_for(&mut Default::default(), is_selected),
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
             )
             .into_any()
     }
@@ -797,7 +798,7 @@ impl ContactList {
             .width
             .or(theme.contact_avatar.height)
             .unwrap_or(0.);
-        let row = &theme.project_row.default;
+        let row = &theme.project_row.inactive_state().default;
         let tree_branch = theme.tree_branch;
         let line_height = row.name.text.line_height(font_cache);
         let cap_height = row.name.text.cap_height(font_cache);
@@ -810,8 +811,11 @@ impl ContactList {
         };
 
         MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
-            let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
-            let row = theme.project_row.style_for(mouse_state, is_selected);
+            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+            let row = theme
+                .project_row
+                .in_state(is_selected)
+                .style_for(mouse_state);
 
             Flex::row()
                 .with_child(
@@ -893,7 +897,7 @@ impl ContactList {
             .width
             .or(theme.contact_avatar.height)
             .unwrap_or(0.);
-        let row = &theme.project_row.default;
+        let row = &theme.project_row.inactive_state().default;
         let tree_branch = theme.tree_branch;
         let line_height = row.name.text.line_height(font_cache);
         let cap_height = row.name.text.cap_height(font_cache);
@@ -904,8 +908,11 @@ impl ContactList {
             peer_id.as_u64() as usize,
             cx,
             |mouse_state, _| {
-                let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
-                let row = theme.project_row.style_for(mouse_state, is_selected);
+                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+                let row = theme
+                    .project_row
+                    .in_state(is_selected)
+                    .style_for(mouse_state);
 
                 Flex::row()
                     .with_child(
@@ -989,7 +996,8 @@ impl ContactList {
 
         let header_style = theme
             .header_row
-            .style_for(&mut Default::default(), is_selected);
+            .in_state(is_selected)
+            .style_for(&mut Default::default());
         let text = match section {
             Section::ActiveCall => "Collaborators",
             Section::Requests => "Contact Requests",
@@ -999,7 +1007,7 @@ impl ContactList {
         let leave_call = if section == Section::ActiveCall {
             Some(
                 MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
-                    let style = theme.leave_call.style_for(state, false);
+                    let style = theme.leave_call.style_for(state);
                     Label::new("Leave Call", style.text.clone())
                         .contained()
                         .with_style(style.container)
@@ -1110,8 +1118,7 @@ impl ContactList {
                             contact.user.id as usize,
                             cx,
                             |mouse_state, _| {
-                                let button_style =
-                                    theme.contact_button.style_for(mouse_state, false);
+                                let button_style = theme.contact_button.style_for(mouse_state);
                                 render_icon_button(button_style, "icons/x_mark_8.svg")
                                     .aligned()
                                     .flex_float()
@@ -1146,7 +1153,8 @@ impl ContactList {
                     .with_style(
                         *theme
                             .contact_row
-                            .style_for(&mut Default::default(), is_selected),
+                            .in_state(is_selected)
+                            .style_for(&mut Default::default()),
                     )
             })
             .on_click(MouseButton::Left, move |_, this, cx| {
@@ -1204,7 +1212,7 @@ impl ContactList {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
-                        theme.contact_button.style_for(mouse_state, false)
+                        theme.contact_button.style_for(mouse_state)
                     };
                     render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
                 })
@@ -1227,7 +1235,7 @@ impl ContactList {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
-                        theme.contact_button.style_for(mouse_state, false)
+                        theme.contact_button.style_for(mouse_state)
                     };
                     render_icon_button(button_style, "icons/check_8.svg")
                         .aligned()
@@ -1250,7 +1258,7 @@ impl ContactList {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
-                        theme.contact_button.style_for(mouse_state, false)
+                        theme.contact_button.style_for(mouse_state)
                     };
                     render_icon_button(button_style, "icons/x_mark_8.svg")
                         .aligned()
@@ -1277,7 +1285,8 @@ impl ContactList {
             .with_style(
                 *theme
                     .contact_row
-                    .style_for(&mut Default::default(), is_selected),
+                    .in_state(is_selected)
+                    .style_for(&mut Default::default()),
             )
             .into_any()
     }

crates/collab_ui/src/notifications.rs πŸ”—

@@ -53,7 +53,7 @@ where
                 )
                 .with_child(
                     MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
-                        let style = theme.dismiss_button.style_for(state, false);
+                        let style = theme.dismiss_button.style_for(state);
                         Svg::new("icons/x_mark_8.svg")
                             .with_color(style.color)
                             .constrained()
@@ -93,7 +93,7 @@ where
                     .with_children(buttons.into_iter().enumerate().map(
                         |(ix, (message, handler))| {
                             MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
-                                let button = theme.button.style_for(state, false);
+                                let button = theme.button.style_for(state);
                                 Label::new(message, button.text.clone())
                                     .contained()
                                     .with_style(button.container)

crates/command_palette/src/command_palette.rs πŸ”—

@@ -185,8 +185,8 @@ impl PickerDelegate for CommandPaletteDelegate {
         let mat = &self.matches[ix];
         let command = &self.actions[mat.candidate_id];
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
-        let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+        let key_style = &theme.command_palette.key.in_state(selected);
         let keystroke_spacing = theme.command_palette.keystroke_spacing;
 
         Flex::row()

crates/context_menu/src/context_menu.rs πŸ”—

@@ -124,6 +124,7 @@ pub struct ContextMenu {
     items: Vec<ContextMenuItem>,
     selected_index: Option<usize>,
     visible: bool,
+    delay_cancel: bool,
     previously_focused_view_id: Option<usize>,
     parent_view_id: usize,
     _actions_observation: Subscription,
@@ -178,6 +179,7 @@ impl ContextMenu {
     pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
         Self {
             show_count: 0,
+            delay_cancel: false,
             anchor_position: Default::default(),
             anchor_corner: AnchorCorner::TopLeft,
             position_mode: OverlayPositionMode::Window,
@@ -232,15 +234,22 @@ impl ContextMenu {
         }
     }
 
+    pub fn delay_cancel(&mut self) {
+        self.delay_cancel = true;
+    }
+
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        self.reset(cx);
-        let show_count = self.show_count;
-        cx.defer(move |this, cx| {
-            if cx.handle().is_focused(cx) && this.show_count == show_count {
-                let window_id = cx.window_id();
-                (**cx).focus(window_id, this.previously_focused_view_id.take());
-            }
-        });
+        if !self.delay_cancel {
+            self.reset(cx);
+            let show_count = self.show_count;
+            cx.defer(move |this, cx| {
+                if cx.handle().is_focused(cx) && this.show_count == show_count {
+                    (**cx).focus(this.previously_focused_view_id.take());
+                }
+            });
+        } else {
+            self.delay_cancel = false;
+        }
     }
 
     fn reset(&mut self, cx: &mut ViewContext<Self>) {
@@ -293,6 +302,34 @@ impl ContextMenu {
         }
     }
 
+    pub fn toggle(
+        &mut self,
+        anchor_position: Vector2F,
+        anchor_corner: AnchorCorner,
+        items: Vec<ContextMenuItem>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.visible() {
+            self.cancel(&Cancel, cx);
+        } else {
+            let mut items = items.into_iter().peekable();
+            if items.peek().is_some() {
+                self.items = items.collect();
+                self.anchor_position = anchor_position;
+                self.anchor_corner = anchor_corner;
+                self.visible = true;
+                self.show_count += 1;
+                if !cx.is_self_focused() {
+                    self.previously_focused_view_id = cx.focused_view_id();
+                }
+                cx.focus_self();
+            } else {
+                self.visible = false;
+            }
+        }
+        cx.notify();
+    }
+
     pub fn show(
         &mut self,
         anchor_position: Vector2F,
@@ -328,10 +365,8 @@ impl ContextMenu {
                 Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
                     match item {
                         ContextMenuItem::Item { label, .. } => {
-                            let style = style.item.style_for(
-                                &mut Default::default(),
-                                Some(ix) == self.selected_index,
-                            );
+                            let style = style.item.in_state(self.selected_index == Some(ix));
+                            let style = style.style_for(&mut Default::default());
 
                             match label {
                                 ContextMenuItemLabel::String(label) => {
@@ -363,10 +398,8 @@ impl ContextMenu {
                     .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                         match item {
                             ContextMenuItem::Item { action, .. } => {
-                                let style = style.item.style_for(
-                                    &mut Default::default(),
-                                    Some(ix) == self.selected_index,
-                                );
+                                let style = style.item.in_state(self.selected_index == Some(ix));
+                                let style = style.style_for(&mut Default::default());
 
                                 match action {
                                     ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
@@ -412,8 +445,8 @@ impl ContextMenu {
                             let action = action.clone();
                             let view_id = self.parent_view_id;
                             MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
-                                let style =
-                                    style.item.style_for(state, Some(ix) == self.selected_index);
+                                let style = style.item.in_state(self.selected_index == Some(ix));
+                                let style = style.style_for(state);
                                 let keystroke = match &action {
                                     ContextMenuItemAction::Action(action) => Some(
                                         KeystrokeLabel::new(

crates/copilot/src/copilot.rs πŸ”—

@@ -15,7 +15,7 @@ use language::{
     ToPointUtf16,
 };
 use log::{debug, error};
-use lsp::{LanguageServer, LanguageServerId};
+use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
 use node_runtime::NodeRuntime;
 use request::{LogMessage, StatusNotification};
 use settings::SettingsStore;
@@ -340,7 +340,7 @@ impl Copilot {
         let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
         let this = cx.add_model(|cx| Self {
             http: http.clone(),
-            node_runtime: NodeRuntime::new(http, cx.background().clone()),
+            node_runtime: NodeRuntime::instance(http, cx.background().clone()),
             server: CopilotServer::Running(RunningCopilotServer {
                 lsp: Arc::new(server),
                 sign_in_status: SignInStatus::Authorized,
@@ -361,11 +361,14 @@ impl Copilot {
             let start_language_server = async {
                 let server_path = get_copilot_lsp(http).await?;
                 let node_path = node_runtime.binary_path().await?;
-                let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
+                let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
+                let binary = LanguageServerBinary {
+                    path: node_path,
+                    arguments,
+                };
                 let server = LanguageServer::new(
                     LanguageServerId(0),
-                    &node_path,
-                    arguments,
+                    binary,
                     Path::new("/"),
                     None,
                     cx.clone(),

crates/copilot/src/sign_in.rs πŸ”—

@@ -127,16 +127,16 @@ impl CopilotCodeVerification {
                 .with_child(
                     Label::new(
                         if copied { "Copied!" } else { "Copy" },
-                        device_code_style.cta.style_for(state, false).text.clone(),
+                        device_code_style.cta.style_for(state).text.clone(),
                     )
                     .aligned()
                     .contained()
-                    .with_style(*device_code_style.right_container.style_for(state, false))
+                    .with_style(*device_code_style.right_container.style_for(state))
                     .constrained()
                     .with_width(device_code_style.right),
                 )
                 .contained()
-                .with_style(device_code_style.cta.style_for(state, false).container)
+                .with_style(device_code_style.cta.style_for(state).container)
         })
         .on_click(gpui::platform::MouseButton::Left, {
             let user_code = data.user_code.clone();

crates/copilot_button/src/copilot_button.rs πŸ”—

@@ -71,7 +71,8 @@ impl View for CopilotButton {
                             .status_bar
                             .panel_buttons
                             .button
-                            .style_for(state, active);
+                            .in_state(active)
+                            .style_for(state);
 
                         Flex::row()
                             .with_child(
@@ -101,6 +102,9 @@ impl View for CopilotButton {
                     }
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
+                .on_down(MouseButton::Left, |_, this, cx| {
+                    this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
+                })
                 .on_click(MouseButton::Left, {
                     let status = status.clone();
                     move |_, this, cx| match status {
@@ -185,7 +189,7 @@ impl CopilotButton {
         }));
 
         self.popup_menu.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::BottomRight,
                 menu_options,
@@ -255,7 +259,7 @@ impl CopilotButton {
             move |state: &mut MouseState, style: &theme::ContextMenuItem| {
                 Flex::row()
                     .with_child(Label::new("Copilot Settings", style.label.clone()))
-                    .with_child(theme::ui::icon(icon_style.style_for(state, false)))
+                    .with_child(theme::ui::icon(icon_style.style_for(state)))
                     .align_children_center()
                     .into_any()
             },
@@ -265,7 +269,7 @@ impl CopilotButton {
         menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
 
         self.popup_menu.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::BottomRight,
                 menu_options,

crates/diagnostics/src/diagnostics.rs πŸ”—

@@ -1509,7 +1509,8 @@ mod tests {
             let snapshot = editor.snapshot(cx);
             snapshot
                 .blocks_in_range(0..snapshot.max_point().row())
-                .filter_map(|(row, block)| {
+                .enumerate()
+                .filter_map(|(ix, (row, block))| {
                     let name = match block {
                         TransformBlock::Custom(block) => block
                             .render(&mut BlockContext {
@@ -1520,6 +1521,7 @@ mod tests {
                                 gutter_width: 0.,
                                 line_height: 0.,
                                 em_width: 0.,
+                                block_id: ix,
                             })
                             .name()?
                             .to_string(),

crates/diagnostics/src/items.rs πŸ”—

@@ -100,7 +100,7 @@ impl View for DiagnosticIndicator {
                     .workspace
                     .status_bar
                     .diagnostic_summary
-                    .style_for(state, false);
+                    .style_for(state);
 
                 let mut summary_row = Flex::row();
                 if self.summary.error_count > 0 {
@@ -198,7 +198,7 @@ impl View for DiagnosticIndicator {
                 MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
                     Label::new(
                         diagnostic.message.split('\n').next().unwrap().to_string(),
-                        message_style.style_for(state, false).text.clone(),
+                        message_style.style_for(state).text.clone(),
                     )
                     .aligned()
                     .contained()

crates/editor/src/display_map.rs πŸ”—

@@ -1,24 +1,23 @@
 mod block_map;
 mod fold_map;
-mod suggestion_map;
+mod inlay_map;
 mod tab_map;
 mod wrap_map;
 
-use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
 pub use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
-use fold_map::{FoldMap, FoldOffset};
+use fold_map::FoldMap;
 use gpui::{
     color::Color,
     fonts::{FontId, HighlightStyle},
     Entity, ModelContext, ModelHandle,
 };
+use inlay_map::InlayMap;
 use language::{
     language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
 };
 use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
-pub use suggestion_map::Suggestion;
-use suggestion_map::SuggestionMap;
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
 use wrap_map::WrapMap;
@@ -28,6 +27,8 @@ pub use block_map::{
     BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
+pub use self::inlay_map::Inlay;
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum FoldStatus {
     Folded,
@@ -44,7 +45,7 @@ pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
     buffer_subscription: BufferSubscription,
     fold_map: FoldMap,
-    suggestion_map: SuggestionMap,
+    inlay_map: InlayMap,
     tab_map: TabMap,
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
@@ -69,8 +70,8 @@ impl DisplayMap {
         let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
 
         let tab_size = Self::tab_size(&buffer, cx);
-        let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
-        let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
+        let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        let (fold_map, snapshot) = FoldMap::new(snapshot);
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
         let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
         let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -79,7 +80,7 @@ impl DisplayMap {
             buffer,
             buffer_subscription,
             fold_map,
-            suggestion_map,
+            inlay_map,
             tab_map,
             wrap_map,
             block_map,
@@ -88,16 +89,13 @@ impl DisplayMap {
         }
     }
 
-    pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
+    pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
         let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
-        let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
-        let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
-
+        let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+        let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
         let tab_size = Self::tab_size(&self.buffer, cx);
-        let (tab_snapshot, edits) = self
-            .tab_map
-            .sync(suggestion_snapshot.clone(), edits, tab_size);
+        let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
         let (wrap_snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
@@ -106,7 +104,7 @@ impl DisplayMap {
         DisplaySnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(cx),
             fold_snapshot,
-            suggestion_snapshot,
+            inlay_snapshot,
             tab_snapshot,
             wrap_snapshot,
             block_snapshot,
@@ -132,15 +130,14 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.fold(ranges);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -157,15 +154,14 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -181,8 +177,8 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -199,8 +195,8 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -231,32 +227,6 @@ impl DisplayMap {
         self.text_highlights.remove(&Some(type_id))
     }
 
-    pub fn has_suggestion(&self) -> bool {
-        self.suggestion_map.has_suggestion()
-    }
-
-    pub fn replace_suggestion<T>(
-        &self,
-        new_suggestion: Option<Suggestion<T>>,
-        cx: &mut ModelContext<Self>,
-    ) -> Option<Suggestion<FoldOffset>>
-    where
-        T: ToPoint,
-    {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let edits = self.buffer_subscription.consume().into_inner();
-        let tab_size = Self::tab_size(&self.buffer, cx);
-        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits, old_suggestion) =
-            self.suggestion_map.replace(new_suggestion, snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
-        let (snapshot, edits) = self
-            .wrap_map
-            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
-        self.block_map.read(snapshot, edits);
-        old_suggestion
-    }
-
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_font(font_id, font_size, cx))
@@ -271,6 +241,39 @@ impl DisplayMap {
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
     }
 
+    pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+        self.inlay_map.current_inlays()
+    }
+
+    pub fn splice_inlays(
+        &mut self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<Inlay>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if to_remove.is_empty() && to_insert.is_empty() {
+            return;
+        }
+        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+
+        let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+    }
+
     fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
         let language = buffer
             .read(cx)
@@ -288,7 +291,7 @@ impl DisplayMap {
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
     fold_snapshot: fold_map::FoldSnapshot,
-    suggestion_snapshot: suggestion_map::SuggestionSnapshot,
+    inlay_snapshot: inlay_map::InlaySnapshot,
     tab_snapshot: tab_map::TabSnapshot,
     wrap_snapshot: wrap_map::WrapSnapshot,
     block_snapshot: block_map::BlockSnapshot,
@@ -316,9 +319,11 @@ impl DisplaySnapshot {
 
     pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
-            *fold_point.column_mut() = 0;
-            point = fold_point.to_buffer_point(&self.fold_snapshot);
+            let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+            let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
+            fold_point.0.column = 0;
+            inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+            point = self.inlay_snapshot.to_buffer_point(inlay_point);
 
             let mut display_point = self.point_to_display_point(point, Bias::Left);
             *display_point.column_mut() = 0;
@@ -332,9 +337,11 @@ impl DisplaySnapshot {
 
     pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
-            *fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
-            point = fold_point.to_buffer_point(&self.fold_snapshot);
+            let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+            let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
+            fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
+            inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+            point = self.inlay_snapshot.to_buffer_point(inlay_point);
 
             let mut display_point = self.point_to_display_point(point, Bias::Right);
             *display_point.column_mut() = self.line_len(display_point.row());
@@ -364,9 +371,9 @@ impl DisplaySnapshot {
     }
 
     fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
-        let fold_point = self.fold_snapshot.to_fold_point(point, bias);
-        let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
-        let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
+        let inlay_point = self.inlay_snapshot.to_inlay_point(point);
+        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)
@@ -376,9 +383,9 @@ impl DisplaySnapshot {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
         let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
-        let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
-        let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
-        fold_point.to_buffer_point(&self.fold_snapshot)
+        let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+        self.inlay_snapshot.to_buffer_point(inlay_point)
     }
 
     pub fn max_point(&self) -> DisplayPoint {
@@ -388,7 +395,13 @@ impl DisplaySnapshot {
     /// Returns text chunks starting at the given display row until the end of the file
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         self.block_snapshot
-            .chunks(display_row..self.max_point().row() + 1, false, None, None)
+            .chunks(
+                display_row..self.max_point().row() + 1,
+                false,
+                None,
+                None,
+                None,
+            )
             .map(|h| h.text)
     }
 
@@ -396,7 +409,7 @@ impl DisplaySnapshot {
     pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         (0..=display_row).into_iter().rev().flat_map(|row| {
             self.block_snapshot
-                .chunks(row..row + 1, false, None, None)
+                .chunks(row..row + 1, false, None, None, None)
                 .map(|h| h.text)
                 .collect::<Vec<_>>()
                 .into_iter()
@@ -408,13 +421,15 @@ impl DisplaySnapshot {
         &self,
         display_rows: Range<u32>,
         language_aware: bool,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> DisplayChunks<'_> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
             Some(&self.text_highlights),
-            suggestion_highlight,
+            hint_highlights,
+            suggestion_highlights,
         )
     }
 
@@ -790,9 +805,10 @@ impl DisplayPoint {
     pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
         let wrap_point = map.block_snapshot.to_wrap_point(self.0);
         let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
-        let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
-        let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
-        fold_point.to_buffer_offset(&map.fold_snapshot)
+        let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
+        map.inlay_snapshot
+            .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
     }
 }
 
@@ -1706,7 +1722,7 @@ pub mod tests {
     ) -> Vec<(String, Option<Color>, Option<Color>)> {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
-        for chunk in snapshot.chunks(rows, true, None) {
+        for chunk in snapshot.chunks(rows, true, None, None) {
             let syntax_color = chunk
                 .syntax_highlight_id
                 .and_then(|id| id.style(theme)?.color);

crates/editor/src/display_map/block_map.rs πŸ”—

@@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b, 'c> {
     pub gutter_padding: f32,
     pub em_width: f32,
     pub line_height: f32,
+    pub block_id: usize,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -243,7 +244,7 @@ impl BlockMap {
             // Preserve any old transforms that precede this edit.
             let old_start = WrapRow(edit.old.start);
             let new_start = WrapRow(edit.new.start);
-            new_transforms.push_tree(cursor.slice(&old_start, Bias::Left, &()), &());
+            new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
             if let Some(transform) = cursor.item() {
                 if transform.is_isomorphic() && old_start == cursor.end(&()) {
                     new_transforms.push(transform.clone(), &());
@@ -425,7 +426,7 @@ impl BlockMap {
             push_isomorphic(&mut new_transforms, extent_after_edit);
         }
 
-        new_transforms.push_tree(cursor.suffix(&()), &());
+        new_transforms.append(cursor.suffix(&()), &());
         debug_assert_eq!(
             new_transforms.summary().input_rows,
             wrap_snapshot.max_point().row() + 1
@@ -572,9 +573,15 @@ impl<'a> BlockMapWriter<'a> {
 impl BlockSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(0..self.transforms.summary().output_rows, false, None, None)
-            .map(|chunk| chunk.text)
-            .collect()
+        self.chunks(
+            0..self.transforms.summary().output_rows,
+            false,
+            None,
+            None,
+            None,
+        )
+        .map(|chunk| chunk.text)
+        .collect()
     }
 
     pub fn chunks<'a>(
@@ -582,7 +589,8 @@ impl BlockSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -615,7 +623,8 @@ impl BlockSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                suggestion_highlight,
+                hint_highlights,
+                suggestion_highlights,
             ),
             input_chunk: Default::default(),
             transforms: cursor,
@@ -988,7 +997,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::display_map::suggestion_map::SuggestionMap;
+    use crate::display_map::inlay_map::InlayMap;
     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
     use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
@@ -1029,9 +1038,9 @@ mod tests {
         let buffer = MultiBuffer::build_simple(text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
-        let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
         let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
 
@@ -1174,12 +1183,11 @@ mod tests {
             buffer.snapshot(cx)
         });
 
-        let (fold_snapshot, fold_edits) =
-            fold_map.read(buffer_snapshot, subscription.consume().into_inner());
-        let (suggestion_snapshot, suggestion_edits) =
-            suggestion_map.sync(fold_snapshot, fold_edits);
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
         let (tab_snapshot, tab_edits) =
-            tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
+            tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
             wrap_map.sync(tab_snapshot, tab_edits, cx)
         });
@@ -1204,9 +1212,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
         let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
 
@@ -1276,9 +1284,9 @@ mod tests {
         };
 
         let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
         let (wrap_map, wraps_snapshot) =
             WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
         let mut block_map = BlockMap::new(
@@ -1331,12 +1339,11 @@ mod tests {
                         })
                         .collect::<Vec<_>>();
 
-                    let (fold_snapshot, fold_edits) =
-                        fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (suggestion_snapshot, suggestion_edits) =
-                        suggestion_map.sync(fold_snapshot, fold_edits);
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tab_snapshot, tab_edits) =
-                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tab_snapshot, tab_edits, cx)
                     });
@@ -1356,12 +1363,11 @@ mod tests {
                         })
                         .collect();
 
-                    let (fold_snapshot, fold_edits) =
-                        fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (suggestion_snapshot, suggestion_edits) =
-                        suggestion_map.sync(fold_snapshot, fold_edits);
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tab_snapshot, tab_edits) =
-                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tab_snapshot, tab_edits, cx)
                     });
@@ -1380,11 +1386,10 @@ mod tests {
                 }
             }
 
-            let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            let (suggestion_snapshot, suggestion_edits) =
-                suggestion_map.sync(fold_snapshot, fold_edits);
-            let (tab_snapshot, tab_edits) =
-                tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+            let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                 wrap_map.sync(tab_snapshot, tab_edits, cx)
             });
@@ -1498,6 +1503,7 @@ mod tests {
                         false,
                         None,
                         None,
+                        None,
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

crates/editor/src/display_map/fold_map.rs πŸ”—

@@ -1,19 +1,15 @@
-use super::TextHighlights;
-use crate::{
-    multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
-    ToOffset,
+use super::{
+    inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
+    TextHighlights,
 };
-use collections::BTreeMap;
+use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
 use gpui::{color::Color, fonts::HighlightStyle};
 use language::{Chunk, Edit, Point, TextSummary};
-use parking_lot::Mutex;
 use std::{
     any::TypeId,
     cmp::{self, Ordering},
-    iter::{self, Peekable},
-    ops::{Range, Sub},
-    sync::atomic::{AtomicUsize, Ordering::SeqCst},
-    vec,
+    iter,
+    ops::{Add, AddAssign, Range, Sub},
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
 
@@ -29,28 +25,24 @@ impl FoldPoint {
         self.0.row
     }
 
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
     pub fn row_mut(&mut self) -> &mut u32 {
         &mut self.0.row
     }
 
+    #[cfg(test)]
     pub fn column_mut(&mut self) -> &mut u32 {
         &mut self.0.column
     }
 
-    pub fn to_buffer_point(self, snapshot: &FoldSnapshot) -> Point {
-        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
-        cursor.seek(&self, Bias::Right, &());
-        let overshoot = self.0 - cursor.start().0 .0;
-        cursor.start().1 + overshoot
-    }
-
-    pub fn to_buffer_offset(self, snapshot: &FoldSnapshot) -> usize {
-        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
+    pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
+        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&self, Bias::Right, &());
         let overshoot = self.0 - cursor.start().0 .0;
-        snapshot
-            .buffer_snapshot
-            .point_to_offset(cursor.start().1 + overshoot)
+        InlayPoint(cursor.start().1 .0 + overshoot)
     }
 
     pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
@@ -63,10 +55,10 @@ impl FoldPoint {
         if !overshoot.is_zero() {
             let transform = cursor.item().expect("display point out of range");
             assert!(transform.output_text.is_none());
-            let end_buffer_offset = snapshot
-                .buffer_snapshot
-                .point_to_offset(cursor.start().1.input.lines + overshoot);
-            offset += end_buffer_offset - cursor.start().1.input.len;
+            let end_inlay_offset = snapshot
+                .inlay_snapshot
+                .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
+            offset += end_inlay_offset.0 - cursor.start().1.input.len;
         }
         FoldOffset(offset)
     }
@@ -87,8 +79,9 @@ impl<'a> FoldMapWriter<'a> {
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
         let mut edits = Vec::new();
         let mut folds = Vec::new();
-        let buffer = self.0.buffer.lock().clone();
+        let snapshot = self.0.snapshot.inlay_snapshot.clone();
         for range in ranges.into_iter() {
+            let buffer = &snapshot.buffer;
             let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
 
             // Ignore any empty ranges.
@@ -103,35 +96,32 @@ impl<'a> FoldMapWriter<'a> {
             }
 
             folds.push(fold);
-            edits.push(text::Edit {
-                old: range.clone(),
-                new: range,
+
+            let inlay_range =
+                snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
+            edits.push(InlayEdit {
+                old: inlay_range.clone(),
+                new: inlay_range,
             });
         }
 
-        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer));
+        let buffer = &snapshot.buffer;
+        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
 
-        self.0.folds = {
+        self.0.snapshot.folds = {
             let mut new_tree = SumTree::new();
-            let mut cursor = self.0.folds.cursor::<Fold>();
+            let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
             for fold in folds {
-                new_tree.push_tree(cursor.slice(&fold, Bias::Right, &buffer), &buffer);
-                new_tree.push(fold, &buffer);
+                new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+                new_tree.push(fold, buffer);
             }
-            new_tree.push_tree(cursor.suffix(&buffer), &buffer);
+            new_tree.append(cursor.suffix(buffer), buffer);
             new_tree
         };
 
-        consolidate_buffer_edits(&mut edits);
-        let edits = self.0.sync(buffer.clone(), edits);
-        let snapshot = FoldSnapshot {
-            transforms: self.0.transforms.lock().clone(),
-            folds: self.0.folds.clone(),
-            buffer_snapshot: buffer,
-            version: self.0.version.load(SeqCst),
-            ellipses_color: self.0.ellipses_color,
-        };
-        (snapshot, edits)
+        consolidate_inlay_edits(&mut edits);
+        let edits = self.0.sync(snapshot.clone(), edits);
+        (self.0.snapshot.clone(), edits)
     }
 
     pub fn unfold<T: ToOffset>(
@@ -141,110 +131,93 @@ impl<'a> FoldMapWriter<'a> {
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
         let mut edits = Vec::new();
         let mut fold_ixs_to_delete = Vec::new();
-        let buffer = self.0.buffer.lock().clone();
+        let snapshot = self.0.snapshot.inlay_snapshot.clone();
+        let buffer = &snapshot.buffer;
         for range in ranges.into_iter() {
             // Remove intersecting folds and add their ranges to edits that are passed to sync.
-            let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
+            let mut folds_cursor =
+                intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
             while let Some(fold) = folds_cursor.item() {
-                let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
+                let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
                 if offset_range.end > offset_range.start {
-                    edits.push(text::Edit {
-                        old: offset_range.clone(),
-                        new: offset_range,
+                    let inlay_range = snapshot.to_inlay_offset(offset_range.start)
+                        ..snapshot.to_inlay_offset(offset_range.end);
+                    edits.push(InlayEdit {
+                        old: inlay_range.clone(),
+                        new: inlay_range,
                     });
                 }
                 fold_ixs_to_delete.push(*folds_cursor.start());
-                folds_cursor.next(&buffer);
+                folds_cursor.next(buffer);
             }
         }
 
         fold_ixs_to_delete.sort_unstable();
         fold_ixs_to_delete.dedup();
 
-        self.0.folds = {
-            let mut cursor = self.0.folds.cursor::<usize>();
+        self.0.snapshot.folds = {
+            let mut cursor = self.0.snapshot.folds.cursor::<usize>();
             let mut folds = SumTree::new();
             for fold_ix in fold_ixs_to_delete {
-                folds.push_tree(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer);
-                cursor.next(&buffer);
+                folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
+                cursor.next(buffer);
             }
-            folds.push_tree(cursor.suffix(&buffer), &buffer);
+            folds.append(cursor.suffix(buffer), buffer);
             folds
         };
 
-        consolidate_buffer_edits(&mut edits);
-        let edits = self.0.sync(buffer.clone(), edits);
-        let snapshot = FoldSnapshot {
-            transforms: self.0.transforms.lock().clone(),
-            folds: self.0.folds.clone(),
-            buffer_snapshot: buffer,
-            version: self.0.version.load(SeqCst),
-            ellipses_color: self.0.ellipses_color,
-        };
-        (snapshot, edits)
+        consolidate_inlay_edits(&mut edits);
+        let edits = self.0.sync(snapshot.clone(), edits);
+        (self.0.snapshot.clone(), edits)
     }
 }
 
 pub struct FoldMap {
-    buffer: Mutex<MultiBufferSnapshot>,
-    transforms: Mutex<SumTree<Transform>>,
-    folds: SumTree<Fold>,
-    version: AtomicUsize,
+    snapshot: FoldSnapshot,
     ellipses_color: Option<Color>,
 }
 
 impl FoldMap {
-    pub fn new(buffer: MultiBufferSnapshot) -> (Self, FoldSnapshot) {
+    pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
         let this = Self {
-            buffer: Mutex::new(buffer.clone()),
-            folds: Default::default(),
-            transforms: Mutex::new(SumTree::from_item(
-                Transform {
-                    summary: TransformSummary {
-                        input: buffer.text_summary(),
-                        output: buffer.text_summary(),
+            snapshot: FoldSnapshot {
+                folds: Default::default(),
+                transforms: SumTree::from_item(
+                    Transform {
+                        summary: TransformSummary {
+                            input: inlay_snapshot.text_summary(),
+                            output: inlay_snapshot.text_summary(),
+                        },
+                        output_text: None,
                     },
-                    output_text: None,
-                },
-                &(),
-            )),
-            ellipses_color: None,
-            version: Default::default(),
-        };
-
-        let snapshot = FoldSnapshot {
-            transforms: this.transforms.lock().clone(),
-            folds: this.folds.clone(),
-            buffer_snapshot: this.buffer.lock().clone(),
-            version: this.version.load(SeqCst),
+                    &(),
+                ),
+                inlay_snapshot: inlay_snapshot.clone(),
+                version: 0,
+                ellipses_color: None,
+            },
             ellipses_color: None,
         };
+        let snapshot = this.snapshot.clone();
         (this, snapshot)
     }
 
     pub fn read(
-        &self,
-        buffer: MultiBufferSnapshot,
-        edits: Vec<Edit<usize>>,
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        edits: Vec<InlayEdit>,
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
-        let edits = self.sync(buffer, edits);
+        let edits = self.sync(inlay_snapshot, edits);
         self.check_invariants();
-        let snapshot = FoldSnapshot {
-            transforms: self.transforms.lock().clone(),
-            folds: self.folds.clone(),
-            buffer_snapshot: self.buffer.lock().clone(),
-            version: self.version.load(SeqCst),
-            ellipses_color: self.ellipses_color,
-        };
-        (snapshot, edits)
+        (self.snapshot.clone(), edits)
     }
 
     pub fn write(
         &mut self,
-        buffer: MultiBufferSnapshot,
-        edits: Vec<Edit<usize>>,
+        inlay_snapshot: InlaySnapshot,
+        edits: Vec<InlayEdit>,
     ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
-        let (snapshot, edits) = self.read(buffer, edits);
+        let (snapshot, edits) = self.read(inlay_snapshot, edits);
         (FoldMapWriter(self), snapshot, edits)
     }
 
@@ -260,15 +233,17 @@ impl FoldMap {
     fn check_invariants(&self) {
         if cfg!(test) {
             assert_eq!(
-                self.transforms.lock().summary().input.len,
-                self.buffer.lock().len(),
-                "transform tree does not match buffer's length"
+                self.snapshot.transforms.summary().input.len,
+                self.snapshot.inlay_snapshot.len().0,
+                "transform tree does not match inlay snapshot's length"
             );
 
-            let mut folds = self.folds.iter().peekable();
+            let mut folds = self.snapshot.folds.iter().peekable();
             while let Some(fold) = folds.next() {
                 if let Some(next_fold) = folds.peek() {
-                    let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
+                    let comparison = fold
+                        .0
+                        .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
                     assert!(comparison.is_le());
                 }
             }
@@ -276,50 +251,42 @@ impl FoldMap {
     }
 
     fn sync(
-        &self,
-        new_buffer: MultiBufferSnapshot,
-        buffer_edits: Vec<text::Edit<usize>>,
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        inlay_edits: Vec<InlayEdit>,
     ) -> Vec<FoldEdit> {
-        if buffer_edits.is_empty() {
-            let mut buffer = self.buffer.lock();
-            if buffer.edit_count() != new_buffer.edit_count()
-                || buffer.parse_count() != new_buffer.parse_count()
-                || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
-                || buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
-                || buffer.trailing_excerpt_update_count()
-                    != new_buffer.trailing_excerpt_update_count()
-            {
-                self.version.fetch_add(1, SeqCst);
+        if inlay_edits.is_empty() {
+            if self.snapshot.inlay_snapshot.version != inlay_snapshot.version {
+                self.snapshot.version += 1;
             }
-            *buffer = new_buffer;
+            self.snapshot.inlay_snapshot = inlay_snapshot;
             Vec::new()
         } else {
-            let mut buffer_edits_iter = buffer_edits.iter().cloned().peekable();
+            let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
 
             let mut new_transforms = SumTree::new();
-            let mut transforms = self.transforms.lock();
-            let mut cursor = transforms.cursor::<usize>();
-            cursor.seek(&0, Bias::Right, &());
+            let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
+            cursor.seek(&InlayOffset(0), Bias::Right, &());
 
-            while let Some(mut edit) = buffer_edits_iter.next() {
-                new_transforms.push_tree(cursor.slice(&edit.old.start, Bias::Left, &()), &());
-                edit.new.start -= edit.old.start - cursor.start();
+            while let Some(mut edit) = inlay_edits_iter.next() {
+                new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
+                edit.new.start -= edit.old.start - *cursor.start();
                 edit.old.start = *cursor.start();
 
                 cursor.seek(&edit.old.end, Bias::Right, &());
                 cursor.next(&());
 
-                let mut delta = edit.new.len() as isize - edit.old.len() as isize;
+                let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
                 loop {
                     edit.old.end = *cursor.start();
 
-                    if let Some(next_edit) = buffer_edits_iter.peek() {
+                    if let Some(next_edit) = inlay_edits_iter.peek() {
                         if next_edit.old.start > edit.old.end {
                             break;
                         }
 
-                        let next_edit = buffer_edits_iter.next().unwrap();
-                        delta += next_edit.new.len() as isize - next_edit.old.len() as isize;
+                        let next_edit = inlay_edits_iter.next().unwrap();
+                        delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize;
 
                         if next_edit.old.end >= edit.old.end {
                             edit.old.end = next_edit.old.end;
@@ -331,19 +298,29 @@ impl FoldMap {
                     }
                 }
 
-                edit.new.end = ((edit.new.start + edit.old.len()) as isize + delta) as usize;
-
-                let anchor = new_buffer.anchor_before(edit.new.start);
-                let mut folds_cursor = self.folds.cursor::<Fold>();
-                folds_cursor.seek(&Fold(anchor..Anchor::max()), Bias::Left, &new_buffer);
+                edit.new.end =
+                    InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
+
+                let anchor = inlay_snapshot
+                    .buffer
+                    .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
+                let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+                folds_cursor.seek(
+                    &Fold(anchor..Anchor::max()),
+                    Bias::Left,
+                    &inlay_snapshot.buffer,
+                );
 
                 let mut folds = iter::from_fn({
-                    let buffer = &new_buffer;
+                    let inlay_snapshot = &inlay_snapshot;
                     move || {
-                        let item = folds_cursor
-                            .item()
-                            .map(|f| f.0.start.to_offset(buffer)..f.0.end.to_offset(buffer));
-                        folds_cursor.next(buffer);
+                        let item = folds_cursor.item().map(|f| {
+                            let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
+                            let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+                            inlay_snapshot.to_inlay_offset(buffer_start)
+                                ..inlay_snapshot.to_inlay_offset(buffer_end)
+                        });
+                        folds_cursor.next(&inlay_snapshot.buffer);
                         item
                     }
                 })
@@ -353,7 +330,7 @@ impl FoldMap {
                     let mut fold = folds.next().unwrap();
                     let sum = new_transforms.summary();
 
-                    assert!(fold.start >= sum.input.len);
+                    assert!(fold.start.0 >= sum.input.len);
 
                     while folds
                         .peek()
@@ -365,9 +342,9 @@ impl FoldMap {
                         }
                     }
 
-                    if fold.start > sum.input.len {
-                        let text_summary = new_buffer
-                            .text_summary_for_range::<TextSummary, _>(sum.input.len..fold.start);
+                    if fold.start.0 > sum.input.len {
+                        let text_summary = inlay_snapshot
+                            .text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
                         new_transforms.push(
                             Transform {
                                 summary: TransformSummary {
@@ -386,7 +363,8 @@ impl FoldMap {
                             Transform {
                                 summary: TransformSummary {
                                     output: TextSummary::from(output_text),
-                                    input: new_buffer.text_summary_for_range(fold.start..fold.end),
+                                    input: inlay_snapshot
+                                        .text_summary_for_range(fold.start..fold.end),
                                 },
                                 output_text: Some(output_text),
                             },
@@ -396,9 +374,9 @@ impl FoldMap {
                 }
 
                 let sum = new_transforms.summary();
-                if sum.input.len < edit.new.end {
-                    let text_summary = new_buffer
-                        .text_summary_for_range::<TextSummary, _>(sum.input.len..edit.new.end);
+                if sum.input.len < edit.new.end.0 {
+                    let text_summary = inlay_snapshot
+                        .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
                     new_transforms.push(
                         Transform {
                             summary: TransformSummary {
@@ -412,9 +390,9 @@ impl FoldMap {
                 }
             }
 
-            new_transforms.push_tree(cursor.suffix(&()), &());
+            new_transforms.append(cursor.suffix(&()), &());
             if new_transforms.is_empty() {
-                let text_summary = new_buffer.text_summary();
+                let text_summary = inlay_snapshot.text_summary();
                 new_transforms.push(
                     Transform {
                         summary: TransformSummary {
@@ -429,18 +407,21 @@ impl FoldMap {
 
             drop(cursor);
 
-            let mut fold_edits = Vec::with_capacity(buffer_edits.len());
+            let mut fold_edits = Vec::with_capacity(inlay_edits.len());
             {
-                let mut old_transforms = transforms.cursor::<(usize, FoldOffset)>();
-                let mut new_transforms = new_transforms.cursor::<(usize, FoldOffset)>();
+                let mut old_transforms = self
+                    .snapshot
+                    .transforms
+                    .cursor::<(InlayOffset, FoldOffset)>();
+                let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>();
 
-                for mut edit in buffer_edits {
+                for mut edit in inlay_edits {
                     old_transforms.seek(&edit.old.start, Bias::Left, &());
                     if old_transforms.item().map_or(false, |t| t.is_fold()) {
                         edit.old.start = old_transforms.start().0;
                     }
                     let old_start =
-                        old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0);
+                        old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0;
 
                     old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
                     if old_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -448,14 +429,14 @@ impl FoldMap {
                         edit.old.end = old_transforms.start().0;
                     }
                     let old_end =
-                        old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0);
+                        old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0;
 
                     new_transforms.seek(&edit.new.start, Bias::Left, &());
                     if new_transforms.item().map_or(false, |t| t.is_fold()) {
                         edit.new.start = new_transforms.start().0;
                     }
                     let new_start =
-                        new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0);
+                        new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0;
 
                     new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
                     if new_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -463,7 +444,7 @@ impl FoldMap {
                         edit.new.end = new_transforms.start().0;
                     }
                     let new_end =
-                        new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0);
+                        new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0;
 
                     fold_edits.push(FoldEdit {
                         old: FoldOffset(old_start)..FoldOffset(old_end),
@@ -474,9 +455,9 @@ impl FoldMap {
                 consolidate_fold_edits(&mut fold_edits);
             }
 
-            *transforms = new_transforms;
-            *self.buffer.lock() = new_buffer;
-            self.version.fetch_add(1, SeqCst);
+            self.snapshot.transforms = new_transforms;
+            self.snapshot.inlay_snapshot = inlay_snapshot;
+            self.snapshot.version += 1;
             fold_edits
         }
     }
@@ -486,32 +467,28 @@ impl FoldMap {
 pub struct FoldSnapshot {
     transforms: SumTree<Transform>,
     folds: SumTree<Fold>,
-    buffer_snapshot: MultiBufferSnapshot,
+    pub inlay_snapshot: InlaySnapshot,
     pub version: usize,
     pub ellipses_color: Option<Color>,
 }
 
 impl FoldSnapshot {
-    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        &self.buffer_snapshot
-    }
-
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(FoldOffset(0)..self.len(), false, None)
+        self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
             .map(|c| c.text)
             .collect()
     }
 
     #[cfg(test)]
     pub fn fold_count(&self) -> usize {
-        self.folds.items(&self.buffer_snapshot).len()
+        self.folds.items(&self.inlay_snapshot.buffer).len()
     }
 
     pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
         let mut summary = TextSummary::default();
 
-        let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&range.start, Bias::Right, &());
         if let Some(transform) = cursor.item() {
             let start_in_transform = range.start.0 - cursor.start().0 .0;
@@ -522,11 +499,15 @@ impl FoldSnapshot {
                         [start_in_transform.column as usize..end_in_transform.column as usize],
                 );
             } else {
-                let buffer_start = cursor.start().1 + start_in_transform;
-                let buffer_end = cursor.start().1 + end_in_transform;
+                let inlay_start = self
+                    .inlay_snapshot
+                    .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform));
+                let inlay_end = self
+                    .inlay_snapshot
+                    .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
                 summary = self
-                    .buffer_snapshot
-                    .text_summary_for_range(buffer_start..buffer_end);
+                    .inlay_snapshot
+                    .text_summary_for_range(inlay_start..inlay_end);
             }
         }
 
@@ -540,11 +521,13 @@ impl FoldSnapshot {
                 if let Some(output_text) = transform.output_text {
                     summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
                 } else {
-                    let buffer_start = cursor.start().1;
-                    let buffer_end = cursor.start().1 + end_in_transform;
+                    let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
+                    let inlay_end = self
+                        .inlay_snapshot
+                        .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
                     summary += self
-                        .buffer_snapshot
-                        .text_summary_for_range::<TextSummary, _>(buffer_start..buffer_end);
+                        .inlay_snapshot
+                        .text_summary_for_range(inlay_start..inlay_end);
                 }
             }
         }
@@ -552,8 +535,8 @@ impl FoldSnapshot {
         summary
     }
 
-    pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
-        let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>();
+    pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>();
         cursor.seek(&point, Bias::Right, &());
         if cursor.item().map_or(false, |t| t.is_fold()) {
             if bias == Bias::Left || point == cursor.start().0 {
@@ -562,7 +545,7 @@ impl FoldSnapshot {
                 cursor.end(&()).1
             }
         } else {
-            let overshoot = point - cursor.start().0;
+            let overshoot = point.0 - cursor.start().0 .0;
             FoldPoint(cmp::min(
                 cursor.start().1 .0 + overshoot,
                 cursor.end(&()).1 .0,
@@ -590,12 +573,12 @@ impl FoldSnapshot {
         }
 
         let fold_point = FoldPoint::new(start_row, 0);
-        let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&fold_point, Bias::Left, &());
 
         let overshoot = fold_point.0 - cursor.start().0 .0;
-        let buffer_point = cursor.start().1 + overshoot;
-        let input_buffer_rows = self.buffer_snapshot.buffer_rows(buffer_point.row);
+        let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
+        let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
 
         FoldBufferRows {
             fold_point,
@@ -617,10 +600,10 @@ impl FoldSnapshot {
     where
         T: ToOffset,
     {
-        let mut folds = intersecting_folds(&self.buffer_snapshot, &self.folds, range, false);
+        let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
         iter::from_fn(move || {
             let item = folds.item().map(|f| &f.0);
-            folds.next(&self.buffer_snapshot);
+            folds.next(&self.inlay_snapshot.buffer);
             item
         })
     }
@@ -629,26 +612,39 @@ impl FoldSnapshot {
     where
         T: ToOffset,
     {
-        let offset = offset.to_offset(&self.buffer_snapshot);
-        let mut cursor = self.transforms.cursor::<usize>();
-        cursor.seek(&offset, Bias::Right, &());
+        let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
+        let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
+        let mut cursor = self.transforms.cursor::<InlayOffset>();
+        cursor.seek(&inlay_offset, Bias::Right, &());
         cursor.item().map_or(false, |t| t.output_text.is_some())
     }
 
     pub fn is_line_folded(&self, buffer_row: u32) -> bool {
-        let mut cursor = self.transforms.cursor::<Point>();
-        cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
-        while let Some(transform) = cursor.item() {
-            if transform.output_text.is_some() {
-                return true;
+        let mut inlay_point = self
+            .inlay_snapshot
+            .to_inlay_point(Point::new(buffer_row, 0));
+        let mut cursor = self.transforms.cursor::<InlayPoint>();
+        cursor.seek(&inlay_point, Bias::Right, &());
+        loop {
+            match cursor.item() {
+                Some(transform) => {
+                    let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
+                    if buffer_point.row != buffer_row {
+                        return false;
+                    } else if transform.output_text.is_some() {
+                        return true;
+                    }
+                }
+                None => return false,
             }
-            if cursor.end(&()).row == buffer_row {
-                cursor.next(&())
+
+            if cursor.end(&()).row() == inlay_point.row() {
+                cursor.next(&());
             } else {
-                break;
+                inlay_point.0 += Point::new(1, 0);
+                cursor.seek(&inlay_point, Bias::Right, &());
             }
         }
-        false
     }
 
     pub fn chunks<'a>(
@@ -656,127 +652,56 @@ impl FoldSnapshot {
         range: Range<FoldOffset>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> FoldChunks<'a> {
-        let mut highlight_endpoints = Vec::new();
-        let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
+        let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
 
-        let buffer_end = {
+        let inlay_end = {
             transform_cursor.seek(&range.end, Bias::Right, &());
             let overshoot = range.end.0 - transform_cursor.start().0 .0;
-            transform_cursor.start().1 + overshoot
+            transform_cursor.start().1 + InlayOffset(overshoot)
         };
 
-        let buffer_start = {
+        let inlay_start = {
             transform_cursor.seek(&range.start, Bias::Right, &());
             let overshoot = range.start.0 - transform_cursor.start().0 .0;
-            transform_cursor.start().1 + overshoot
+            transform_cursor.start().1 + InlayOffset(overshoot)
         };
 
-        if let Some(text_highlights) = text_highlights {
-            if !text_highlights.is_empty() {
-                while transform_cursor.start().0 < range.end {
-                    if !transform_cursor.item().unwrap().is_fold() {
-                        let transform_start = self
-                            .buffer_snapshot
-                            .anchor_after(cmp::max(buffer_start, transform_cursor.start().1));
-
-                        let transform_end = {
-                            let overshoot = range.end.0 - transform_cursor.start().0 .0;
-                            self.buffer_snapshot.anchor_before(cmp::min(
-                                transform_cursor.end(&()).1,
-                                transform_cursor.start().1 + overshoot,
-                            ))
-                        };
-
-                        for (tag, highlights) in text_highlights.iter() {
-                            let style = highlights.0;
-                            let ranges = &highlights.1;
-
-                            let start_ix = match ranges.binary_search_by(|probe| {
-                                let cmp = probe.end.cmp(&transform_start, self.buffer_snapshot());
-                                if cmp.is_gt() {
-                                    Ordering::Greater
-                                } else {
-                                    Ordering::Less
-                                }
-                            }) {
-                                Ok(i) | Err(i) => i,
-                            };
-                            for range in &ranges[start_ix..] {
-                                if range
-                                    .start
-                                    .cmp(&transform_end, &self.buffer_snapshot)
-                                    .is_ge()
-                                {
-                                    break;
-                                }
-
-                                highlight_endpoints.push(HighlightEndpoint {
-                                    offset: range.start.to_offset(&self.buffer_snapshot),
-                                    is_start: true,
-                                    tag: *tag,
-                                    style,
-                                });
-                                highlight_endpoints.push(HighlightEndpoint {
-                                    offset: range.end.to_offset(&self.buffer_snapshot),
-                                    is_start: false,
-                                    tag: *tag,
-                                    style,
-                                });
-                            }
-                        }
-                    }
-
-                    transform_cursor.next(&());
-                }
-                highlight_endpoints.sort();
-                transform_cursor.seek(&range.start, Bias::Right, &());
-            }
-        }
-
         FoldChunks {
             transform_cursor,
-            buffer_chunks: self
-                .buffer_snapshot
-                .chunks(buffer_start..buffer_end, language_aware),
-            buffer_chunk: None,
-            buffer_offset: buffer_start,
+            inlay_chunks: self.inlay_snapshot.chunks(
+                inlay_start..inlay_end,
+                language_aware,
+                text_highlights,
+                hint_highlights,
+                suggestion_highlights,
+            ),
+            inlay_chunk: None,
+            inlay_offset: inlay_start,
             output_offset: range.start.0,
             max_output_offset: range.end.0,
-            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
-            active_highlights: Default::default(),
             ellipses_color: self.ellipses_color,
         }
     }
 
+    pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+        self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
+            .flat_map(|chunk| chunk.text.chars())
+    }
+
     #[cfg(test)]
     pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
-        let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>();
-        cursor.seek(&offset, Bias::Right, &());
-        if let Some(transform) = cursor.item() {
-            let transform_start = cursor.start().0 .0;
-            if transform.output_text.is_some() {
-                if offset.0 == transform_start || matches!(bias, Bias::Left) {
-                    FoldOffset(transform_start)
-                } else {
-                    FoldOffset(cursor.end(&()).0 .0)
-                }
-            } else {
-                let overshoot = offset.0 - transform_start;
-                let buffer_offset = cursor.start().1 + overshoot;
-                let clipped_buffer_offset = self.buffer_snapshot.clip_offset(buffer_offset, bias);
-                FoldOffset(
-                    (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize))
-                        as usize,
-                )
-            }
+        if offset > self.len() {
+            self.len()
         } else {
-            FoldOffset(self.transforms.summary().output.len)
+            self.clip_point(offset.to_point(self), bias).to_offset(self)
         }
     }
 
     pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
-        let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&point, Bias::Right, &());
         if let Some(transform) = cursor.item() {
             let transform_start = cursor.start().0 .0;
@@ -787,11 +712,10 @@ impl FoldSnapshot {
                     FoldPoint(cursor.end(&()).0 .0)
                 }
             } else {
-                let overshoot = point.0 - transform_start;
-                let buffer_position = cursor.start().1 + overshoot;
-                let clipped_buffer_position =
-                    self.buffer_snapshot.clip_point(buffer_position, bias);
-                FoldPoint(cursor.start().0 .0 + (clipped_buffer_position - cursor.start().1))
+                let overshoot = InlayPoint(point.0 - transform_start);
+                let inlay_point = cursor.start().1 + overshoot;
+                let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
+                FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0)
             }
         } else {
             FoldPoint(self.transforms.summary().output.lines)
@@ -800,7 +724,7 @@ impl FoldSnapshot {
 }
 
 fn intersecting_folds<'a, T>(
-    buffer: &'a MultiBufferSnapshot,
+    inlay_snapshot: &'a InlaySnapshot,
     folds: &'a SumTree<Fold>,
     range: Range<T>,
     inclusive: bool,
@@ -808,6 +732,7 @@ fn intersecting_folds<'a, T>(
 where
     T: ToOffset,
 {
+    let buffer = &inlay_snapshot.buffer;
     let start = buffer.anchor_before(range.start.to_offset(buffer));
     let end = buffer.anchor_after(range.end.to_offset(buffer));
     let mut cursor = folds.filter::<_, usize>(move |summary| {
@@ -824,7 +749,7 @@ where
     cursor
 }
 
-fn consolidate_buffer_edits(edits: &mut Vec<text::Edit<usize>>) {
+fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
     edits.sort_unstable_by(|a, b| {
         a.old
             .start
@@ -952,7 +877,7 @@ impl Default for FoldSummary {
 impl sum_tree::Summary for FoldSummary {
     type Context = MultiBufferSnapshot;
 
-    fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
+    fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
         if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
             self.min_start = other.min_start.clone();
         }
@@ -996,8 +921,8 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
 
 #[derive(Clone)]
 pub struct FoldBufferRows<'a> {
-    cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
-    input_buffer_rows: MultiBufferRows<'a>,
+    cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
+    input_buffer_rows: InlayBufferRows<'a>,
     fold_point: FoldPoint,
 }
 
@@ -1016,7 +941,7 @@ impl<'a> Iterator for FoldBufferRows<'a> {
 
         if self.cursor.item().is_some() {
             if traversed_fold {
-                self.input_buffer_rows.seek(self.cursor.start().1.row);
+                self.input_buffer_rows.seek(self.cursor.start().1.row());
                 self.input_buffer_rows.next();
             }
             *self.fold_point.row_mut() += 1;
@@ -1028,14 +953,12 @@ impl<'a> Iterator for FoldBufferRows<'a> {
 }
 
 pub struct FoldChunks<'a> {
-    transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
-    buffer_chunks: MultiBufferChunks<'a>,
-    buffer_chunk: Option<(usize, Chunk<'a>)>,
-    buffer_offset: usize,
+    transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
+    inlay_chunks: InlayChunks<'a>,
+    inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
+    inlay_offset: InlayOffset,
     output_offset: usize,
     max_output_offset: usize,
-    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
-    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
     ellipses_color: Option<Color>,
 }
 
@@ -1052,11 +975,11 @@ impl<'a> Iterator for FoldChunks<'a> {
         // If we're in a fold, then return the fold's display text and
         // advance the transform and buffer cursors to the end of the fold.
         if let Some(output_text) = transform.output_text {
-            self.buffer_chunk.take();
-            self.buffer_offset += transform.summary.input.len;
-            self.buffer_chunks.seek(self.buffer_offset);
+            self.inlay_chunk.take();
+            self.inlay_offset += InlayOffset(transform.summary.input.len);
+            self.inlay_chunks.seek(self.inlay_offset);
 
-            while self.buffer_offset >= self.transform_cursor.end(&()).1
+            while self.inlay_offset >= self.transform_cursor.end(&()).1
                 && self.transform_cursor.item().is_some()
             {
                 self.transform_cursor.next(&());
@@ -1073,53 +996,28 @@ impl<'a> Iterator for FoldChunks<'a> {
             });
         }
 
-        let mut next_highlight_endpoint = usize::MAX;
-        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
-            if endpoint.offset <= self.buffer_offset {
-                if endpoint.is_start {
-                    self.active_highlights.insert(endpoint.tag, endpoint.style);
-                } else {
-                    self.active_highlights.remove(&endpoint.tag);
-                }
-                self.highlight_endpoints.next();
-            } else {
-                next_highlight_endpoint = endpoint.offset;
-                break;
-            }
-        }
-
         // Retrieve a chunk from the current location in the buffer.
-        if self.buffer_chunk.is_none() {
-            let chunk_offset = self.buffer_chunks.offset();
-            self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk));
+        if self.inlay_chunk.is_none() {
+            let chunk_offset = self.inlay_chunks.offset();
+            self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk));
         }
 
         // Otherwise, take a chunk from the buffer's text.
-        if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk {
-            let buffer_chunk_end = buffer_chunk_start + chunk.text.len();
+        if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+            let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
             let transform_end = self.transform_cursor.end(&()).1;
-            let chunk_end = buffer_chunk_end
-                .min(transform_end)
-                .min(next_highlight_endpoint);
+            let chunk_end = buffer_chunk_end.min(transform_end);
 
             chunk.text = &chunk.text
-                [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start];
-
-            if !self.active_highlights.is_empty() {
-                let mut highlight_style = HighlightStyle::default();
-                for active_highlight in self.active_highlights.values() {
-                    highlight_style.highlight(*active_highlight);
-                }
-                chunk.highlight_style = Some(highlight_style);
-            }
+                [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
 
             if chunk_end == transform_end {
                 self.transform_cursor.next(&());
             } else if chunk_end == buffer_chunk_end {
-                self.buffer_chunk.take();
+                self.inlay_chunk.take();
             }
 
-            self.buffer_offset = chunk_end;
+            self.inlay_offset = chunk_end;
             self.output_offset += chunk.text.len();
             return Some(chunk);
         }

crates/editor/src/display_map/inlay_map.rs πŸ”—

@@ -0,0 +1,1787 @@
+use crate::{
+    multi_buffer::{MultiBufferChunks, MultiBufferRows},
+    Anchor, InlayId, MultiBufferSnapshot, ToOffset,
+};
+use collections::{BTreeMap, BTreeSet};
+use gpui::fonts::HighlightStyle;
+use language::{Chunk, Edit, Point, TextSummary};
+use std::{
+    any::TypeId,
+    cmp,
+    iter::Peekable,
+    ops::{Add, AddAssign, Range, Sub, SubAssign},
+    vec,
+};
+use sum_tree::{Bias, Cursor, SumTree};
+use text::{Patch, Rope};
+
+use super::TextHighlights;
+
+pub struct InlayMap {
+    snapshot: InlaySnapshot,
+    inlays: Vec<Inlay>,
+}
+
+#[derive(Clone)]
+pub struct InlaySnapshot {
+    pub buffer: MultiBufferSnapshot,
+    transforms: SumTree<Transform>,
+    pub version: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Transform {
+    Isomorphic(TextSummary),
+    Inlay(Inlay),
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+    pub id: InlayId,
+    pub position: Anchor,
+    pub text: text::Rope,
+}
+
+impl Inlay {
+    pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
+        let mut text = hint.text();
+        if hint.padding_right && !text.ends_with(' ') {
+            text.push(' ');
+        }
+        if hint.padding_left && !text.starts_with(' ') {
+            text.insert(0, ' ');
+        }
+        Self {
+            id: InlayId::Hint(id),
+            position,
+            text: text.into(),
+        }
+    }
+
+    pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::Suggestion(id),
+            position,
+            text: text.into(),
+        }
+    }
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        match self {
+            Transform::Isomorphic(summary) => TransformSummary {
+                input: summary.clone(),
+                output: summary.clone(),
+            },
+            Transform::Inlay(inlay) => TransformSummary {
+                input: TextSummary::default(),
+                output: inlay.text.summary(),
+            },
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+    input: TextSummary,
+    output: TextSummary,
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, other: &Self, _: &()) {
+        self.input += &other.input;
+        self.output += &other.output;
+    }
+}
+
+pub type InlayEdit = Edit<InlayOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayOffset(pub usize);
+
+impl Add for InlayOffset {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for InlayOffset {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl AddAssign for InlayOffset {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += rhs.0;
+    }
+}
+
+impl SubAssign for InlayOffset {
+    fn sub_assign(&mut self, rhs: Self) {
+        self.0 -= rhs.0;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.len;
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayPoint(pub Point);
+
+impl Add for InlayPoint {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for InlayPoint {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.lines;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += &summary.input.len;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += &summary.input.lines;
+    }
+}
+
+#[derive(Clone)]
+pub struct InlayBufferRows<'a> {
+    transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
+    buffer_rows: MultiBufferRows<'a>,
+    inlay_row: u32,
+    max_buffer_row: u32,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+    offset: InlayOffset,
+    is_start: bool,
+    tag: Option<TypeId>,
+    style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HighlightEndpoint {
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        self.offset
+            .cmp(&other.offset)
+            .then_with(|| other.is_start.cmp(&self.is_start))
+    }
+}
+
+pub struct InlayChunks<'a> {
+    transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
+    buffer_chunks: MultiBufferChunks<'a>,
+    buffer_chunk: Option<Chunk<'a>>,
+    inlay_chunks: Option<text::Chunks<'a>>,
+    output_offset: InlayOffset,
+    max_output_offset: InlayOffset,
+    hint_highlight_style: Option<HighlightStyle>,
+    suggestion_highlight_style: Option<HighlightStyle>,
+    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+    snapshot: &'a InlaySnapshot,
+}
+
+impl<'a> InlayChunks<'a> {
+    pub fn seek(&mut self, offset: InlayOffset) {
+        self.transforms.seek(&offset, Bias::Right, &());
+
+        let buffer_offset = self.snapshot.to_buffer_offset(offset);
+        self.buffer_chunks.seek(buffer_offset);
+        self.inlay_chunks = None;
+        self.buffer_chunk = None;
+        self.output_offset = offset;
+    }
+
+    pub fn offset(&self) -> InlayOffset {
+        self.output_offset
+    }
+}
+
+impl<'a> Iterator for InlayChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_offset == self.max_output_offset {
+            return None;
+        }
+
+        let mut next_highlight_endpoint = InlayOffset(usize::MAX);
+        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+            if endpoint.offset <= self.output_offset {
+                if endpoint.is_start {
+                    self.active_highlights.insert(endpoint.tag, endpoint.style);
+                } else {
+                    self.active_highlights.remove(&endpoint.tag);
+                }
+                self.highlight_endpoints.next();
+            } else {
+                next_highlight_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
+        let chunk = match self.transforms.item()? {
+            Transform::Isomorphic(_) => {
+                let chunk = self
+                    .buffer_chunk
+                    .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+                if chunk.text.is_empty() {
+                    *chunk = self.buffer_chunks.next().unwrap();
+                }
+
+                let (prefix, suffix) = chunk.text.split_at(
+                    chunk
+                        .text
+                        .len()
+                        .min(self.transforms.end(&()).0 .0 - self.output_offset.0)
+                        .min(next_highlight_endpoint.0 - self.output_offset.0),
+                );
+
+                chunk.text = suffix;
+                self.output_offset.0 += prefix.len();
+                let mut prefix = Chunk {
+                    text: prefix,
+                    ..chunk.clone()
+                };
+                if !self.active_highlights.is_empty() {
+                    let mut highlight_style = HighlightStyle::default();
+                    for active_highlight in self.active_highlights.values() {
+                        highlight_style.highlight(*active_highlight);
+                    }
+                    prefix.highlight_style = Some(highlight_style);
+                }
+                prefix
+            }
+            Transform::Inlay(inlay) => {
+                let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
+                    let start = self.output_offset - self.transforms.start().0;
+                    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)
+                });
+
+                let chunk = inlay_chunks.next().unwrap();
+                self.output_offset.0 += chunk.len();
+                let highlight_style = match inlay.id {
+                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
+                    InlayId::Hint(_) => self.hint_highlight_style,
+                };
+                Chunk {
+                    text: chunk,
+                    highlight_style,
+                    ..Default::default()
+                }
+            }
+        };
+
+        if self.output_offset == self.transforms.end(&()).0 {
+            self.inlay_chunks = None;
+            self.transforms.next(&());
+        }
+
+        Some(chunk)
+    }
+}
+
+impl<'a> InlayBufferRows<'a> {
+    pub fn seek(&mut self, row: u32) {
+        let inlay_point = InlayPoint::new(row, 0);
+        self.transforms.seek(&inlay_point, Bias::Left, &());
+
+        let mut buffer_point = self.transforms.start().1;
+        let buffer_row = if row == 0 {
+            0
+        } else {
+            match self.transforms.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    buffer_point += inlay_point.0 - self.transforms.start().0 .0;
+                    buffer_point.row
+                }
+                _ => cmp::min(buffer_point.row + 1, self.max_buffer_row),
+            }
+        };
+        self.inlay_row = inlay_point.row();
+        self.buffer_rows.seek(buffer_row);
+    }
+}
+
+impl<'a> Iterator for InlayBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let buffer_row = if self.inlay_row == 0 {
+            self.buffer_rows.next().unwrap()
+        } else {
+            match self.transforms.item()? {
+                Transform::Inlay(_) => None,
+                Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
+            }
+        };
+
+        self.inlay_row += 1;
+        self.transforms
+            .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
+
+        Some(buffer_row)
+    }
+}
+
+impl InlayPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+}
+
+impl InlayMap {
+    pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
+        let version = 0;
+        let snapshot = InlaySnapshot {
+            buffer: buffer.clone(),
+            transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
+            version,
+        };
+
+        (
+            Self {
+                snapshot: snapshot.clone(),
+                inlays: Vec::new(),
+            },
+            snapshot,
+        )
+    }
+
+    pub fn sync(
+        &mut self,
+        buffer_snapshot: MultiBufferSnapshot,
+        mut buffer_edits: Vec<text::Edit<usize>>,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        let mut snapshot = &mut self.snapshot;
+
+        if buffer_edits.is_empty() {
+            if snapshot.buffer.trailing_excerpt_update_count()
+                != buffer_snapshot.trailing_excerpt_update_count()
+            {
+                buffer_edits.push(Edit {
+                    old: snapshot.buffer.len()..snapshot.buffer.len(),
+                    new: buffer_snapshot.len()..buffer_snapshot.len(),
+                });
+            }
+        }
+
+        if buffer_edits.is_empty() {
+            if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
+                || snapshot.buffer.parse_count() != buffer_snapshot.parse_count()
+                || snapshot.buffer.diagnostics_update_count()
+                    != buffer_snapshot.diagnostics_update_count()
+                || snapshot.buffer.git_diff_update_count()
+                    != buffer_snapshot.git_diff_update_count()
+                || snapshot.buffer.trailing_excerpt_update_count()
+                    != buffer_snapshot.trailing_excerpt_update_count()
+            {
+                snapshot.version += 1;
+            }
+
+            snapshot.buffer = buffer_snapshot;
+            (snapshot.clone(), Vec::new())
+        } else {
+            let mut inlay_edits = Patch::default();
+            let mut new_transforms = SumTree::new();
+            let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>();
+            let mut buffer_edits_iter = buffer_edits.iter().peekable();
+            while let Some(buffer_edit) = buffer_edits_iter.next() {
+                new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
+                if let Some(Transform::Isomorphic(transform)) = cursor.item() {
+                    if cursor.end(&()).0 == buffer_edit.old.start {
+                        push_isomorphic(&mut new_transforms, transform.clone());
+                        cursor.next(&());
+                    }
+                }
+
+                // Remove all the inlays and transforms contained by the edit.
+                let old_start =
+                    cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
+                cursor.seek(&buffer_edit.old.end, Bias::Right, &());
+                let old_end =
+                    cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
+
+                // Push the unchanged prefix.
+                let prefix_start = new_transforms.summary().input.len;
+                let prefix_end = buffer_edit.new.start;
+                push_isomorphic(
+                    &mut new_transforms,
+                    buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+                );
+                let new_start = InlayOffset(new_transforms.summary().output.len);
+
+                let start_ix = match self.inlays.binary_search_by(|probe| {
+                    probe
+                        .position
+                        .to_offset(&buffer_snapshot)
+                        .cmp(&buffer_edit.new.start)
+                        .then(std::cmp::Ordering::Greater)
+                }) {
+                    Ok(ix) | Err(ix) => ix,
+                };
+
+                for inlay in &self.inlays[start_ix..] {
+                    let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
+                    if buffer_offset > buffer_edit.new.end {
+                        break;
+                    }
+
+                    let prefix_start = new_transforms.summary().input.len;
+                    let prefix_end = buffer_offset;
+                    push_isomorphic(
+                        &mut new_transforms,
+                        buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+                    );
+
+                    if inlay.position.is_valid(&buffer_snapshot) {
+                        new_transforms.push(Transform::Inlay(inlay.clone()), &());
+                    }
+                }
+
+                // Apply the rest of the edit.
+                let transform_start = new_transforms.summary().input.len;
+                push_isomorphic(
+                    &mut new_transforms,
+                    buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
+                );
+                let new_end = InlayOffset(new_transforms.summary().output.len);
+                inlay_edits.push(Edit {
+                    old: old_start..old_end,
+                    new: new_start..new_end,
+                });
+
+                // If the next edit doesn't intersect the current isomorphic transform, then
+                // we can push its remainder.
+                if buffer_edits_iter
+                    .peek()
+                    .map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
+                {
+                    let transform_start = new_transforms.summary().input.len;
+                    let transform_end =
+                        buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
+                    push_isomorphic(
+                        &mut new_transforms,
+                        buffer_snapshot.text_summary_for_range(transform_start..transform_end),
+                    );
+                    cursor.next(&());
+                }
+            }
+
+            new_transforms.append(cursor.suffix(&()), &());
+            if new_transforms.is_empty() {
+                new_transforms.push(Transform::Isomorphic(Default::default()), &());
+            }
+
+            drop(cursor);
+            snapshot.transforms = new_transforms;
+            snapshot.version += 1;
+            snapshot.buffer = buffer_snapshot;
+            snapshot.check_invariants();
+
+            (snapshot.clone(), inlay_edits.into_inner())
+        }
+    }
+
+    pub fn splice(
+        &mut self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<Inlay>,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        let snapshot = &mut self.snapshot;
+        let mut edits = BTreeSet::new();
+
+        self.inlays.retain(|inlay| {
+            let retain = !to_remove.contains(&inlay.id);
+            if !retain {
+                let offset = inlay.position.to_offset(&snapshot.buffer);
+                edits.insert(offset);
+            }
+            retain
+        });
+
+        for inlay_to_insert in to_insert {
+            // Avoid inserting empty inlays.
+            if inlay_to_insert.text.is_empty() {
+                continue;
+            }
+
+            let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
+            match self.inlays.binary_search_by(|probe| {
+                probe
+                    .position
+                    .cmp(&inlay_to_insert.position, &snapshot.buffer)
+            }) {
+                Ok(ix) | Err(ix) => {
+                    self.inlays.insert(ix, inlay_to_insert);
+                }
+            }
+
+            edits.insert(offset);
+        }
+
+        let buffer_edits = edits
+            .into_iter()
+            .map(|offset| Edit {
+                old: offset..offset,
+                new: offset..offset,
+            })
+            .collect();
+        let buffer_snapshot = snapshot.buffer.clone();
+        drop(snapshot);
+        let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
+        (snapshot, edits)
+    }
+
+    pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+        self.inlays.iter()
+    }
+
+    #[cfg(test)]
+    pub(crate) fn randomly_mutate(
+        &mut self,
+        next_inlay_id: &mut usize,
+        rng: &mut rand::rngs::StdRng,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        use rand::prelude::*;
+        use util::post_inc;
+
+        let mut to_remove = Vec::new();
+        let mut to_insert = Vec::new();
+        let snapshot = &mut self.snapshot;
+        for i in 0..rng.gen_range(1..=5) {
+            if self.inlays.is_empty() || rng.gen() {
+                let position = snapshot.buffer.random_byte_range(0, rng).start;
+                let bias = if rng.gen() { Bias::Left } else { Bias::Right };
+                let len = if rng.gen_bool(0.01) {
+                    0
+                } else {
+                    rng.gen_range(1..=5)
+                };
+                let text = util::RandomCharIter::new(&mut *rng)
+                    .filter(|ch| *ch != '\r')
+                    .take(len)
+                    .collect::<String>();
+                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))
+                };
+                to_insert.push(Inlay {
+                    id: inlay_id,
+                    position: snapshot.buffer.anchor_at(position, bias),
+                    text: text.into(),
+                });
+            } else {
+                to_remove.push(
+                    self.inlays
+                        .iter()
+                        .choose(rng)
+                        .map(|inlay| inlay.id)
+                        .unwrap(),
+                );
+            }
+        }
+        log::info!("removing inlays: {:?}", to_remove);
+
+        drop(snapshot);
+        let (snapshot, edits) = self.splice(to_remove, to_insert);
+        (snapshot, edits)
+    }
+}
+
+impl InlaySnapshot {
+    pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
+        let mut cursor = self
+            .transforms
+            .cursor::<(InlayOffset, (InlayPoint, usize))>();
+        cursor.seek(&offset, Bias::Right, &());
+        let overshoot = offset.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_offset_start = cursor.start().1 .1;
+                let buffer_offset_end = buffer_offset_start + overshoot;
+                let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
+                let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
+                InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let overshoot = inlay.text.offset_to_point(overshoot);
+                InlayPoint(cursor.start().1 .0 .0 + overshoot)
+            }
+            None => self.max_point(),
+        }
+    }
+
+    pub fn len(&self) -> InlayOffset {
+        InlayOffset(self.transforms.summary().output.len)
+    }
+
+    pub fn max_point(&self) -> InlayPoint {
+        InlayPoint(self.transforms.summary().output.lines)
+    }
+
+    pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
+        let mut cursor = self
+            .transforms
+            .cursor::<(InlayPoint, (InlayOffset, Point))>();
+        cursor.seek(&point, Bias::Right, &());
+        let overshoot = point.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_point_start = cursor.start().1 .1;
+                let buffer_point_end = buffer_point_start + overshoot;
+                let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
+                let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
+                InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let overshoot = inlay.text.point_to_offset(overshoot);
+                InlayOffset(cursor.start().1 .0 .0 + overshoot)
+            }
+            None => self.len(),
+        }
+    }
+
+    pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        cursor.seek(&point, Bias::Right, &());
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let overshoot = point.0 - cursor.start().0 .0;
+                cursor.start().1 + overshoot
+            }
+            Some(Transform::Inlay(_)) => cursor.start().1,
+            None => self.buffer.max_point(),
+        }
+    }
+
+    pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&offset, Bias::Right, &());
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let overshoot = offset - cursor.start().0;
+                cursor.start().1 + overshoot.0
+            }
+            Some(Transform::Inlay(_)) => cursor.start().1,
+            None => self.buffer.len(),
+        }
+    }
+
+    pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
+        let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>();
+        cursor.seek(&offset, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    if offset == cursor.end(&()).0 {
+                        while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                break;
+                            } else {
+                                cursor.next(&());
+                            }
+                        }
+                        return cursor.end(&()).1;
+                    } else {
+                        let overshoot = offset - cursor.start().0;
+                        return InlayOffset(cursor.start().1 .0 + overshoot);
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if inlay.position.bias() == Bias::Left {
+                        cursor.next(&());
+                    } else {
+                        return cursor.start().1;
+                    }
+                }
+                None => {
+                    return self.len();
+                }
+            }
+        }
+    }
+
+    pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
+        let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>();
+        cursor.seek(&point, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    if point == cursor.end(&()).0 {
+                        while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                break;
+                            } else {
+                                cursor.next(&());
+                            }
+                        }
+                        return cursor.end(&()).1;
+                    } else {
+                        let overshoot = point - cursor.start().0;
+                        return InlayPoint(cursor.start().1 .0 + overshoot);
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if inlay.position.bias() == Bias::Left {
+                        cursor.next(&());
+                    } else {
+                        return cursor.start().1;
+                    }
+                }
+                None => {
+                    return self.max_point();
+                }
+            }
+        }
+    }
+
+    pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        cursor.seek(&point, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(transform)) => {
+                    if cursor.start().0 == point {
+                        if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
+                            if inlay.position.bias() == Bias::Left {
+                                return point;
+                            } else if bias == Bias::Left {
+                                cursor.prev(&());
+                            } else if transform.first_line_chars == 0 {
+                                point.0 += Point::new(1, 0);
+                            } else {
+                                point.0 += Point::new(0, 1);
+                            }
+                        } else {
+                            return point;
+                        }
+                    } else if cursor.end(&()).0 == point {
+                        if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                return point;
+                            } else if bias == Bias::Right {
+                                cursor.next(&());
+                            } else if point.0.column == 0 {
+                                point.0.row -= 1;
+                                point.0.column = self.line_len(point.0.row);
+                            } else {
+                                point.0.column -= 1;
+                            }
+                        } else {
+                            return point;
+                        }
+                    } else {
+                        let overshoot = point.0 - cursor.start().0 .0;
+                        let buffer_point = cursor.start().1 + overshoot;
+                        let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
+                        let clipped_overshoot = clipped_buffer_point - cursor.start().1;
+                        let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
+                        if clipped_point == point {
+                            return clipped_point;
+                        } else {
+                            point = clipped_point;
+                        }
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
+                        match cursor.prev_item() {
+                            Some(Transform::Inlay(inlay)) => {
+                                if inlay.position.bias() == Bias::Left {
+                                    return point;
+                                }
+                            }
+                            _ => return point,
+                        }
+                    } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
+                        match cursor.next_item() {
+                            Some(Transform::Inlay(inlay)) => {
+                                if inlay.position.bias() == Bias::Right {
+                                    return point;
+                                }
+                            }
+                            _ => return point,
+                        }
+                    }
+
+                    if bias == Bias::Left {
+                        point = cursor.start().0;
+                        cursor.prev(&());
+                    } else {
+                        cursor.next(&());
+                        point = cursor.start().0;
+                    }
+                }
+                None => {
+                    bias = bias.invert();
+                    if bias == Bias::Left {
+                        point = cursor.start().0;
+                        cursor.prev(&());
+                    } else {
+                        cursor.next(&());
+                        point = cursor.start().0;
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn text_summary(&self) -> TextSummary {
+        self.transforms.summary().output.clone()
+    }
+
+    pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
+        let mut summary = TextSummary::default();
+
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&range.start, Bias::Right, &());
+
+        let overshoot = range.start.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_start = cursor.start().1;
+                let suffix_start = buffer_start + overshoot;
+                let suffix_end =
+                    buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
+                summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
+                cursor.next(&());
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let suffix_start = overshoot;
+                let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0;
+                summary = inlay.text.cursor(suffix_start).summary(suffix_end);
+                cursor.next(&());
+            }
+            None => {}
+        }
+
+        if range.end > cursor.start().0 {
+            summary += cursor
+                .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+                .output;
+
+            let overshoot = range.end.0 - cursor.start().0 .0;
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    let prefix_start = cursor.start().1;
+                    let prefix_end = prefix_start + overshoot;
+                    summary += self
+                        .buffer
+                        .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    let prefix_end = overshoot;
+                    summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
+                }
+                None => {}
+            }
+        }
+
+        summary
+    }
+
+    pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        let inlay_point = InlayPoint::new(row, 0);
+        cursor.seek(&inlay_point, Bias::Left, &());
+
+        let max_buffer_row = self.buffer.max_point().row;
+        let mut buffer_point = cursor.start().1;
+        let buffer_row = if row == 0 {
+            0
+        } else {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    buffer_point += inlay_point.0 - cursor.start().0 .0;
+                    buffer_point.row
+                }
+                _ => cmp::min(buffer_point.row + 1, max_buffer_row),
+            }
+        };
+
+        InlayBufferRows {
+            transforms: cursor,
+            inlay_row: inlay_point.row(),
+            buffer_rows: self.buffer.buffer_rows(buffer_row),
+            max_buffer_row,
+        }
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
+        let line_end = if row >= self.max_point().row() {
+            self.len().0
+        } else {
+            self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
+        };
+        (line_end - line_start) as u32
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<InlayOffset>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
+    ) -> 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 !text_highlights.is_empty() {
+                while cursor.start().0 < range.end {
+                    if true {
+                        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, highlights) in text_highlights.iter() {
+                            let style = highlights.0;
+                            let ranges = &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(&());
+                }
+                highlight_endpoints.sort();
+                cursor.seek(&range.start, Bias::Right, &());
+            }
+        }
+
+        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);
+
+        InlayChunks {
+            transforms: cursor,
+            buffer_chunks,
+            inlay_chunks: None,
+            buffer_chunk: None,
+            output_offset: range.start,
+            max_output_offset: range.end,
+            hint_highlight_style: hint_highlights,
+            suggestion_highlight_style: suggestion_highlights,
+            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+            active_highlights: Default::default(),
+            snapshot: self,
+        }
+    }
+
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(Default::default()..self.len(), false, None, None, None)
+            .map(|chunk| chunk.text)
+            .collect()
+    }
+
+    fn check_invariants(&self) {
+        #[cfg(any(debug_assertions, feature = "test-support"))]
+        {
+            assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
+            let mut transforms = self.transforms.iter().peekable();
+            while let Some(transform) = transforms.next() {
+                let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
+                if let Some(next_transform) = transforms.peek() {
+                    let next_transform_is_isomorphic =
+                        matches!(next_transform, Transform::Isomorphic(_));
+                    assert!(
+                        !transform_is_isomorphic || !next_transform_is_isomorphic,
+                        "two adjacent isomorphic transforms"
+                    );
+                }
+            }
+        }
+    }
+}
+
+fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
+    if summary.len == 0 {
+        return;
+    }
+
+    let mut summary = Some(summary);
+    sum_tree.update_last(
+        |transform| {
+            if let Transform::Isomorphic(transform) = transform {
+                *transform += summary.take().unwrap();
+            }
+        },
+        &(),
+    );
+
+    if let Some(summary) = summary {
+        sum_tree.push(Transform::Isomorphic(summary), &());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{InlayId, MultiBuffer};
+    use gpui::AppContext;
+    use project::{InlayHint, InlayHintLabel};
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use std::{cmp::Reverse, env, sync::Arc};
+    use sum_tree::TreeMap;
+    use text::Patch;
+    use util::post_inc;
+
+    #[test]
+    fn test_inlay_properties_label_padding() {
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("a".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: false,
+                    padding_right: false,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            "a",
+            "Should not pad label if not requested"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String("a".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should pad label for every side requested"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String(" a ".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: false,
+                    padding_right: false,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should not change already padded label"
+        );
+
+        assert_eq!(
+            Inlay::hint(
+                0,
+                Anchor::min(),
+                &InlayHint {
+                    label: InlayHintLabel::String(" a ".to_string()),
+                    buffer_id: 0,
+                    position: text::Anchor::default(),
+                    padding_left: true,
+                    padding_right: true,
+                    tooltip: None,
+                    kind: None,
+                },
+            )
+            .text
+            .to_string(),
+            " a ",
+            "Should not change already padded label"
+        );
+    }
+
+    #[gpui::test]
+    fn test_basic_inlays(cx: &mut AppContext) {
+        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        assert_eq!(inlay_snapshot.text(), "abcdefghi");
+        let mut next_inlay_id = 0;
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![Inlay {
+                id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                text: "|123|".into(),
+            }],
+        );
+        assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 0)),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 1)),
+            InlayPoint::new(0, 1)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 2)),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 3)),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 4)),
+            InlayPoint::new(0, 9)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 5)),
+            InlayPoint::new(0, 10)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+            InlayPoint::new(0, 9)
+        );
+
+        // Edits before or after the inlay should not affect it.
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
+        });
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
+
+        // An edit surrounding the inlay should invalidate it.
+        buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(3),
+                    text: "|123|".into(),
+                },
+                Inlay {
+                    id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                    text: "|456|".into(),
+                },
+            ],
+        );
+        assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
+
+        // Edits ending where the inlay starts should not move it if it has a left bias.
+        buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+            InlayPoint::new(0, 0)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
+            InlayPoint::new(0, 1)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
+            InlayPoint::new(0, 1)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
+            InlayPoint::new(0, 2)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
+            InlayPoint::new(0, 8)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
+            InlayPoint::new(0, 9)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
+            InlayPoint::new(0, 9)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
+            InlayPoint::new(0, 10)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
+            InlayPoint::new(0, 10)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
+            InlayPoint::new(0, 11)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
+            InlayPoint::new(0, 17)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
+            InlayPoint::new(0, 18)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
+            InlayPoint::new(0, 18)
+        );
+
+        // The inlays can be manually removed.
+        let (inlay_snapshot, _) = inlay_map.splice(
+            inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
+            Vec::new(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
+    }
+
+    #[gpui::test]
+    fn test_inlay_buffer_rows(cx: &mut AppContext) {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
+        let mut next_inlay_id = 0;
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(0),
+                    text: "|123|\n".into(),
+                },
+                Inlay {
+                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(4),
+                    text: "|456|".into(),
+                },
+                Inlay {
+                    id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    position: buffer.read(cx).snapshot(cx).anchor_before(7),
+                    text: "\n|567|\n".into(),
+                },
+            ],
+        );
+        assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
+        assert_eq!(
+            inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            vec![Some(0), None, Some(1), None, None, Some(2)]
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) {
+        init_test(cx);
+
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let len = rng.gen_range(0..30);
+        let buffer = if rng.gen() {
+            let text = util::RandomCharIter::new(&mut rng)
+                .take(len)
+                .collect::<String>();
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let mut next_inlay_id = 0;
+        log::info!("buffer text: {:?}", buffer_snapshot.text());
+
+        let mut highlights = TreeMap::default();
+        let highlight_count = rng.gen_range(0_usize..10);
+        let mut highlight_ranges = (0..highlight_count)
+            .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+            .collect::<Vec<_>>();
+        highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+        log::info!("highlighting ranges {:?}", highlight_ranges);
+        let highlight_ranges = highlight_ranges
+            .into_iter()
+            .map(|range| {
+                buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
+            })
+            .collect::<Vec<_>>();
+
+        highlights.insert(
+            Some(TypeId::of::<()>()),
+            Arc::new((HighlightStyle::default(), highlight_ranges)),
+        );
+
+        let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        for _ in 0..operations {
+            let mut inlay_edits = Patch::default();
+
+            let mut prev_inlay_text = inlay_snapshot.text();
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=50 => {
+                    let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    log::info!("mutated text: {:?}", snapshot.text());
+                    inlay_edits = Patch::new(edits);
+                }
+                _ => buffer.update(cx, |buffer, cx| {
+                    let subscription = buffer.subscribe();
+                    let edit_count = rng.gen_range(1..=5);
+                    buffer.randomly_mutate(&mut rng, edit_count, cx);
+                    buffer_snapshot = buffer.snapshot(cx);
+                    let edits = subscription.consume().into_inner();
+                    log::info!("editing {:?}", edits);
+                    buffer_edits.extend(edits);
+                }),
+            };
+
+            let (new_inlay_snapshot, new_inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            inlay_snapshot = new_inlay_snapshot;
+            inlay_edits = inlay_edits.compose(new_inlay_edits);
+
+            log::info!("buffer text: {:?}", buffer_snapshot.text());
+            log::info!("inlay text: {:?}", inlay_snapshot.text());
+
+            let inlays = inlay_map
+                .inlays
+                .iter()
+                .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
+                .map(|inlay| {
+                    let offset = inlay.position.to_offset(&buffer_snapshot);
+                    (offset, inlay.clone())
+                })
+                .collect::<Vec<_>>();
+            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());
+            }
+            assert_eq!(inlay_snapshot.text(), expected_text.to_string());
+
+            let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            assert_eq!(
+                expected_buffer_rows.len() as u32,
+                expected_text.max_point().row + 1
+            );
+            for row_start in 0..expected_buffer_rows.len() {
+                assert_eq!(
+                    inlay_snapshot
+                        .buffer_rows(row_start as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[row_start..],
+                    "incorrect buffer rows starting at {}",
+                    row_start
+                );
+            }
+
+            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 actual_text = inlay_snapshot
+                    .chunks(
+                        InlayOffset(start)..InlayOffset(end),
+                        false,
+                        Some(&highlights),
+                        None,
+                        None,
+                    )
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text,
+                    expected_text.slice(start..end).to_string(),
+                    "incorrect text in range {:?}",
+                    start..end
+                );
+
+                assert_eq!(
+                    inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
+                    expected_text.slice(start..end).summary()
+                );
+            }
+
+            for edit in inlay_edits {
+                prev_inlay_text.replace_range(
+                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+                    &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
+                );
+            }
+            assert_eq!(prev_inlay_text, inlay_snapshot.text());
+
+            assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
+            assert_eq!(expected_text.len(), inlay_snapshot.len().0);
+
+            let mut buffer_point = Point::default();
+            let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+            let mut buffer_chars = buffer_snapshot.chars_at(0);
+            loop {
+                // Ensure conversion from buffer coordinates to inlay coordinates
+                // is consistent.
+                let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
+                assert_eq!(
+                    inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
+                    inlay_point
+                );
+
+                // No matter which bias we clip an inlay point with, it doesn't move
+                // because it was constructed from a buffer point.
+                assert_eq!(
+                    inlay_snapshot.clip_point(inlay_point, Bias::Left),
+                    inlay_point,
+                    "invalid inlay point for buffer point {:?} when clipped left",
+                    buffer_point
+                );
+                assert_eq!(
+                    inlay_snapshot.clip_point(inlay_point, Bias::Right),
+                    inlay_point,
+                    "invalid inlay point for buffer point {:?} when clipped right",
+                    buffer_point
+                );
+
+                if let Some(ch) = buffer_chars.next() {
+                    if ch == '\n' {
+                        buffer_point += Point::new(1, 0);
+                    } else {
+                        buffer_point += Point::new(0, ch.len_utf8() as u32);
+                    }
+
+                    // Ensure that moving forward in the buffer always moves the inlay point forward as well.
+                    let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+                    assert!(new_inlay_point > inlay_point);
+                    inlay_point = new_inlay_point;
+                } else {
+                    break;
+                }
+            }
+
+            let mut inlay_point = InlayPoint::default();
+            let mut inlay_offset = InlayOffset::default();
+            for ch in expected_text.chars() {
+                assert_eq!(
+                    inlay_snapshot.to_offset(inlay_point),
+                    inlay_offset,
+                    "invalid to_offset({:?})",
+                    inlay_point
+                );
+                assert_eq!(
+                    inlay_snapshot.to_point(inlay_offset),
+                    inlay_point,
+                    "invalid to_point({:?})",
+                    inlay_offset
+                );
+
+                let mut bytes = [0; 4];
+                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+                    inlay_offset.0 += 1;
+                    if *byte == b'\n' {
+                        inlay_point.0 += Point::new(1, 0);
+                    } else {
+                        inlay_point.0 += Point::new(0, 1);
+                    }
+
+                    let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
+                    let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
+                    assert!(
+                        clipped_left_point <= clipped_right_point,
+                        "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
+                        inlay_point,
+                        clipped_left_point,
+                        clipped_right_point
+                    );
+
+                    // Ensure the clipped points are at valid text locations.
+                    assert_eq!(
+                        clipped_left_point.0,
+                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
+                    );
+                    assert_eq!(
+                        clipped_right_point.0,
+                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
+                    );
+
+                    // Ensure the clipped points never overshoot the end of the map.
+                    assert!(clipped_left_point <= inlay_snapshot.max_point());
+                    assert!(clipped_right_point <= inlay_snapshot.max_point());
+
+                    // Ensure the clipped points are at valid buffer locations.
+                    assert_eq!(
+                        inlay_snapshot
+                            .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
+                        clipped_left_point,
+                        "to_buffer_point({:?}) = {:?}",
+                        clipped_left_point,
+                        inlay_snapshot.to_buffer_point(clipped_left_point),
+                    );
+                    assert_eq!(
+                        inlay_snapshot
+                            .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
+                        clipped_right_point,
+                        "to_buffer_point({:?}) = {:?}",
+                        clipped_right_point,
+                        inlay_snapshot.to_buffer_point(clipped_right_point),
+                    );
+                }
+            }
+        }
+    }
+
+    fn init_test(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+    }
+}

crates/editor/src/display_map/suggestion_map.rs πŸ”—

@@ -1,871 +0,0 @@
-use super::{
-    fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
-    TextHighlights,
-};
-use crate::{MultiBufferSnapshot, ToPoint};
-use gpui::fonts::HighlightStyle;
-use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
-use parking_lot::Mutex;
-use std::{
-    cmp,
-    ops::{Add, AddAssign, Range, Sub},
-};
-use util::post_inc;
-
-pub type SuggestionEdit = Edit<SuggestionOffset>;
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionOffset(pub usize);
-
-impl Add for SuggestionOffset {
-    type Output = Self;
-
-    fn add(self, rhs: Self) -> Self::Output {
-        Self(self.0 + rhs.0)
-    }
-}
-
-impl Sub for SuggestionOffset {
-    type Output = Self;
-
-    fn sub(self, rhs: Self) -> Self::Output {
-        Self(self.0 - rhs.0)
-    }
-}
-
-impl AddAssign for SuggestionOffset {
-    fn add_assign(&mut self, rhs: Self) {
-        self.0 += rhs.0;
-    }
-}
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionPoint(pub Point);
-
-impl SuggestionPoint {
-    pub fn new(row: u32, column: u32) -> Self {
-        Self(Point::new(row, column))
-    }
-
-    pub fn row(self) -> u32 {
-        self.0.row
-    }
-
-    pub fn column(self) -> u32 {
-        self.0.column
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct Suggestion<T> {
-    pub position: T,
-    pub text: Rope,
-}
-
-pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
-
-impl SuggestionMap {
-    pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
-        let snapshot = SuggestionSnapshot {
-            fold_snapshot,
-            suggestion: None,
-            version: 0,
-        };
-        (Self(Mutex::new(snapshot.clone())), snapshot)
-    }
-
-    pub fn replace<T>(
-        &self,
-        new_suggestion: Option<Suggestion<T>>,
-        fold_snapshot: FoldSnapshot,
-        fold_edits: Vec<FoldEdit>,
-    ) -> (
-        SuggestionSnapshot,
-        Vec<SuggestionEdit>,
-        Option<Suggestion<FoldOffset>>,
-    )
-    where
-        T: ToPoint,
-    {
-        let new_suggestion = new_suggestion.map(|new_suggestion| {
-            let buffer_point = new_suggestion
-                .position
-                .to_point(fold_snapshot.buffer_snapshot());
-            let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
-            let fold_offset = fold_point.to_offset(&fold_snapshot);
-            Suggestion {
-                position: fold_offset,
-                text: new_suggestion.text,
-            }
-        });
-
-        let (_, edits) = self.sync(fold_snapshot, fold_edits);
-        let mut snapshot = self.0.lock();
-
-        let mut patch = Patch::new(edits);
-        let old_suggestion = snapshot.suggestion.take();
-        if let Some(suggestion) = &old_suggestion {
-            patch = patch.compose([SuggestionEdit {
-                old: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
-                new: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0),
-            }]);
-        }
-
-        if let Some(suggestion) = new_suggestion.as_ref() {
-            patch = patch.compose([SuggestionEdit {
-                old: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0),
-                new: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
-            }]);
-        }
-
-        snapshot.suggestion = new_suggestion;
-        snapshot.version += 1;
-        (snapshot.clone(), patch.into_inner(), old_suggestion)
-    }
-
-    pub fn sync(
-        &self,
-        fold_snapshot: FoldSnapshot,
-        fold_edits: Vec<FoldEdit>,
-    ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
-        let mut snapshot = self.0.lock();
-
-        if snapshot.fold_snapshot.version != fold_snapshot.version {
-            snapshot.version += 1;
-        }
-
-        let mut suggestion_edits = Vec::new();
-
-        let mut suggestion_old_len = 0;
-        let mut suggestion_new_len = 0;
-        for fold_edit in fold_edits {
-            let start = fold_edit.new.start;
-            let end = FoldOffset(start.0 + fold_edit.old_len().0);
-            if let Some(suggestion) = snapshot.suggestion.as_mut() {
-                if end <= suggestion.position {
-                    suggestion.position.0 += fold_edit.new_len().0;
-                    suggestion.position.0 -= fold_edit.old_len().0;
-                } else if start > suggestion.position {
-                    suggestion_old_len = suggestion.text.len();
-                    suggestion_new_len = suggestion_old_len;
-                } else {
-                    suggestion_old_len = suggestion.text.len();
-                    snapshot.suggestion.take();
-                    suggestion_edits.push(SuggestionEdit {
-                        old: SuggestionOffset(fold_edit.old.start.0)
-                            ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
-                        new: SuggestionOffset(fold_edit.new.start.0)
-                            ..SuggestionOffset(fold_edit.new.end.0),
-                    });
-                    continue;
-                }
-            }
-
-            suggestion_edits.push(SuggestionEdit {
-                old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
-                    ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
-                new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
-                    ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
-            });
-        }
-        snapshot.fold_snapshot = fold_snapshot;
-
-        (snapshot.clone(), suggestion_edits)
-    }
-
-    pub fn has_suggestion(&self) -> bool {
-        let snapshot = self.0.lock();
-        snapshot.suggestion.is_some()
-    }
-}
-
-#[derive(Clone)]
-pub struct SuggestionSnapshot {
-    pub fold_snapshot: FoldSnapshot,
-    pub suggestion: Option<Suggestion<FoldOffset>>,
-    pub version: usize,
-}
-
-impl SuggestionSnapshot {
-    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        self.fold_snapshot.buffer_snapshot()
-    }
-
-    pub fn max_point(&self) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
-            let mut max_point = suggestion_point.0;
-            max_point += suggestion.text.max_point();
-            max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
-            SuggestionPoint(max_point)
-        } else {
-            SuggestionPoint(self.fold_snapshot.max_point().0)
-        }
-    }
-
-    pub fn len(&self) -> SuggestionOffset {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let mut len = suggestion.position.0;
-            len += suggestion.text.len();
-            len += self.fold_snapshot.len().0 - suggestion.position.0;
-            SuggestionOffset(len)
-        } else {
-            SuggestionOffset(self.fold_snapshot.len().0)
-        }
-    }
-
-    pub fn line_len(&self, row: u32) -> u32 {
-        if let Some(suggestion) = &self.suggestion {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-
-            if row < suggestion_start.row {
-                self.fold_snapshot.line_len(row)
-            } else if row > suggestion_end.row {
-                self.fold_snapshot
-                    .line_len(suggestion_start.row + (row - suggestion_end.row))
-            } else {
-                let mut result = suggestion.text.line_len(row - suggestion_start.row);
-                if row == suggestion_start.row {
-                    result += suggestion_start.column;
-                }
-                if row == suggestion_end.row {
-                    result +=
-                        self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
-                }
-                result
-            }
-        } else {
-            self.fold_snapshot.line_len(row)
-        }
-    }
-
-    pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-            if point.0 <= suggestion_start {
-                SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
-            } else if point.0 > suggestion_end {
-                let fold_point = self.fold_snapshot.clip_point(
-                    FoldPoint(suggestion_start + (point.0 - suggestion_end)),
-                    bias,
-                );
-                let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
-                if bias == Bias::Left && suggestion_point == suggestion_end {
-                    SuggestionPoint(suggestion_start)
-                } else {
-                    SuggestionPoint(suggestion_point)
-                }
-            } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
-                SuggestionPoint(suggestion_start)
-            } else {
-                let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
-                    > suggestion_start.column
-                {
-                    FoldPoint(suggestion_start + Point::new(0, 1))
-                } else {
-                    FoldPoint(suggestion_start + Point::new(1, 0))
-                };
-                let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
-                SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
-            }
-        } else {
-            SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
-        }
-    }
-
-    pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-
-            if point.0 <= suggestion_start {
-                SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
-            } else if point.0 > suggestion_end {
-                let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
-                    .to_offset(&self.fold_snapshot);
-                SuggestionOffset(fold_offset.0 + suggestion.text.len())
-            } else {
-                let offset_in_suggestion =
-                    suggestion.text.point_to_offset(point.0 - suggestion_start);
-                SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
-            }
-        } else {
-            SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
-        }
-    }
-
-    pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            if offset.0 <= suggestion.position.0 {
-                SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
-            } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
-                let fold_point = FoldOffset(offset.0 - suggestion.text.len())
-                    .to_point(&self.fold_snapshot)
-                    .0;
-
-                SuggestionPoint(
-                    suggestion_point_start
-                        + suggestion.text.max_point()
-                        + (fold_point - suggestion_point_start),
-                )
-            } else {
-                let point_in_suggestion = suggestion
-                    .text
-                    .offset_to_point(offset.0 - suggestion.position.0);
-                SuggestionPoint(suggestion_point_start + point_in_suggestion)
-            }
-        } else {
-            SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
-        }
-    }
-
-    pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-
-            if point.0 <= suggestion_start {
-                FoldPoint(point.0)
-            } else if point.0 > suggestion_end {
-                FoldPoint(suggestion_start + (point.0 - suggestion_end))
-            } else {
-                FoldPoint(suggestion_start)
-            }
-        } else {
-            FoldPoint(point.0)
-        }
-    }
-
-    pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-
-            if point.0 <= suggestion_start {
-                SuggestionPoint(point.0)
-            } else {
-                let suggestion_end = suggestion_start + suggestion.text.max_point();
-                SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
-            }
-        } else {
-            SuggestionPoint(point.0)
-        }
-    }
-
-    pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-            let mut summary = TextSummary::default();
-
-            let prefix_range =
-                cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
-            if prefix_range.start < prefix_range.end {
-                summary += self.fold_snapshot.text_summary_for_range(
-                    FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
-                );
-            }
-
-            let suggestion_range =
-                cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
-            if suggestion_range.start < suggestion_range.end {
-                let point_range = suggestion_range.start - suggestion_start
-                    ..suggestion_range.end - suggestion_start;
-                let offset_range = suggestion.text.point_to_offset(point_range.start)
-                    ..suggestion.text.point_to_offset(point_range.end);
-                summary += suggestion
-                    .text
-                    .cursor(offset_range.start)
-                    .summary::<TextSummary>(offset_range.end);
-            }
-
-            let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
-            if suffix_range.start < suffix_range.end {
-                let start = suggestion_start + (suffix_range.start - suggestion_end);
-                let end = suggestion_start + (suffix_range.end - suggestion_end);
-                summary += self
-                    .fold_snapshot
-                    .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
-            }
-
-            summary
-        } else {
-            self.fold_snapshot
-                .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
-        }
-    }
-
-    pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
-        let start = self.to_offset(start);
-        self.chunks(start..self.len(), false, None, None)
-            .flat_map(|chunk| chunk.text.chars())
-    }
-
-    pub fn chunks<'a>(
-        &'a self,
-        range: Range<SuggestionOffset>,
-        language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
-    ) -> SuggestionChunks<'a> {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_range =
-                suggestion.position.0..suggestion.position.0 + suggestion.text.len();
-
-            let prefix_chunks = if range.start.0 < suggestion_range.start {
-                Some(self.fold_snapshot.chunks(
-                    FoldOffset(range.start.0)
-                        ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
-                    language_aware,
-                    text_highlights,
-                ))
-            } else {
-                None
-            };
-
-            let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
-                ..cmp::min(range.end.0, suggestion_range.end);
-            let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
-            {
-                let start = clipped_suggestion_range.start - suggestion_range.start;
-                let end = clipped_suggestion_range.end - suggestion_range.start;
-                Some(suggestion.text.chunks_in_range(start..end))
-            } else {
-                None
-            };
-
-            let suffix_chunks = if range.end.0 > suggestion_range.end {
-                let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
-                let end = range.end.0 - suggestion_range.len();
-                Some(self.fold_snapshot.chunks(
-                    FoldOffset(start)..FoldOffset(end),
-                    language_aware,
-                    text_highlights,
-                ))
-            } else {
-                None
-            };
-
-            SuggestionChunks {
-                prefix_chunks,
-                suggestion_chunks,
-                suffix_chunks,
-                highlight_style: suggestion_highlight,
-            }
-        } else {
-            SuggestionChunks {
-                prefix_chunks: Some(self.fold_snapshot.chunks(
-                    FoldOffset(range.start.0)..FoldOffset(range.end.0),
-                    language_aware,
-                    text_highlights,
-                )),
-                suggestion_chunks: None,
-                suffix_chunks: None,
-                highlight_style: None,
-            }
-        }
-    }
-
-    pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
-        let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
-            let start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let end = start + suggestion.text.max_point();
-            start.row..end.row
-        } else {
-            u32::MAX..u32::MAX
-        };
-
-        let fold_buffer_rows = if row <= suggestion_range.start {
-            self.fold_snapshot.buffer_rows(row)
-        } else if row > suggestion_range.end {
-            self.fold_snapshot
-                .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
-        } else {
-            let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
-            rows.next();
-            rows
-        };
-
-        SuggestionBufferRows {
-            current_row: row,
-            suggestion_row_start: suggestion_range.start,
-            suggestion_row_end: suggestion_range.end,
-            fold_buffer_rows,
-        }
-    }
-
-    #[cfg(test)]
-    pub fn text(&self) -> String {
-        self.chunks(Default::default()..self.len(), false, None, None)
-            .map(|chunk| chunk.text)
-            .collect()
-    }
-}
-
-pub struct SuggestionChunks<'a> {
-    prefix_chunks: Option<FoldChunks<'a>>,
-    suggestion_chunks: Option<text::Chunks<'a>>,
-    suffix_chunks: Option<FoldChunks<'a>>,
-    highlight_style: Option<HighlightStyle>,
-}
-
-impl<'a> Iterator for SuggestionChunks<'a> {
-    type Item = Chunk<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if let Some(chunks) = self.prefix_chunks.as_mut() {
-            if let Some(chunk) = chunks.next() {
-                return Some(chunk);
-            } else {
-                self.prefix_chunks = None;
-            }
-        }
-
-        if let Some(chunks) = self.suggestion_chunks.as_mut() {
-            if let Some(chunk) = chunks.next() {
-                return Some(Chunk {
-                    text: chunk,
-                    highlight_style: self.highlight_style,
-                    ..Default::default()
-                });
-            } else {
-                self.suggestion_chunks = None;
-            }
-        }
-
-        if let Some(chunks) = self.suffix_chunks.as_mut() {
-            if let Some(chunk) = chunks.next() {
-                return Some(chunk);
-            } else {
-                self.suffix_chunks = None;
-            }
-        }
-
-        None
-    }
-}
-
-#[derive(Clone)]
-pub struct SuggestionBufferRows<'a> {
-    current_row: u32,
-    suggestion_row_start: u32,
-    suggestion_row_end: u32,
-    fold_buffer_rows: FoldBufferRows<'a>,
-}
-
-impl<'a> Iterator for SuggestionBufferRows<'a> {
-    type Item = Option<u32>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let row = post_inc(&mut self.current_row);
-        if row <= self.suggestion_row_start || row > self.suggestion_row_end {
-            self.fold_buffer_rows.next()
-        } else {
-            Some(None)
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{display_map::fold_map::FoldMap, MultiBuffer};
-    use gpui::AppContext;
-    use rand::{prelude::StdRng, Rng};
-    use settings::SettingsStore;
-    use std::{
-        env,
-        ops::{Bound, RangeBounds},
-    };
-
-    #[gpui::test]
-    fn test_basic(cx: &mut AppContext) {
-        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
-        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
-        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-        assert_eq!(suggestion_snapshot.text(), "abcdefghi");
-
-        let (suggestion_snapshot, _, _) = suggestion_map.replace(
-            Some(Suggestion {
-                position: 3,
-                text: "123\n456".into(),
-            }),
-            fold_snapshot,
-            Default::default(),
-        );
-        assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
-
-        buffer.update(cx, |buffer, cx| {
-            buffer.edit(
-                [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
-                None,
-                cx,
-            )
-        });
-        let (fold_snapshot, fold_edits) = fold_map.read(
-            buffer.read(cx).snapshot(cx),
-            buffer_edits.consume().into_inner(),
-        );
-        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
-        assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
-
-        let (mut fold_map_writer, _, _) =
-            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
-        let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
-        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
-        assert_eq!(suggestion_snapshot.text(), "β‹―abcDEF123\n456dGHIefghiJKL");
-
-        let (mut fold_map_writer, _, _) =
-            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
-        let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
-        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
-        assert_eq!(suggestion_snapshot.text(), "β‹―abcβ‹―GHIefghiJKL");
-    }
-
-    #[gpui::test(iterations = 100)]
-    fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
-        init_test(cx);
-
-        let operations = env::var("OPERATIONS")
-            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-            .unwrap_or(10);
-
-        let len = rng.gen_range(0..30);
-        let buffer = if rng.gen() {
-            let text = util::RandomCharIter::new(&mut rng)
-                .take(len)
-                .collect::<String>();
-            MultiBuffer::build_simple(&text, cx)
-        } else {
-            MultiBuffer::build_random(&mut rng, cx)
-        };
-        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-        log::info!("buffer text: {:?}", buffer_snapshot.text());
-
-        let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-
-        for _ in 0..operations {
-            let mut suggestion_edits = Patch::default();
-
-            let mut prev_suggestion_text = suggestion_snapshot.text();
-            let mut buffer_edits = Vec::new();
-            match rng.gen_range(0..=100) {
-                0..=29 => {
-                    let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
-                    suggestion_edits = suggestion_edits.compose(edits);
-                }
-                30..=59 => {
-                    for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-                        fold_snapshot = new_fold_snapshot;
-                        let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
-                        suggestion_edits = suggestion_edits.compose(edits);
-                    }
-                }
-                _ => buffer.update(cx, |buffer, cx| {
-                    let subscription = buffer.subscribe();
-                    let edit_count = rng.gen_range(1..=5);
-                    buffer.randomly_mutate(&mut rng, edit_count, cx);
-                    buffer_snapshot = buffer.snapshot(cx);
-                    let edits = subscription.consume().into_inner();
-                    log::info!("editing {:?}", edits);
-                    buffer_edits.extend(edits);
-                }),
-            };
-
-            let (new_fold_snapshot, fold_edits) =
-                fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            fold_snapshot = new_fold_snapshot;
-            let (new_suggestion_snapshot, edits) =
-                suggestion_map.sync(fold_snapshot.clone(), fold_edits);
-            suggestion_snapshot = new_suggestion_snapshot;
-            suggestion_edits = suggestion_edits.compose(edits);
-
-            log::info!("buffer text: {:?}", buffer_snapshot.text());
-            log::info!("folds text: {:?}", fold_snapshot.text());
-            log::info!("suggestions text: {:?}", suggestion_snapshot.text());
-
-            let mut expected_text = Rope::from(fold_snapshot.text().as_str());
-            let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
-            if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
-                expected_text.replace(
-                    suggestion.position.0..suggestion.position.0,
-                    &suggestion.text.to_string(),
-                );
-                let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
-                let suggestion_end = suggestion_start + suggestion.text.max_point();
-                expected_buffer_rows.splice(
-                    (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
-                    (0..suggestion_end.row - suggestion_start.row).map(|_| None),
-                );
-            }
-            assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
-            for row_start in 0..expected_buffer_rows.len() {
-                assert_eq!(
-                    suggestion_snapshot
-                        .buffer_rows(row_start as u32)
-                        .collect::<Vec<_>>(),
-                    &expected_buffer_rows[row_start..],
-                    "incorrect buffer rows starting at {}",
-                    row_start
-                );
-            }
-
-            for _ in 0..5 {
-                let mut end = rng.gen_range(0..=suggestion_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 actual_text = suggestion_snapshot
-                    .chunks(
-                        SuggestionOffset(start)..SuggestionOffset(end),
-                        false,
-                        None,
-                        None,
-                    )
-                    .map(|chunk| chunk.text)
-                    .collect::<String>();
-                assert_eq!(
-                    actual_text,
-                    expected_text.slice(start..end).to_string(),
-                    "incorrect text in range {:?}",
-                    start..end
-                );
-
-                let start_point = SuggestionPoint(expected_text.offset_to_point(start));
-                let end_point = SuggestionPoint(expected_text.offset_to_point(end));
-                assert_eq!(
-                    suggestion_snapshot.text_summary_for_range(start_point..end_point),
-                    expected_text.slice(start..end).summary()
-                );
-            }
-
-            for edit in suggestion_edits.into_inner() {
-                prev_suggestion_text.replace_range(
-                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
-                    &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
-                );
-            }
-            assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
-
-            assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
-            assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
-
-            let mut suggestion_point = SuggestionPoint::default();
-            let mut suggestion_offset = SuggestionOffset::default();
-            for ch in expected_text.chars() {
-                assert_eq!(
-                    suggestion_snapshot.to_offset(suggestion_point),
-                    suggestion_offset,
-                    "invalid to_offset({:?})",
-                    suggestion_point
-                );
-                assert_eq!(
-                    suggestion_snapshot.to_point(suggestion_offset),
-                    suggestion_point,
-                    "invalid to_point({:?})",
-                    suggestion_offset
-                );
-                assert_eq!(
-                    suggestion_snapshot
-                        .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
-                    suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
-                );
-
-                let mut bytes = [0; 4];
-                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
-                    suggestion_offset.0 += 1;
-                    if *byte == b'\n' {
-                        suggestion_point.0 += Point::new(1, 0);
-                    } else {
-                        suggestion_point.0 += Point::new(0, 1);
-                    }
-
-                    let clipped_left_point =
-                        suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
-                    let clipped_right_point =
-                        suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
-                    assert!(
-                        clipped_left_point <= clipped_right_point,
-                        "clipped left point {:?} is greater than clipped right point {:?}",
-                        clipped_left_point,
-                        clipped_right_point
-                    );
-                    assert_eq!(
-                        clipped_left_point.0,
-                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
-                    );
-                    assert_eq!(
-                        clipped_right_point.0,
-                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
-                    );
-                    assert!(clipped_left_point <= suggestion_snapshot.max_point());
-                    assert!(clipped_right_point <= suggestion_snapshot.max_point());
-
-                    if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
-                        let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
-                        let suggestion_end = suggestion_start + suggestion.text.max_point();
-                        let invalid_range = (
-                            Bound::Excluded(suggestion_start),
-                            Bound::Included(suggestion_end),
-                        );
-                        assert!(
-                            !invalid_range.contains(&clipped_left_point.0),
-                            "clipped left point {:?} is inside invalid suggestion range {:?}",
-                            clipped_left_point,
-                            invalid_range
-                        );
-                        assert!(
-                            !invalid_range.contains(&clipped_right_point.0),
-                            "clipped right point {:?} is inside invalid suggestion range {:?}",
-                            clipped_right_point,
-                            invalid_range
-                        );
-                    }
-                }
-            }
-        }
-    }
-
-    fn init_test(cx: &mut AppContext) {
-        cx.set_global(SettingsStore::test(cx));
-        theme::init((), cx);
-    }
-
-    impl SuggestionMap {
-        pub fn randomly_mutate(
-            &self,
-            rng: &mut impl Rng,
-        ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
-            let fold_snapshot = self.0.lock().fold_snapshot.clone();
-            let new_suggestion = if rng.gen_bool(0.3) {
-                None
-            } else {
-                let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
-                let len = rng.gen_range(0..30);
-                Some(Suggestion {
-                    position: index,
-                    text: util::RandomCharIter::new(rng)
-                        .take(len)
-                        .filter(|ch| *ch != '\r')
-                        .collect::<String>()
-                        .as_str()
-                        .into(),
-                })
-            };
-
-            log::info!("replacing suggestion with {:?}", new_suggestion);
-            let (snapshot, edits, _) =
-                self.replace(new_suggestion, fold_snapshot, Default::default());
-            (snapshot, edits)
-        }
-    }
-}

crates/editor/src/display_map/tab_map.rs πŸ”—

@@ -1,80 +1,76 @@
 use super::{
-    suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
+    fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
     TextHighlights,
 };
 use crate::MultiBufferSnapshot;
 use gpui::fonts::HighlightStyle;
 use language::{Chunk, Point};
-use parking_lot::Mutex;
 use std::{cmp, mem, num::NonZeroU32, ops::Range};
 use sum_tree::Bias;
 
 const MAX_EXPANSION_COLUMN: u32 = 256;
 
-pub struct TabMap(Mutex<TabSnapshot>);
+pub struct TabMap(TabSnapshot);
 
 impl TabMap {
-    pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
         let snapshot = TabSnapshot {
-            suggestion_snapshot: input,
+            fold_snapshot,
             tab_size,
             max_expansion_column: MAX_EXPANSION_COLUMN,
             version: 0,
         };
-        (Self(Mutex::new(snapshot.clone())), snapshot)
+        (Self(snapshot.clone()), snapshot)
     }
 
     #[cfg(test)]
-    pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
-        self.0.lock().max_expansion_column = column;
-        self.0.lock().clone()
+    pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+        self.0.max_expansion_column = column;
+        self.0.clone()
     }
 
     pub fn sync(
-        &self,
-        suggestion_snapshot: SuggestionSnapshot,
-        mut suggestion_edits: Vec<SuggestionEdit>,
+        &mut self,
+        fold_snapshot: FoldSnapshot,
+        mut fold_edits: Vec<FoldEdit>,
         tab_size: NonZeroU32,
     ) -> (TabSnapshot, Vec<TabEdit>) {
-        let mut old_snapshot = self.0.lock();
+        let old_snapshot = &mut self.0;
         let mut new_snapshot = TabSnapshot {
-            suggestion_snapshot,
+            fold_snapshot,
             tab_size,
             max_expansion_column: old_snapshot.max_expansion_column,
             version: old_snapshot.version,
         };
 
-        if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
+        if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
             new_snapshot.version += 1;
         }
 
-        let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
+        let mut tab_edits = Vec::with_capacity(fold_edits.len());
 
         if old_snapshot.tab_size == new_snapshot.tab_size {
             // Expand each edit to include the next tab on the same line as the edit,
             // and any subsequent tabs on that line that moved across the tab expansion
             // boundary.
-            for suggestion_edit in &mut suggestion_edits {
-                let old_end = old_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.old.end);
-                let old_end_row_successor_offset =
-                    old_snapshot.suggestion_snapshot.to_offset(cmp::min(
-                        SuggestionPoint::new(old_end.row() + 1, 0),
-                        old_snapshot.suggestion_snapshot.max_point(),
-                    ));
-                let new_end = new_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.new.end);
+            for fold_edit in &mut fold_edits {
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let old_end_row_successor_offset = cmp::min(
+                    FoldPoint::new(old_end.row() + 1, 0),
+                    old_snapshot.fold_snapshot.max_point(),
+                )
+                .to_offset(&old_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
 
                 let mut offset_from_edit = 0;
                 let mut first_tab_offset = None;
                 let mut last_tab_with_changed_expansion_offset = None;
-                'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
-                    suggestion_edit.old.end..old_end_row_successor_offset,
+                'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+                    fold_edit.old.end..old_end_row_successor_offset,
                     false,
                     None,
                     None,
+                    None,
                 ) {
                     for (ix, _) in chunk.text.match_indices('\t') {
                         let offset_from_edit = offset_from_edit + (ix as u32);
@@ -102,39 +98,31 @@ impl TabMap {
                 }
 
                 if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
-                    suggestion_edit.old.end.0 += offset as usize + 1;
-                    suggestion_edit.new.end.0 += offset as usize + 1;
+                    fold_edit.old.end.0 += offset as usize + 1;
+                    fold_edit.new.end.0 += offset as usize + 1;
                 }
             }
 
             // Combine any edits that overlap due to the expansion.
             let mut ix = 1;
-            while ix < suggestion_edits.len() {
-                let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
+            while ix < fold_edits.len() {
+                let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
                 let prev_edit = prev_edits.last_mut().unwrap();
                 let edit = &next_edits[0];
                 if prev_edit.old.end >= edit.old.start {
                     prev_edit.old.end = edit.old.end;
                     prev_edit.new.end = edit.new.end;
-                    suggestion_edits.remove(ix);
+                    fold_edits.remove(ix);
                 } else {
                     ix += 1;
                 }
             }
 
-            for suggestion_edit in suggestion_edits {
-                let old_start = old_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.old.start);
-                let old_end = old_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.old.end);
-                let new_start = new_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.new.start);
-                let new_end = new_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.new.end);
+            for fold_edit in fold_edits {
+                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
                 tab_edits.push(TabEdit {
                     old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
                     new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
@@ -155,7 +143,7 @@ impl TabMap {
 
 #[derive(Clone)]
 pub struct TabSnapshot {
-    pub suggestion_snapshot: SuggestionSnapshot,
+    pub fold_snapshot: FoldSnapshot,
     pub tab_size: NonZeroU32,
     pub max_expansion_column: u32,
     pub version: usize,
@@ -163,18 +151,15 @@ pub struct TabSnapshot {
 
 impl TabSnapshot {
     pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        self.suggestion_snapshot.buffer_snapshot()
+        &self.fold_snapshot.inlay_snapshot.buffer
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
         let max_point = self.max_point();
         if row < max_point.row() {
-            self.to_tab_point(SuggestionPoint::new(
-                row,
-                self.suggestion_snapshot.line_len(row),
-            ))
-            .0
-            .column
+            self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+                .0
+                .column
         } else {
             max_point.column()
         }
@@ -185,10 +170,10 @@ impl TabSnapshot {
     }
 
     pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
-        let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
-        let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
+        let input_start = self.to_fold_point(range.start, Bias::Left).0;
+        let input_end = self.to_fold_point(range.end, Bias::Right).0;
         let input_summary = self
-            .suggestion_snapshot
+            .fold_snapshot
             .text_summary_for_range(input_start..input_end);
 
         let mut first_line_chars = 0;
@@ -198,7 +183,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false, None, None)
+            .chunks(range.start..line_end, false, None, None, None)
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -217,6 +202,7 @@ impl TabSnapshot {
                     false,
                     None,
                     None,
+                    None,
                 )
                 .flat_map(|chunk| chunk.text.chars())
             {
@@ -238,15 +224,17 @@ impl TabSnapshot {
         range: Range<TabPoint>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
-            self.to_suggestion_point(range.start, Bias::Left);
+            self.to_fold_point(range.start, Bias::Left);
         let input_column = input_start.column();
-        let input_start = self.suggestion_snapshot.to_offset(input_start);
+        let input_start = input_start.to_offset(&self.fold_snapshot);
         let input_end = self
-            .suggestion_snapshot
-            .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
+            .to_fold_point(range.end, Bias::Right)
+            .0
+            .to_offset(&self.fold_snapshot);
         let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
             range.end.column() - range.start.column()
         } else {
@@ -254,11 +242,12 @@ impl TabSnapshot {
         };
 
         TabChunks {
-            suggestion_chunks: self.suggestion_snapshot.chunks(
+            fold_chunks: self.fold_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                suggestion_highlight,
+                hint_highlights,
+                suggestion_highlights,
             ),
             input_column,
             column: expanded_char_column,
@@ -275,63 +264,58 @@ impl TabSnapshot {
         }
     }
 
-    pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
-        self.suggestion_snapshot.buffer_rows(row)
+    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
+        self.fold_snapshot.buffer_rows(row)
     }
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
+        self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
             .map(|chunk| chunk.text)
             .collect()
     }
 
     pub fn max_point(&self) -> TabPoint {
-        self.to_tab_point(self.suggestion_snapshot.max_point())
+        self.to_tab_point(self.fold_snapshot.max_point())
     }
 
     pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
         self.to_tab_point(
-            self.suggestion_snapshot
-                .clip_point(self.to_suggestion_point(point, bias).0, bias),
+            self.fold_snapshot
+                .clip_point(self.to_fold_point(point, bias).0, bias),
         )
     }
 
-    pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
-        let chars = self
-            .suggestion_snapshot
-            .chars_at(SuggestionPoint::new(input.row(), 0));
+    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+        let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
         let expanded = self.expand_tabs(chars, input.column());
         TabPoint::new(input.row(), expanded)
     }
 
-    pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
-        let chars = self
-            .suggestion_snapshot
-            .chars_at(SuggestionPoint::new(output.row(), 0));
+    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+        let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
         let expanded = output.column();
         let (collapsed, expanded_char_column, to_next_stop) =
             self.collapse_tabs(chars, expanded, bias);
         (
-            SuggestionPoint::new(output.row(), collapsed as u32),
+            FoldPoint::new(output.row(), collapsed as u32),
             expanded_char_column,
             to_next_stop,
         )
     }
 
     pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
-        let fold_point = self
-            .suggestion_snapshot
-            .fold_snapshot
-            .to_fold_point(point, bias);
-        let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
-        self.to_tab_point(suggestion_point)
+        let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
+        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+        self.to_tab_point(fold_point)
     }
 
     pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
-        let suggestion_point = self.to_suggestion_point(point, bias).0;
-        let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
-        fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
+        let fold_point = self.to_fold_point(point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+        self.fold_snapshot
+            .inlay_snapshot
+            .to_buffer_point(inlay_point)
     }
 
     fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
@@ -490,7 +474,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
 const SPACES: &str = "                ";
 
 pub struct TabChunks<'a> {
-    suggestion_chunks: SuggestionChunks<'a>,
+    fold_chunks: FoldChunks<'a>,
     chunk: Chunk<'a>,
     column: u32,
     max_expansion_column: u32,
@@ -506,7 +490,7 @@ impl<'a> Iterator for TabChunks<'a> {
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.chunk.text.is_empty() {
-            if let Some(chunk) = self.suggestion_chunks.next() {
+            if let Some(chunk) = self.fold_chunks.next() {
                 self.chunk = chunk;
                 if self.inside_leading_tab {
                     self.chunk.text = &self.chunk.text[1..];
@@ -574,7 +558,7 @@ impl<'a> Iterator for TabChunks<'a> {
 mod tests {
     use super::*;
     use crate::{
-        display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap},
         MultiBuffer,
     };
     use rand::{prelude::StdRng, Rng};
@@ -583,9 +567,9 @@ mod tests {
     fn test_expand_tabs(cx: &mut gpui::AppContext) {
         let buffer = MultiBuffer::build_simple("", cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
         assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
@@ -600,9 +584,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         tab_snapshot.max_expansion_column = max_expansion_column;
         assert_eq!(tab_snapshot.text(), output);
@@ -615,6 +599,7 @@ mod tests {
                         false,
                         None,
                         None,
+                        None,
                     )
                     .map(|c| c.text)
                     .collect::<String>(),
@@ -626,16 +611,16 @@ mod tests {
                 let input_point = Point::new(0, ix as u32);
                 let output_point = Point::new(0, output.find(c).unwrap() as u32);
                 assert_eq!(
-                    tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
+                    tab_snapshot.to_tab_point(FoldPoint(input_point)),
                     TabPoint(output_point),
                     "to_tab_point({input_point:?})"
                 );
                 assert_eq!(
                     tab_snapshot
-                        .to_suggestion_point(TabPoint(output_point), Bias::Left)
+                        .to_fold_point(TabPoint(output_point), Bias::Left)
                         .0,
-                    SuggestionPoint(input_point),
-                    "to_suggestion_point({output_point:?})"
+                    FoldPoint(input_point),
+                    "to_fold_point({output_point:?})"
                 );
             }
         }
@@ -648,9 +633,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         tab_snapshot.max_expansion_column = max_expansion_column;
         assert_eq!(tab_snapshot.text(), input);
@@ -662,9 +647,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(&input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         assert_eq!(
             chunks(&tab_snapshot, TabPoint::zero()),
@@ -689,7 +674,7 @@ 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) {
+            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
                 if chunk.is_tab != was_tab {
                     if !text.is_empty() {
                         chunks.push((mem::take(&mut text), was_tab));
@@ -721,15 +706,16 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         log::info!("Buffer text: {:?}", buffer_snapshot.text());
 
-        let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
         fold_map.randomly_mutate(&mut rng);
-        let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
+        let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
         log::info!("FoldMap text: {:?}", fold_snapshot.text());
-        let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
-        let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
-        log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+        let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
 
-        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
         let tabs_snapshot = tab_map.set_max_expansion_column(32);
 
         let text = text::Rope::from(tabs_snapshot.text().as_str());
@@ -757,7 +743,7 @@ mod tests {
             let expected_summary = TextSummary::from(expected_text.as_str());
             assert_eq!(
                 tabs_snapshot
-                    .chunks(start..end, false, None, None)
+                    .chunks(start..end, false, None, None, None)
                     .map(|c| c.text)
                     .collect::<String>(),
                 expected_text,
@@ -767,7 +753,7 @@ mod tests {
             );
 
             let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
-            if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
+            if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
                 actual_summary.longest_row = expected_summary.longest_row;
                 actual_summary.longest_row_chars = expected_summary.longest_row_chars;
             }

crates/editor/src/display_map/wrap_map.rs πŸ”—

@@ -1,5 +1,5 @@
 use super::{
-    suggestion_map::SuggestionBufferRows,
+    fold_map::FoldBufferRows,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
     TextHighlights,
 };
@@ -65,7 +65,7 @@ pub struct WrapChunks<'a> {
 
 #[derive(Clone)]
 pub struct WrapBufferRows<'a> {
-    input_buffer_rows: SuggestionBufferRows<'a>,
+    input_buffer_rows: FoldBufferRows<'a>,
     input_buffer_row: Option<u32>,
     output_row: u32,
     soft_wrapped: bool,
@@ -353,7 +353,7 @@ impl WrapSnapshot {
                         }
 
                         old_cursor.next(&());
-                        new_transforms.push_tree(
+                        new_transforms.append(
                             old_cursor.slice(&next_edit.old.start, Bias::Right, &()),
                             &(),
                         );
@@ -366,7 +366,7 @@ impl WrapSnapshot {
                         new_transforms.push_or_extend(Transform::isomorphic(summary));
                     }
                     old_cursor.next(&());
-                    new_transforms.push_tree(old_cursor.suffix(&()), &());
+                    new_transforms.append(old_cursor.suffix(&()), &());
                 }
             }
         }
@@ -446,6 +446,7 @@ impl WrapSnapshot {
                     false,
                     None,
                     None,
+                    None,
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -500,7 +501,7 @@ impl WrapSnapshot {
                             new_transforms.push_or_extend(Transform::isomorphic(summary));
                         }
                         old_cursor.next(&());
-                        new_transforms.push_tree(
+                        new_transforms.append(
                             old_cursor.slice(
                                 &TabPoint::new(next_edit.old_rows.start, 0),
                                 Bias::Right,
@@ -517,7 +518,7 @@ impl WrapSnapshot {
                         new_transforms.push_or_extend(Transform::isomorphic(summary));
                     }
                     old_cursor.next(&());
-                    new_transforms.push_tree(old_cursor.suffix(&()), &());
+                    new_transforms.append(old_cursor.suffix(&()), &());
                 }
             }
         }
@@ -575,7 +576,8 @@ impl WrapSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
@@ -593,7 +595,8 @@ impl WrapSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                suggestion_highlight,
+                hint_highlights,
+                suggestion_highlights,
             ),
             input_chunk: Default::default(),
             output_position: output_start,
@@ -757,28 +760,18 @@ impl WrapSnapshot {
             }
 
             let text = language::Rope::from(self.text().as_str());
-            let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
+            let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
             let mut expected_buffer_rows = Vec::new();
-            let mut prev_fold_row = 0;
+            let mut prev_tab_row = 0;
             for display_row in 0..=self.max_point().row() {
                 let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
-                let suggestion_point = self
-                    .tab_snapshot
-                    .to_suggestion_point(tab_point, Bias::Left)
-                    .0;
-                let fold_point = self
-                    .tab_snapshot
-                    .suggestion_snapshot
-                    .to_fold_point(suggestion_point);
-                if fold_point.row() == prev_fold_row && display_row != 0 {
+                if tab_point.row() == prev_tab_row && display_row != 0 {
                     expected_buffer_rows.push(None);
                 } else {
-                    let buffer_point = fold_point
-                        .to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
-                    expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
-                    prev_fold_row = fold_point.row();
+                    expected_buffer_rows.push(input_buffer_rows.next().unwrap());
                 }
 
+                prev_tab_row = tab_point.row();
                 assert_eq!(self.line_len(display_row), text.line_len(display_row));
             }
 
@@ -1038,7 +1031,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
 mod tests {
     use super::*;
     use crate::{
-        display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
         MultiBuffer,
     };
     use gpui::test::observe;
@@ -1089,11 +1082,11 @@ mod tests {
         });
         let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
         log::info!("Buffer text: {:?}", buffer_snapshot.text());
-        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
         log::info!("FoldMap text: {:?}", fold_snapshot.text());
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-        log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
-        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
         let tabs_snapshot = tab_map.set_max_expansion_column(32);
         log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
@@ -1122,6 +1115,7 @@ mod tests {
         );
         log::info!("Wrapped text: {:?}", actual_text);
 
+        let mut next_inlay_id = 0;
         let mut edits = Vec::new();
         for _i in 0..operations {
             log::info!("{} ==============================================", _i);
@@ -1139,10 +1133,8 @@ mod tests {
                 }
                 20..=39 => {
                     for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-                        let (suggestion_snapshot, suggestion_edits) =
-                            suggestion_map.sync(fold_snapshot, fold_edits);
                         let (tabs_snapshot, tab_edits) =
-                            tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                            tab_map.sync(fold_snapshot, fold_edits, tab_size);
                         let (mut snapshot, wrap_edits) =
                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
@@ -1151,10 +1143,11 @@ mod tests {
                     }
                 }
                 40..=59 => {
-                    let (suggestion_snapshot, suggestion_edits) =
-                        suggestion_map.randomly_mutate(&mut rng);
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tabs_snapshot, tab_edits) =
-                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (mut snapshot, wrap_edits) =
                         wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                     snapshot.check_invariants();
@@ -1173,13 +1166,12 @@ mod tests {
             }
 
             log::info!("Buffer text: {:?}", buffer_snapshot.text());
-            let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
             log::info!("FoldMap text: {:?}", fold_snapshot.text());
-            let (suggestion_snapshot, suggestion_edits) =
-                suggestion_map.sync(fold_snapshot, fold_edits);
-            log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
-            let (tabs_snapshot, tab_edits) =
-                tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
             log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();
@@ -1227,7 +1219,7 @@ mod tests {
                 if tab_size.get() == 1
                     || !wrapped_snapshot
                         .tab_snapshot
-                        .suggestion_snapshot
+                        .fold_snapshot
                         .text()
                         .contains('\t')
                 {
@@ -1328,8 +1320,14 @@ mod tests {
         }
 
         pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-            self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
-                .map(|h| h.text)
+            self.chunks(
+                wrap_row..self.max_point().row() + 1,
+                false,
+                None,
+                None,
+                None,
+            )
+            .map(|h| h.text)
         }
 
         fn verify_chunks(&mut self, rng: &mut impl Rng) {
@@ -1352,7 +1350,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true, None, None)
+                    .chunks(start_row..end_row, true, None, None, None)
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/editor.rs πŸ”—

@@ -2,6 +2,7 @@ mod blink_manager;
 pub mod display_map;
 mod editor_settings;
 mod element;
+mod inlay_hint_cache;
 
 mod git;
 mod highlight_matching_bracket;
@@ -25,7 +26,7 @@ use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Result};
 use blink_manager::BlinkManager;
 use client::{ClickhouseEvent, TelemetrySettings};
-use clock::ReplicaId;
+use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use copilot::Copilot;
 pub use display_map::DisplayPoint;
@@ -52,11 +53,12 @@ use gpui::{
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
+use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
 pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
-    language_settings::{self, all_language_settings},
+    language_settings::{self, all_language_settings, InlayHintSettings},
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
     Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
     OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@@ -64,11 +66,12 @@ use language::{
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
 };
+use log::error;
+use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
     ToPoint,
 };
-use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
 use scroll::{
@@ -85,12 +88,13 @@ use std::{
     cmp::{self, Ordering, Reverse},
     mem,
     num::NonZeroU32,
-    ops::{Deref, DerefMut, Range},
+    ops::{ControlFlow, Deref, DerefMut, Range},
     path::Path,
     sync::Arc,
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
+use text::Rope;
 use theme::{DiagnosticStyle, Theme, ThemeSettings};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{ItemNavHistory, ViewId, Workspace};
@@ -180,6 +184,21 @@ pub struct GutterHover {
     pub hovered: bool,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+    Suggestion(usize),
+    Hint(usize),
+}
+
+impl InlayId {
+    fn id(&self) -> usize {
+        match self {
+            Self::Suggestion(id) => *id,
+            Self::Hint(id) => *id,
+        }
+    }
+}
+
 actions!(
     editor,
     [
@@ -206,6 +225,7 @@ actions!(
         DuplicateLine,
         MoveLineUp,
         MoveLineDown,
+        JoinLines,
         Transpose,
         Cut,
         Copy,
@@ -321,6 +341,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::indent);
     cx.add_action(Editor::outdent);
     cx.add_action(Editor::delete_line);
+    cx.add_action(Editor::join_lines);
     cx.add_action(Editor::delete_to_previous_word_start);
     cx.add_action(Editor::delete_to_previous_subword_start);
     cx.add_action(Editor::delete_to_next_word_end);
@@ -533,6 +554,8 @@ pub struct Editor {
     gutter_hovered: bool,
     link_go_to_definition_state: LinkGoToDefinitionState,
     copilot_state: CopilotState,
+    inlay_hint_cache: InlayHintCache,
+    next_inlay_id: usize,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -1054,6 +1077,7 @@ pub struct CopilotState {
     cycled: bool,
     completions: Vec<copilot::Completion>,
     active_completion_index: usize,
+    suggestion: Option<Inlay>,
 }
 
 impl Default for CopilotState {
@@ -1065,6 +1089,7 @@ impl Default for CopilotState {
             completions: Default::default(),
             active_completion_index: 0,
             cycled: false,
+            suggestion: None,
         }
     }
 }
@@ -1179,6 +1204,14 @@ enum GotoDefinitionKind {
     Type,
 }
 
+#[derive(Debug, Clone)]
+enum InlayRefreshReason {
+    SettingsChange(InlayHintSettings),
+    NewLinesShown,
+    BufferEdited(HashSet<Arc<Language>>),
+    RefreshRequested,
+}
+
 impl Editor {
     pub fn single_line(
         field_editor_style: Option<Arc<GetFieldEditorTheme>>,
@@ -1280,15 +1313,28 @@ impl Editor {
         let soft_wrap_mode_override =
             (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
 
-        let mut project_subscription = None;
-        if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
+        let mut project_subscriptions = Vec::new();
+        if mode == EditorMode::Full {
             if let Some(project) = project.as_ref() {
-                project_subscription = Some(cx.observe(project, |_, _, cx| {
-                    cx.emit(Event::TitleChanged);
-                }))
+                if buffer.read(cx).is_singleton() {
+                    project_subscriptions.push(cx.observe(project, |_, _, cx| {
+                        cx.emit(Event::TitleChanged);
+                    }));
+                }
+                project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
+                    if let project::Event::RefreshInlays = event {
+                        editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
+                    };
+                }));
             }
         }
 
+        let inlay_hint_settings = inlay_hint_settings(
+            selections.newest_anchor().head(),
+            &buffer.read(cx).snapshot(cx),
+            cx,
+        );
+
         let mut this = Self {
             handle: cx.weak_handle(),
             buffer: buffer.clone(),
@@ -1322,6 +1368,7 @@ impl Editor {
                 .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
             completion_tasks: Default::default(),
             next_completion_id: 0,
+            next_inlay_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
             document_highlights_task: Default::default(),
@@ -1338,6 +1385,7 @@ impl Editor {
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
             copilot_state: Default::default(),
+            inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
             gutter_hovered: false,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
@@ -1348,9 +1396,7 @@ impl Editor {
             ],
         };
 
-        if let Some(project_subscription) = project_subscription {
-            this._subscriptions.push(project_subscription);
-        }
+        this._subscriptions.extend(project_subscriptions);
 
         this.end_selection(cx);
         this.scroll_manager.show_scrollbar(cx);
@@ -1871,7 +1917,7 @@ impl Editor {
                 s.set_pending(pending, mode);
             });
         } else {
-            log::error!("update_selection dispatched with no pending selection");
+            error!("update_selection dispatched with no pending selection");
             return;
         }
 
@@ -1989,6 +2035,7 @@ impl Editor {
         }
 
         let selections = self.selections.all_adjusted(cx);
+        let mut brace_inserted = false;
         let mut edits = Vec::new();
         let mut new_selections = Vec::with_capacity(selections.len());
         let mut new_autoclose_regions = Vec::new();
@@ -2047,6 +2094,7 @@ impl Editor {
                                     selection.range(),
                                     format!("{}{}", text, bracket_pair.end).into(),
                                 ));
+                                brace_inserted = true;
                                 continue;
                             }
                         }
@@ -2073,6 +2121,7 @@ impl Editor {
                             selection.end..selection.end,
                             bracket_pair.end.as_str().into(),
                         ));
+                        brace_inserted = true;
                         new_selections.push((
                             Selection {
                                 id: selection.id,
@@ -2140,8 +2189,7 @@ impl Editor {
             let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
 
-            // When buffer contents is updated and caret is moved, try triggering on type formatting.
-            if settings::get::<EditorSettings>(cx).use_on_type_format {
+            if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
                 if let Some(on_type_format_task) =
                     this.trigger_on_type_formatting(text.to_string(), cx)
                 {
@@ -2575,6 +2623,108 @@ impl Editor {
         }
     }
 
+    fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
+        if self.project.is_none() || self.mode != EditorMode::Full {
+            return;
+        }
+
+        let (invalidate_cache, required_languages) = match reason {
+            InlayRefreshReason::SettingsChange(new_settings) => {
+                match self.inlay_hint_cache.update_settings(
+                    &self.buffer,
+                    new_settings,
+                    self.visible_inlay_hints(cx),
+                    cx,
+                ) {
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove,
+                        to_insert,
+                    })) => {
+                        self.splice_inlay_hints(to_remove, to_insert, cx);
+                        return;
+                    }
+                    ControlFlow::Break(None) => return,
+                    ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
+                }
+            }
+            InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+            InlayRefreshReason::BufferEdited(buffer_languages) => {
+                (InvalidationStrategy::BufferEdited, Some(buffer_languages))
+            }
+            InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
+        };
+
+        self.inlay_hint_cache.refresh_inlay_hints(
+            self.excerpt_visible_offsets(required_languages.as_ref(), cx),
+            invalidate_cache,
+            cx,
+        )
+    }
+
+    fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(move |inlay| {
+                Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
+            })
+            .cloned()
+            .collect()
+    }
+
+    fn excerpt_visible_offsets(
+        &self,
+        restrict_to_languages: Option<&HashSet<Arc<Language>>>,
+        cx: &mut ViewContext<'_, '_, Editor>,
+    ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
+        let multi_buffer = self.buffer().read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+        let multi_buffer_visible_start = self
+            .scroll_manager
+            .anchor()
+            .anchor
+            .to_point(&multi_buffer_snapshot);
+        let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
+            multi_buffer_visible_start
+                + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
+            Bias::Left,
+        );
+        let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
+        multi_buffer
+            .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+            .into_iter()
+            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+            .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
+                let buffer = buffer_handle.read(cx);
+                let language = buffer.language()?;
+                if let Some(restrict_to_languages) = restrict_to_languages {
+                    if !restrict_to_languages.contains(language) {
+                        return None;
+                    }
+                }
+                Some((
+                    excerpt_id,
+                    (
+                        buffer_handle,
+                        buffer.version().clone(),
+                        excerpt_visible_range,
+                    ),
+                ))
+            })
+            .collect()
+    }
+
+    fn splice_inlay_hints(
+        &self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<Inlay>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.splice_inlays(to_remove, to_insert, cx);
+        });
+    }
+
     fn trigger_on_type_formatting(
         &self,
         input: String,
@@ -3225,10 +3375,7 @@ impl Editor {
     }
 
     fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(suggestion) = self
-            .display_map
-            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
-        {
+        if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
             if let Some((copilot, completion)) =
                 Copilot::global(cx).zip(self.copilot_state.active_completion())
             {
@@ -3247,7 +3394,7 @@ impl Editor {
     }
 
     fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if self.has_active_copilot_suggestion(cx) {
+        if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
             if let Some(copilot) = Copilot::global(cx) {
                 copilot
                     .update(cx, |copilot, cx| {
@@ -3258,8 +3405,9 @@ impl Editor {
                 self.report_copilot_event(None, false, cx)
             }
 
-            self.display_map
-                .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+            self.display_map.update(cx, |map, cx| {
+                map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
+            });
             cx.notify();
             true
         } else {
@@ -3280,7 +3428,26 @@ impl Editor {
     }
 
     fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
-        self.display_map.read(cx).has_suggestion()
+        if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+            let buffer = self.buffer.read(cx).read(cx);
+            suggestion.position.is_valid(&buffer)
+        } else {
+            false
+        }
+    }
+
+    fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+        let suggestion = self.copilot_state.suggestion.take()?;
+        self.display_map.update(cx, |map, cx| {
+            map.splice_inlays(vec![suggestion.id], Default::default(), cx);
+        });
+        let buffer = self.buffer.read(cx).read(cx);
+
+        if suggestion.position.is_valid(&buffer) {
+            Some(suggestion)
+        } else {
+            None
+        }
     }
 
     fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
@@ -3297,14 +3464,17 @@ impl Editor {
             .copilot_state
             .text_for_active_completion(cursor, &snapshot)
         {
+            let text = Rope::from(text);
+            let mut to_remove = Vec::new();
+            if let Some(suggestion) = self.copilot_state.suggestion.take() {
+                to_remove.push(suggestion.id);
+            }
+
+            let suggestion_inlay =
+                Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
+            self.copilot_state.suggestion = Some(suggestion_inlay.clone());
             self.display_map.update(cx, move |map, cx| {
-                map.replace_suggestion(
-                    Some(Suggestion {
-                        position: cursor,
-                        text: text.trim_end().into(),
-                    }),
-                    cx,
-                )
+                map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
             });
             cx.notify();
         } else {
@@ -3320,15 +3490,21 @@ impl Editor {
     pub fn render_code_actions_indicator(
         &self,
         style: &EditorStyle,
-        active: bool,
+        is_active: bool,
         cx: &mut ViewContext<Self>,
     ) -> Option<AnyElement<Self>> {
         if self.available_code_actions.is_some() {
             enum CodeActions {}
             Some(
                 MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
-                    Svg::new("icons/bolt_8.svg")
-                        .with_color(style.code_actions.indicator.style_for(state, active).color)
+                    Svg::new("icons/bolt_8.svg").with_color(
+                        style
+                            .code_actions
+                            .indicator
+                            .in_state(is_active)
+                            .style_for(state)
+                            .color,
+                    )
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .with_padding(Padding::uniform(3.))
@@ -3378,10 +3554,8 @@ impl Editor {
                                     .with_color(
                                         style
                                             .indicator
-                                            .style_for(
-                                                mouse_state,
-                                                fold_status == FoldStatus::Folded,
-                                            )
+                                            .in_state(fold_status == FoldStatus::Folded)
+                                            .style_for(mouse_state)
                                             .color,
                                     )
                                     .constrained()
@@ -3952,6 +4126,60 @@ impl Editor {
         });
     }
 
+    pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
+        let mut row_ranges = Vec::<Range<u32>>::new();
+        for selection in self.selections.all::<Point>(cx) {
+            let start = selection.start.row;
+            let end = if selection.start.row == selection.end.row {
+                selection.start.row + 1
+            } else {
+                selection.end.row
+            };
+
+            if let Some(last_row_range) = row_ranges.last_mut() {
+                if start <= last_row_range.end {
+                    last_row_range.end = end;
+                    continue;
+                }
+            }
+            row_ranges.push(start..end);
+        }
+
+        let snapshot = self.buffer.read(cx).snapshot(cx);
+        let mut cursor_positions = Vec::new();
+        for row_range in &row_ranges {
+            let anchor = snapshot.anchor_before(Point::new(
+                row_range.end - 1,
+                snapshot.line_len(row_range.end - 1),
+            ));
+            cursor_positions.push(anchor.clone()..anchor);
+        }
+
+        self.transact(cx, |this, cx| {
+            for row_range in row_ranges.into_iter().rev() {
+                for row in row_range.rev() {
+                    let end_of_line = Point::new(row, snapshot.line_len(row));
+                    let indent = snapshot.indent_size_for_line(row + 1);
+                    let start_of_next_line = Point::new(row + 1, indent.len);
+
+                    let replace = if snapshot.line_len(row + 1) > indent.len {
+                        " "
+                    } else {
+                        ""
+                    };
+
+                    this.buffer.update(cx, |buffer, cx| {
+                        buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
+                    });
+                }
+            }
+
+            this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.select_anchor_ranges(cursor_positions)
+            });
+        });
+    }
+
     pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
@@ -6581,7 +6809,7 @@ impl Editor {
             if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
                 *end_selections = Some(self.selections.disjoint_anchors());
             } else {
-                log::error!("unexpectedly ended a transaction that wasn't started by this editor");
+                error!("unexpectedly ended a transaction that wasn't started by this editor");
             }
 
             cx.emit(Event::Edited);
@@ -7031,7 +7259,7 @@ impl Editor {
 
     fn on_buffer_event(
         &mut self,
-        _: ModelHandle<MultiBuffer>,
+        multibuffer: ModelHandle<MultiBuffer>,
         event: &multi_buffer::Event,
         cx: &mut ViewContext<Self>,
     ) {
@@ -7043,6 +7271,33 @@ impl Editor {
                     self.update_visible_copilot_suggestion(cx);
                 }
                 cx.emit(Event::BufferEdited);
+
+                if let Some(project) = &self.project {
+                    let project = project.read(cx);
+                    let languages_affected = multibuffer
+                        .read(cx)
+                        .all_buffers()
+                        .into_iter()
+                        .filter_map(|buffer| {
+                            let buffer = buffer.read(cx);
+                            let language = buffer.language()?;
+                            if project.is_local()
+                                && project.language_servers_for_buffer(buffer, cx).count() == 0
+                            {
+                                None
+                            } else {
+                                Some(language)
+                            }
+                        })
+                        .cloned()
+                        .collect::<HashSet<_>>();
+                    if !languages_affected.is_empty() {
+                        self.refresh_inlays(
+                            InlayRefreshReason::BufferEdited(languages_affected),
+                            cx,
+                        );
+                    }
+                }
             }
             multi_buffer::Event::ExcerptsAdded {
                 buffer,
@@ -7067,7 +7322,7 @@ impl Editor {
                 self.refresh_active_diagnostics(cx);
             }
             _ => {}
-        }
+        };
     }
 
     fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
@@ -7076,6 +7331,14 @@ impl Editor {
 
     fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
         self.refresh_copilot_suggestions(true, cx);
+        self.refresh_inlays(
+            InlayRefreshReason::SettingsChange(inlay_hint_settings(
+                self.selections.newest_anchor().head(),
+                &self.buffer.read(cx).snapshot(cx),
+                cx,
+            )),
+            cx,
+        );
     }
 
     pub fn set_searchable(&mut self, searchable: bool) {
@@ -7365,6 +7628,23 @@ impl Editor {
         let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; };
         cx.write_to_clipboard(ClipboardItem::new(lines));
     }
+
+    pub fn inlay_hint_cache(&self) -> &InlayHintCache {
+        &self.inlay_hint_cache
+    }
+}
+
+fn inlay_hint_settings(
+    location: Anchor,
+    snapshot: &MultiBufferSnapshot,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) -> InlayHintSettings {
+    let file = snapshot.file_at(location);
+    let language = snapshot.language_at(location);
+    let settings = all_language_settings(file, cx);
+    settings
+        .language(language.map(|l| l.name()).as_deref())
+        .inlay_hints
 }
 
 fn consume_contiguous_rows(
@@ -7581,8 +7861,14 @@ impl View for Editor {
             keymap.add_identifier("renaming");
         }
         match self.context_menu.as_ref() {
-            Some(ContextMenu::Completions(_)) => keymap.add_identifier("showing_completions"),
-            Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
+            Some(ContextMenu::Completions(_)) => {
+                keymap.add_identifier("menu");
+                keymap.add_identifier("showing_completions")
+            }
+            Some(ContextMenu::CodeActions(_)) => {
+                keymap.add_identifier("menu");
+                keymap.add_identifier("showing_code_actions")
+            }
             None => {}
         }
         for layer in self.keymap_context_layers.values() {
@@ -7949,6 +8235,7 @@ impl Deref for EditorStyle {
 
 pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
     let mut highlighted_lines = Vec::new();
+
     for (index, line) in diagnostic.message.lines().enumerate() {
         let line = match &diagnostic.source {
             Some(source) if index == 0 => {
@@ -7960,25 +8247,44 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
         };
         highlighted_lines.push(line);
     }
-
+    let message = diagnostic.message;
     Arc::new(move |cx: &mut BlockContext| {
+        let message = message.clone();
         let settings = settings::get::<ThemeSettings>(cx);
+        let tooltip_style = settings.theme.tooltip.clone();
         let theme = &settings.theme.editor;
         let style = diagnostic_style(diagnostic.severity, is_valid, theme);
         let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
-        Flex::column()
-            .with_children(highlighted_lines.iter().map(|(line, highlights)| {
-                Label::new(
-                    line.clone(),
-                    style.message.clone().with_font_size(font_size),
-                )
-                .with_highlights(highlights.clone())
-                .contained()
-                .with_margin_left(cx.anchor_x)
-            }))
-            .aligned()
-            .left()
-            .into_any()
+        let anchor_x = cx.anchor_x;
+        enum BlockContextToolip {}
+        MouseEventHandler::<BlockContext, _>::new(cx.block_id, cx, |_, _| {
+            Flex::column()
+                .with_children(highlighted_lines.iter().map(|(line, highlights)| {
+                    Label::new(
+                        line.clone(),
+                        style.message.clone().with_font_size(font_size),
+                    )
+                    .with_highlights(highlights.clone())
+                    .contained()
+                    .with_margin_left(anchor_x)
+                }))
+                .aligned()
+                .left()
+                .into_any()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, _, cx| {
+            cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+        })
+        // We really need to rethink this ID system...
+        .with_tooltip::<BlockContextToolip>(
+            cx.block_id,
+            "Copy diagnostic message".to_string(),
+            None,
+            tooltip_style,
+            cx,
+        )
+        .into_any()
     })
 }
 

crates/editor/src/editor_tests.rs πŸ”—

@@ -1,7 +1,11 @@
 use super::*;
-use crate::test::{
-    assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
-    editor_test_context::EditorTestContext, select_ranges,
+use crate::{
+    scroll::scroll_amount::ScrollAmount,
+    test::{
+        assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
+        editor_test_context::EditorTestContext, select_ranges,
+    },
+    JoinLines,
 };
 use drag_and_drop::DragAndDrop;
 use futures::StreamExt;
@@ -1356,6 +1360,43 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
     );
 }
 
+#[gpui::test]
+async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+    cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5));
+
+    cx.set_state(
+        &r#"Λ‡one
+        two
+        three
+        four
+        five
+        six
+        seven
+        eight
+        nine
+        ten
+        "#,
+    );
+
+    cx.update_editor(|editor, cx| {
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.));
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+        editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.));
+        editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+
+        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.));
+        editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+    });
+}
+
 #[gpui::test]
 async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -2325,6 +2366,137 @@ fn test_delete_line(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let buffer = buffer.read(cx).as_singleton().unwrap();
+
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 0)..Point::new(0, 0)]
+        );
+
+        // When on single line, replace newline at end by space
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 3)..Point::new(0, 3)]
+        );
+
+        // When multiple lines are selected, remove newlines that are spanned by the selection
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
+        });
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 11)..Point::new(0, 11)]
+        );
+
+        // Undo should be transactional
+        editor.undo(&Undo, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            &[Point::new(0, 5)..Point::new(2, 2)]
+        );
+
+        // When joining an empty line don't insert a space
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
+        });
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [Point::new(2, 3)..Point::new(2, 3)]
+        );
+
+        // We can remove trailing newlines
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [Point::new(2, 3)..Point::new(2, 3)]
+        );
+
+        // We don't blow up on the last line
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [Point::new(2, 3)..Point::new(2, 3)]
+        );
+
+        // reset to test indentation
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                [
+                    (Point::new(1, 0)..Point::new(1, 2), "  "),
+                    (Point::new(2, 0)..Point::new(2, 3), "  \n\td"),
+                ],
+                None,
+                cx,
+            )
+        });
+
+        // We remove any leading spaces
+        assert_eq!(buffer.read(cx).text(), "aaa bbb\n  c\n  \n\td");
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
+        });
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb c\n  \n\td");
+
+        // We don't insert a space for a line containing only spaces
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
+
+        // We ignore any leading tabs
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
+
+        editor
+    });
+}
+
+#[gpui::test]
+fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    cx.add_window(|cx| {
+        let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+        let mut editor = build_editor(buffer.clone(), cx);
+        let buffer = buffer.read(cx).as_singleton().unwrap();
+
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([
+                Point::new(0, 2)..Point::new(1, 1),
+                Point::new(1, 2)..Point::new(1, 2),
+                Point::new(3, 1)..Point::new(3, 2),
+            ])
+        });
+
+        editor.join_lines(&JoinLines, cx);
+        assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
+
+        assert_eq!(
+            editor.selections.ranges::<Point>(cx),
+            [
+                Point::new(0, 7)..Point::new(0, 7),
+                Point::new(1, 3)..Point::new(1, 3)
+            ]
+        );
+        editor
+    });
+}
+
 #[gpui::test]
 fn test_duplicate_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -6807,6 +6979,111 @@ async fn test_copilot_disabled_globs(
     assert!(copilot_requests.try_next().is_ok());
 }
 
+#[gpui::test]
+async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            brackets: BracketPairConfig {
+                pairs: vec![BracketPair {
+                    start: "{".to_string(),
+                    end: "}".to_string(),
+                    close: true,
+                    newline: true,
+                }],
+                disabled_scopes_by_bracket_ix: Vec::new(),
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+                    first_trigger_character: "{".to_string(),
+                    more_trigger_character: None,
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/a",
+        json!({
+            "main.rs": "fn main() { let a = 5; }",
+            "other.rs": "// Test file",
+        }),
+    )
+    .await;
+    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+    let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+    let worktree_id = workspace.update(cx, |workspace, cx| {
+        workspace.project().read_with(cx, |project, cx| {
+            project.worktrees(cx).next().unwrap().read(cx).id()
+        })
+    });
+
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    cx.foreground().run_until_parked();
+    cx.foreground().start_waiting();
+    let fake_server = fake_servers.next().await.unwrap();
+    let editor_handle = workspace
+        .update(cx, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+        assert_eq!(
+            params.text_document_position.text_document.uri,
+            lsp::Url::from_file_path("/a/main.rs").unwrap(),
+        );
+        assert_eq!(
+            params.text_document_position.position,
+            lsp::Position::new(0, 21),
+        );
+
+        Ok(Some(vec![lsp::TextEdit {
+            new_text: "]".to_string(),
+            range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
+        }]))
+    });
+
+    editor_handle.update(cx, |editor, cx| {
+        cx.focus(&editor_handle);
+        editor.change_selections(None, cx, |s| {
+            s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
+        });
+        editor.handle_input("{", cx);
+    });
+
+    cx.foreground().run_until_parked();
+
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(
+            buffer.text(),
+            "fn main() { let a = {5}; }",
+            "No extra braces from on type formatting should appear in the buffer"
+        )
+    });
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/editor/src/element.rs πŸ”—

@@ -1433,7 +1433,12 @@ impl EditorElement {
         } else {
             let style = &self.style;
             let chunks = snapshot
-                .chunks(rows.clone(), true, Some(style.theme.suggestion))
+                .chunks(
+                    rows.clone(),
+                    true,
+                    Some(style.theme.hint),
+                    Some(style.theme.suggestion),
+                )
                 .map(|chunk| {
                     let mut highlight_style = chunk
                         .syntax_highlight_id
@@ -1508,6 +1513,7 @@ impl EditorElement {
         editor: &mut Editor,
         cx: &mut LayoutContext<Editor>,
     ) -> (f32, Vec<BlockLayout>) {
+        let mut block_id = 0;
         let scroll_x = snapshot.scroll_anchor.offset.x();
         let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
@@ -1515,7 +1521,7 @@ impl EditorElement {
                 TransformBlock::ExcerptHeader { .. } => false,
                 TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
             });
-        let mut render_block = |block: &TransformBlock, width: f32| {
+        let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
             let mut element = match block {
                 TransformBlock::Custom(block) => {
                     let align_to = block
@@ -1540,6 +1546,7 @@ impl EditorElement {
                         scroll_x,
                         gutter_width,
                         em_width,
+                        block_id,
                     })
                 }
                 TransformBlock::ExcerptHeader {
@@ -1568,7 +1575,7 @@ impl EditorElement {
 
                         enum JumpIcon {}
                         MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
-                            let style = style.jump_icon.style_for(state, false);
+                            let style = style.jump_icon.style_for(state);
                             Svg::new("icons/arrow_up_right_8.svg")
                                 .with_color(style.color)
                                 .constrained()
@@ -1675,7 +1682,8 @@ impl EditorElement {
         let mut fixed_block_max_width = 0f32;
         let mut blocks = Vec::new();
         for (row, block) in fixed_blocks {
-            let element = render_block(block, f32::INFINITY);
+            let element = render_block(block, f32::INFINITY, block_id);
+            block_id += 1;
             fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
             blocks.push(BlockLayout {
                 row,
@@ -1695,7 +1703,8 @@ impl EditorElement {
                     .max(gutter_width + scroll_width),
                 BlockStyle::Fixed => unreachable!(),
             };
-            let element = render_block(block, width);
+            let element = render_block(block, width, block_id);
+            block_id += 1;
             blocks.push(BlockLayout {
                 row,
                 element,
@@ -1958,7 +1967,7 @@ impl Element<Editor> for EditorElement {
         let em_advance = style.text.em_advance(cx.font_cache());
         let overscroll = vec2f(em_width, 0.);
         let snapshot = {
-            editor.set_visible_line_count(size.y() / line_height);
+            editor.set_visible_line_count(size.y() / line_height, cx);
 
             let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
             let wrap_width = match editor.soft_wrap_mode(cx) {
@@ -2131,7 +2140,7 @@ impl Element<Editor> for EditorElement {
                     .folds
                     .ellipses
                     .background
-                    .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
+                    .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
                     .color;
 
                 (id, fold, color)

crates/editor/src/inlay_hint_cache.rs πŸ”—

@@ -0,0 +1,2275 @@
+use std::{
+    cmp,
+    ops::{ControlFlow, Range},
+    sync::Arc,
+};
+
+use crate::{
+    display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
+};
+use anyhow::Context;
+use clock::Global;
+use gpui::{ModelHandle, Task, ViewContext};
+use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
+use log::error;
+use parking_lot::RwLock;
+use project::InlayHint;
+
+use collections::{hash_map, HashMap, HashSet};
+use language::language_settings::InlayHintSettings;
+use util::post_inc;
+
+pub struct InlayHintCache {
+    pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+    pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+    pub version: usize,
+    pub enabled: bool,
+    update_tasks: HashMap<ExcerptId, UpdateTask>,
+}
+
+#[derive(Debug)]
+pub struct CachedExcerptHints {
+    version: usize,
+    buffer_version: Global,
+    buffer_id: u64,
+    pub hints: Vec<(InlayId, InlayHint)>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+    RefreshRequested,
+    BufferEdited,
+    None,
+}
+
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+    pub to_remove: Vec<InlayId>,
+    pub to_insert: Vec<Inlay>,
+}
+
+struct UpdateTask {
+    invalidate: InvalidationStrategy,
+    cache_version: usize,
+    task: RunningTask,
+    pending_refresh: Option<ExcerptQuery>,
+}
+
+struct RunningTask {
+    _task: Task<()>,
+    is_running_rx: smol::channel::Receiver<()>,
+}
+
+#[derive(Debug)]
+struct ExcerptHintsUpdate {
+    excerpt_id: ExcerptId,
+    remove_from_visible: Vec<InlayId>,
+    remove_from_cache: HashSet<InlayId>,
+    add_to_cache: HashSet<InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptQuery {
+    buffer_id: u64,
+    excerpt_id: ExcerptId,
+    dimensions: ExcerptDimensions,
+    cache_version: usize,
+    invalidate: InvalidationStrategy,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptDimensions {
+    excerpt_range_start: language::Anchor,
+    excerpt_range_end: language::Anchor,
+    excerpt_visible_range_start: language::Anchor,
+    excerpt_visible_range_end: language::Anchor,
+}
+
+struct HintFetchRanges {
+    visible_range: Range<language::Anchor>,
+    other_ranges: Vec<Range<language::Anchor>>,
+}
+
+impl InvalidationStrategy {
+    fn should_invalidate(&self) -> bool {
+        matches!(
+            self,
+            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
+        )
+    }
+}
+
+impl ExcerptQuery {
+    fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
+        let visible_range =
+            self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
+        let mut other_ranges = Vec::new();
+        if self
+            .dimensions
+            .excerpt_range_start
+            .cmp(&visible_range.start, buffer)
+            .is_lt()
+        {
+            let mut end = visible_range.start;
+            end.offset -= 1;
+            other_ranges.push(self.dimensions.excerpt_range_start..end);
+        }
+        if self
+            .dimensions
+            .excerpt_range_end
+            .cmp(&visible_range.end, buffer)
+            .is_gt()
+        {
+            let mut start = visible_range.end;
+            start.offset += 1;
+            other_ranges.push(start..self.dimensions.excerpt_range_end);
+        }
+
+        HintFetchRanges {
+            visible_range,
+            other_ranges: other_ranges.into_iter().map(|range| range).collect(),
+        }
+    }
+}
+
+impl InlayHintCache {
+    pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
+        Self {
+            allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
+            enabled: inlay_hint_settings.enabled,
+            hints: HashMap::default(),
+            update_tasks: HashMap::default(),
+            version: 0,
+        }
+    }
+
+    pub fn update_settings(
+        &mut self,
+        multi_buffer: &ModelHandle<MultiBuffer>,
+        new_hint_settings: InlayHintSettings,
+        visible_hints: Vec<Inlay>,
+        cx: &mut ViewContext<Editor>,
+    ) -> ControlFlow<Option<InlaySplice>> {
+        let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
+        match (self.enabled, new_hint_settings.enabled) {
+            (false, false) => {
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                ControlFlow::Break(None)
+            }
+            (true, true) => {
+                if new_allowed_hint_kinds == self.allowed_hint_kinds {
+                    ControlFlow::Break(None)
+                } else {
+                    let new_splice = self.new_allowed_hint_kinds_splice(
+                        multi_buffer,
+                        &visible_hints,
+                        &new_allowed_hint_kinds,
+                        cx,
+                    );
+                    if new_splice.is_some() {
+                        self.version += 1;
+                        self.update_tasks.clear();
+                        self.allowed_hint_kinds = new_allowed_hint_kinds;
+                    }
+                    ControlFlow::Break(new_splice)
+                }
+            }
+            (true, false) => {
+                self.enabled = new_hint_settings.enabled;
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                if self.hints.is_empty() {
+                    ControlFlow::Break(None)
+                } else {
+                    self.clear();
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
+                        to_insert: Vec::new(),
+                    }))
+                }
+            }
+            (false, true) => {
+                self.enabled = new_hint_settings.enabled;
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                ControlFlow::Continue(())
+            }
+        }
+    }
+
+    pub fn refresh_inlay_hints(
+        &mut self,
+        mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+        invalidate: InvalidationStrategy,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        if !self.enabled || excerpts_to_query.is_empty() {
+            return;
+        }
+        let update_tasks = &mut self.update_tasks;
+        if invalidate.should_invalidate() {
+            update_tasks
+                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+        }
+        let cache_version = self.version;
+        excerpts_to_query.retain(|visible_excerpt_id, _| {
+            match update_tasks.entry(*visible_excerpt_id) {
+                hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
+                    cmp::Ordering::Less => true,
+                    cmp::Ordering::Equal => invalidate.should_invalidate(),
+                    cmp::Ordering::Greater => false,
+                },
+                hash_map::Entry::Vacant(_) => true,
+            }
+        });
+
+        cx.spawn(|editor, mut cx| async move {
+            editor
+                .update(&mut cx, |editor, cx| {
+                    spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
+                })
+                .ok();
+        })
+        .detach();
+    }
+
+    fn new_allowed_hint_kinds_splice(
+        &self,
+        multi_buffer: &ModelHandle<MultiBuffer>,
+        visible_hints: &[Inlay],
+        new_kinds: &HashSet<Option<InlayHintKind>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<InlaySplice> {
+        let old_kinds = &self.allowed_hint_kinds;
+        if new_kinds == old_kinds {
+            return None;
+        }
+
+        let mut to_remove = Vec::new();
+        let mut to_insert = Vec::new();
+        let mut shown_hints_to_remove = visible_hints.iter().fold(
+            HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
+            |mut current_hints, inlay| {
+                current_hints
+                    .entry(inlay.position.excerpt_id)
+                    .or_default()
+                    .push((inlay.position, inlay.id));
+                current_hints
+            },
+        );
+
+        let multi_buffer = multi_buffer.read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+
+        for (excerpt_id, excerpt_cached_hints) in &self.hints {
+            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();
+            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
+                let Some(buffer) = shown_anchor
+                    .buffer_id
+                    .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
+                let buffer_snapshot = buffer.read(cx).snapshot();
+                loop {
+                    match excerpt_cache.peek() {
+                        Some((cached_hint_id, cached_hint)) => {
+                            if cached_hint_id == shown_hint_id {
+                                excerpt_cache.next();
+                                return !new_kinds.contains(&cached_hint.kind);
+                            }
+
+                            match cached_hint
+                                .position
+                                .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
+                            {
+                                cmp::Ordering::Less | cmp::Ordering::Equal => {
+                                    if !old_kinds.contains(&cached_hint.kind)
+                                        && new_kinds.contains(&cached_hint.kind)
+                                    {
+                                        to_insert.push(Inlay::hint(
+                                            cached_hint_id.id(),
+                                            multi_buffer_snapshot.anchor_in_excerpt(
+                                                *excerpt_id,
+                                                cached_hint.position,
+                                            ),
+                                            &cached_hint,
+                                        ));
+                                    }
+                                    excerpt_cache.next();
+                                }
+                                cmp::Ordering::Greater => return true,
+                            }
+                        }
+                        None => return true,
+                    }
+                }
+            });
+
+            for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
+                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(
+                        cached_hint_id.id(),
+                        multi_buffer_snapshot
+                            .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
+                        &maybe_missed_cached_hint,
+                    ));
+                }
+            }
+        }
+
+        to_remove.extend(
+            shown_hints_to_remove
+                .into_values()
+                .flatten()
+                .map(|(_, hint_id)| hint_id),
+        );
+        if to_remove.is_empty() && to_insert.is_empty() {
+            None
+        } else {
+            Some(InlaySplice {
+                to_remove,
+                to_insert,
+            })
+        }
+    }
+
+    fn clear(&mut self) {
+        self.version += 1;
+        self.update_tasks.clear();
+        self.hints.clear();
+    }
+}
+
+fn spawn_new_update_tasks(
+    editor: &mut Editor,
+    excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+    invalidate: InvalidationStrategy,
+    update_cache_version: usize,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+    for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
+        excerpts_to_query
+    {
+        if excerpt_visible_range.is_empty() {
+            continue;
+        }
+        let buffer = buffer_handle.read(cx);
+        let buffer_snapshot = buffer.snapshot();
+        if buffer_snapshot
+            .version()
+            .changed_since(&new_task_buffer_version)
+        {
+            continue;
+        }
+
+        let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+            let cached_excerpt_hints = cached_excerpt_hints.read();
+            let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+            if cached_excerpt_hints.version > update_cache_version
+                || cached_buffer_version.changed_since(&new_task_buffer_version)
+            {
+                continue;
+            }
+            if !new_task_buffer_version.changed_since(&cached_buffer_version)
+                && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
+            {
+                continue;
+            }
+        };
+
+        let buffer_id = buffer.remote_id();
+        let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
+        let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
+
+        let (multi_buffer_snapshot, full_excerpt_range) =
+            editor.buffer.update(cx, |multi_buffer, cx| {
+                let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+                (
+                    multi_buffer_snapshot,
+                    multi_buffer
+                        .excerpts_for_buffer(&buffer_handle, cx)
+                        .into_iter()
+                        .find(|(id, _)| id == &excerpt_id)
+                        .map(|(_, range)| range.context),
+                )
+            });
+
+        if let Some(full_excerpt_range) = full_excerpt_range {
+            let query = ExcerptQuery {
+                buffer_id,
+                excerpt_id,
+                dimensions: ExcerptDimensions {
+                    excerpt_range_start: full_excerpt_range.start,
+                    excerpt_range_end: full_excerpt_range.end,
+                    excerpt_visible_range_start,
+                    excerpt_visible_range_end,
+                },
+                cache_version: update_cache_version,
+                invalidate,
+            };
+
+            let new_update_task = |is_refresh_after_regular_task| {
+                new_update_task(
+                    query,
+                    multi_buffer_snapshot,
+                    buffer_snapshot,
+                    Arc::clone(&visible_hints),
+                    cached_excerpt_hints,
+                    is_refresh_after_regular_task,
+                    cx,
+                )
+            };
+            match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+                hash_map::Entry::Occupied(mut o) => {
+                    let update_task = o.get_mut();
+                    match (update_task.invalidate, invalidate) {
+                        (_, InvalidationStrategy::None) => {}
+                        (
+                            InvalidationStrategy::BufferEdited,
+                            InvalidationStrategy::RefreshRequested,
+                        ) if !update_task.task.is_running_rx.is_closed() => {
+                            update_task.pending_refresh = Some(query);
+                        }
+                        _ => {
+                            o.insert(UpdateTask {
+                                invalidate,
+                                cache_version: query.cache_version,
+                                task: new_update_task(false),
+                                pending_refresh: None,
+                            });
+                        }
+                    }
+                }
+                hash_map::Entry::Vacant(v) => {
+                    v.insert(UpdateTask {
+                        invalidate,
+                        cache_version: query.cache_version,
+                        task: new_update_task(false),
+                        pending_refresh: None,
+                    });
+                }
+            }
+        }
+    }
+}
+
+fn new_update_task(
+    query: ExcerptQuery,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    buffer_snapshot: BufferSnapshot,
+    visible_hints: Arc<Vec<Inlay>>,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    is_refresh_after_regular_task: bool,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) -> RunningTask {
+    let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
+    let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
+    let _task = cx.spawn(|editor, mut cx| async move {
+        let _is_running_tx = is_running_tx;
+        let create_update_task = |range| {
+            fetch_and_update_hints(
+                editor.clone(),
+                multi_buffer_snapshot.clone(),
+                buffer_snapshot.clone(),
+                Arc::clone(&visible_hints),
+                cached_excerpt_hints.as_ref().map(Arc::clone),
+                query,
+                range,
+                cx.clone(),
+            )
+        };
+
+        if is_refresh_after_regular_task {
+            let visible_range_has_updates =
+                match create_update_task(hints_fetch_ranges.visible_range).await {
+                    Ok(updated) => updated,
+                    Err(e) => {
+                        error!("inlay hint visible range update task failed: {e:#}");
+                        return;
+                    }
+                };
+
+            if visible_range_has_updates {
+                let other_update_results = futures::future::join_all(
+                    hints_fetch_ranges
+                        .other_ranges
+                        .into_iter()
+                        .map(create_update_task),
+                )
+                .await;
+
+                for result in other_update_results {
+                    if let Err(e) = result {
+                        error!("inlay hint update task failed: {e:#}");
+                    }
+                }
+            }
+        } else {
+            let task_update_results = futures::future::join_all(
+                std::iter::once(hints_fetch_ranges.visible_range)
+                    .chain(hints_fetch_ranges.other_ranges.into_iter())
+                    .map(create_update_task),
+            )
+            .await;
+
+            for result in task_update_results {
+                if let Err(e) = result {
+                    error!("inlay hint update task failed: {e:#}");
+                }
+            }
+        }
+
+        editor
+            .update(&mut cx, |editor, cx| {
+                let pending_refresh_query = editor
+                    .inlay_hint_cache
+                    .update_tasks
+                    .get_mut(&query.excerpt_id)
+                    .and_then(|task| task.pending_refresh.take());
+
+                if let Some(pending_refresh_query) = pending_refresh_query {
+                    let refresh_multi_buffer = editor.buffer().read(cx);
+                    let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
+                    let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+                    let refresh_cached_excerpt_hints = editor
+                        .inlay_hint_cache
+                        .hints
+                        .get(&pending_refresh_query.excerpt_id)
+                        .map(Arc::clone);
+                    if let Some(buffer) =
+                        refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
+                    {
+                        drop(refresh_multi_buffer);
+                        editor.inlay_hint_cache.update_tasks.insert(
+                            pending_refresh_query.excerpt_id,
+                            UpdateTask {
+                                invalidate: InvalidationStrategy::RefreshRequested,
+                                cache_version: editor.inlay_hint_cache.version,
+                                task: new_update_task(
+                                    pending_refresh_query,
+                                    refresh_multi_buffer_snapshot,
+                                    buffer.read(cx).snapshot(),
+                                    refresh_visible_hints,
+                                    refresh_cached_excerpt_hints,
+                                    true,
+                                    cx,
+                                ),
+                                pending_refresh: None,
+                            },
+                        );
+                    }
+                }
+            })
+            .ok();
+    });
+
+    RunningTask {
+        _task,
+        is_running_rx,
+    }
+}
+
+async fn fetch_and_update_hints(
+    editor: gpui::WeakViewHandle<Editor>,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    buffer_snapshot: BufferSnapshot,
+    visible_hints: Arc<Vec<Inlay>>,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    query: ExcerptQuery,
+    fetch_range: Range<language::Anchor>,
+    mut cx: gpui::AsyncAppContext,
+) -> anyhow::Result<bool> {
+    let inlay_hints_fetch_task = editor
+        .update(&mut cx, |editor, cx| {
+            editor
+                .buffer()
+                .read(cx)
+                .buffer(query.buffer_id)
+                .and_then(|buffer| {
+                    let project = editor.project.as_ref()?;
+                    Some(project.update(cx, |project, cx| {
+                        project.inlay_hints(buffer, fetch_range.clone(), cx)
+                    }))
+                })
+        })
+        .ok()
+        .flatten();
+    let mut update_happened = false;
+    let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
+    let new_hints = inlay_hints_fetch_task
+        .await
+        .context("inlay hint fetch task")?;
+    let background_task_buffer_snapshot = buffer_snapshot.clone();
+    let backround_fetch_range = fetch_range.clone();
+    let new_update = cx
+        .background()
+        .spawn(async move {
+            calculate_hint_updates(
+                query,
+                backround_fetch_range,
+                new_hints,
+                &background_task_buffer_snapshot,
+                cached_excerpt_hints,
+                &visible_hints,
+            )
+        })
+        .await;
+
+    editor
+        .update(&mut cx, |editor, cx| {
+            if let Some(new_update) = new_update {
+                update_happened = !new_update.add_to_cache.is_empty()
+                    || !new_update.remove_from_cache.is_empty()
+                    || !new_update.remove_from_visible.is_empty();
+
+                let cached_excerpt_hints = editor
+                    .inlay_hint_cache
+                    .hints
+                    .entry(new_update.excerpt_id)
+                    .or_insert_with(|| {
+                        Arc::new(RwLock::new(CachedExcerptHints {
+                            version: query.cache_version,
+                            buffer_version: buffer_snapshot.version().clone(),
+                            buffer_id: query.buffer_id,
+                            hints: Vec::new(),
+                        }))
+                    });
+                let mut cached_excerpt_hints = cached_excerpt_hints.write();
+                match query.cache_version.cmp(&cached_excerpt_hints.version) {
+                    cmp::Ordering::Less => return,
+                    cmp::Ordering::Greater | cmp::Ordering::Equal => {
+                        cached_excerpt_hints.version = query.cache_version;
+                    }
+                }
+                cached_excerpt_hints
+                    .hints
+                    .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+                cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+                editor.inlay_hint_cache.version += 1;
+
+                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 new_hint_position = multi_buffer_snapshot
+                        .anchor_in_excerpt(query.excerpt_id, new_hint.position);
+                    let new_inlay_id = post_inc(&mut editor.next_inlay_id);
+                    if editor
+                        .inlay_hint_cache
+                        .allowed_hint_kinds
+                        .contains(&new_hint.kind)
+                    {
+                        splice.to_insert.push(Inlay::hint(
+                            new_inlay_id,
+                            new_hint_position,
+                            &new_hint,
+                        ));
+                    }
+
+                    cached_excerpt_hints
+                        .hints
+                        .push((InlayId::Hint(new_inlay_id), new_hint));
+                }
+
+                cached_excerpt_hints
+                    .hints
+                    .sort_by(|(_, hint_a), (_, hint_b)| {
+                        hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
+                    });
+                drop(cached_excerpt_hints);
+
+                if query.invalidate.should_invalidate() {
+                    let mut outdated_excerpt_caches = HashSet::default();
+                    for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
+                        let excerpt_hints = excerpt_hints.read();
+                        if excerpt_hints.buffer_id == query.buffer_id
+                            && excerpt_id != &query.excerpt_id
+                            && buffer_snapshot
+                                .version()
+                                .changed_since(&excerpt_hints.buffer_version)
+                        {
+                            outdated_excerpt_caches.insert(*excerpt_id);
+                            splice
+                                .to_remove
+                                .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
+                        }
+                    }
+                    editor
+                        .inlay_hint_cache
+                        .hints
+                        .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+                }
+
+                let InlaySplice {
+                    to_remove,
+                    to_insert,
+                } = splice;
+                if !to_remove.is_empty() || !to_insert.is_empty() {
+                    editor.splice_inlay_hints(to_remove, to_insert, cx)
+                }
+            }
+        })
+        .ok();
+
+    Ok(update_happened)
+}
+
+fn calculate_hint_updates(
+    query: ExcerptQuery,
+    fetch_range: Range<language::Anchor>,
+    new_excerpt_hints: Vec<InlayHint>,
+    buffer_snapshot: &BufferSnapshot,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    visible_hints: &[Inlay],
+) -> Option<ExcerptHintsUpdate> {
+    let mut add_to_cache: HashSet<InlayHint> = HashSet::default();
+    let mut excerpt_hints_to_persist = HashMap::default();
+    for new_hint in new_excerpt_hints {
+        if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
+            continue;
+        }
+        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)
+                }) {
+                    Ok(ix) => {
+                        let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
+                        if cached_hint == &new_hint {
+                            excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+                            false
+                        } else {
+                            true
+                        }
+                    }
+                    Err(_) => true,
+                }
+            }
+            None => true,
+        };
+        if missing_from_cache {
+            add_to_cache.insert(new_hint);
+        }
+    }
+
+    let mut remove_from_visible = Vec::new();
+    let mut remove_from_cache = HashSet::default();
+    if query.invalidate.should_invalidate() {
+        remove_from_visible.extend(
+            visible_hints
+                .iter()
+                .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
+                .filter(|hint| {
+                    contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
+                })
+                .filter(|hint| {
+                    fetch_range
+                        .start
+                        .cmp(&hint.position.text_anchor, buffer_snapshot)
+                        .is_le()
+                        && fetch_range
+                            .end
+                            .cmp(&hint.position.text_anchor, buffer_snapshot)
+                            .is_ge()
+                })
+                .map(|inlay_hint| inlay_hint.id)
+                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
+        );
+
+        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+            let cached_excerpt_hints = cached_excerpt_hints.read();
+            remove_from_cache.extend(
+                cached_excerpt_hints
+                    .hints
+                    .iter()
+                    .filter(|(cached_inlay_id, _)| {
+                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+                    })
+                    .filter(|(_, cached_hint)| {
+                        fetch_range
+                            .start
+                            .cmp(&cached_hint.position, buffer_snapshot)
+                            .is_le()
+                            && fetch_range
+                                .end
+                                .cmp(&cached_hint.position, buffer_snapshot)
+                                .is_ge()
+                    })
+                    .map(|(cached_inlay_id, _)| *cached_inlay_id),
+            );
+        }
+    }
+
+    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
+        None
+    } else {
+        Some(ExcerptHintsUpdate {
+            excerpt_id: query.excerpt_id,
+            remove_from_visible,
+            remove_from_cache,
+            add_to_cache,
+        })
+    }
+}
+
+fn contains_position(
+    range: &Range<language::Anchor>,
+    position: language::Anchor,
+    buffer_snapshot: &BufferSnapshot,
+) -> bool {
+    range.start.cmp(&position, buffer_snapshot).is_le()
+        && range.end.cmp(&position, buffer_snapshot).is_ge()
+}
+
+#[cfg(test)]
+mod tests {
+    use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+
+    use crate::{
+        scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+        serde_json::json,
+        ExcerptRange, InlayHintSettings,
+    };
+    use futures::StreamExt;
+    use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+    use language::{
+        language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+    };
+    use lsp::FakeLanguageServer;
+    use parking_lot::Mutex;
+    use project::{FakeFs, Project};
+    use settings::SettingsStore;
+    use text::Point;
+    use workspace::Workspace;
+
+    use crate::editor_tests::update_test_settings;
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    let current_call_id =
+                        Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
+                    for _ in 0..2 {
+                        let mut i = current_call_id;
+                        loop {
+                            new_hints.push(lsp::InlayHint {
+                                position: lsp::Position::new(0, i),
+                                label: lsp::InlayHintLabel::String(i.to_string()),
+                                kind: None,
+                                text_edits: None,
+                                tooltip: None,
+                                padding_left: None,
+                                padding_right: None,
+                                data: None,
+                            });
+                            if i == 0 {
+                                break;
+                            }
+                            i -= 1;
+                        }
+                    }
+
+                    Ok(Some(new_hints))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some change", cx);
+            edits_made += 1;
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string(), "1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get new hints after an edit"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        edits_made += 1;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get new hints after hint refresh/ request"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+                    "/a",
+                    json!({
+                        "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                        "other.md": "Test md file with some text",
+                    }),
+                )
+                .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let mut rs_fake_servers = None;
+        let mut md_fake_servers = None;
+        for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
+            let mut language = Language::new(
+                LanguageConfig {
+                    name: name.into(),
+                    path_suffixes: vec![path_suffix.to_string()],
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::language()),
+            );
+            let fake_servers = language
+                .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                    name,
+                    capabilities: lsp::ServerCapabilities {
+                        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                        ..Default::default()
+                    },
+                    ..Default::default()
+                }))
+                .await;
+            match name {
+                "Rust" => rs_fake_servers = Some(fake_servers),
+                "Markdown" => md_fake_servers = Some(fake_servers),
+                _ => unreachable!(),
+            }
+            project.update(cx, |project, _| {
+                project.languages().add(Arc::new(language));
+            });
+        }
+
+        let _rs_buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
+        let rs_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
+        rs_fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        rs_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, 1,
+                "Rust editor update the cache version after every cache/view change"
+            );
+        });
+
+        cx.foreground().run_until_parked();
+        let _md_buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/other.md", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
+        let md_editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "other.md"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let md_lsp_request_count = Arc::new(AtomicU32::new(0));
+        md_fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/other.md").unwrap(),
+                    );
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        md_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Markdown editor should have a separate verison, repeating Rust editor rules"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 1);
+        });
+
+        rs_editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some rs change", cx);
+        });
+        cx.foreground().run_until_parked();
+        rs_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Rust inlay cache should change after the edit"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Every time hint cache changes, cache version should be incremented"
+            );
+        });
+        md_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Markdown editor should not be affected by Rust editor changes"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 1);
+        });
+
+        md_editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some md change", cx);
+        });
+        cx.foreground().run_until_parked();
+        md_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Rust editor should not be affected by Markdown editor changes"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 2);
+        });
+        rs_editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Markdown editor should also change independently"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 2);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 1),
+                            label: lsp::InlayHintLabel::String("type hint".to_string()),
+                            kind: Some(lsp::InlayHintKind::TYPE),
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 2),
+                            label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+                            kind: Some(lsp::InlayHintKind::PARAMETER),
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 3),
+                            label: lsp::InlayHintLabel::String("other hint".to_string()),
+                            kind: None,
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                    ]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                1,
+                "Should query new hints once"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(
+                vec!["other hint".to_string(), "type hint".to_string()],
+                visible_hint_labels(editor, cx)
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should load new hints twice"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Cached hints should not change due to allowed hint kinds settings update"
+            );
+            assert_eq!(
+                vec!["other hint".to_string(), "type hint".to_string()],
+                visible_hint_labels(editor, cx)
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Should not update cache version due to new loaded hints being the same"
+            );
+        });
+
+        for (new_allowed_hint_kinds, expected_visible_hints) in [
+            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Type)]),
+                vec!["type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
+                vec!["parameter hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
+                vec!["other hint".to_string(), "type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
+                vec!["other hint".to_string(), "parameter hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
+                vec!["parameter hint".to_string(), "type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([
+                    None,
+                    Some(InlayHintKind::Type),
+                    Some(InlayHintKind::Parameter),
+                ]),
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+            ),
+        ] {
+            edits_made += 1;
+            update_test_settings(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                    show_parameter_hints: new_allowed_hint_kinds
+                        .contains(&Some(InlayHintKind::Parameter)),
+                    show_other_hints: new_allowed_hint_kinds.contains(&None),
+                })
+            });
+            cx.foreground().run_until_parked();
+            editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    lsp_request_count.load(Ordering::Relaxed),
+                    2,
+                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    vec![
+                        "other hint".to_string(),
+                        "parameter hint".to_string(),
+                        "type hint".to_string(),
+                    ],
+                    cached_hint_labels(editor),
+                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    expected_visible_hints,
+                    visible_hint_labels(editor, cx),
+                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
+                );
+            });
+        }
+
+        edits_made += 1;
+        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
+        update_test_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: false,
+                show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: another_allowed_hint_kinds
+                    .contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: another_allowed_hint_kinds.contains(&None),
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should not load new hints when hints got disabled"
+            );
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear the cache when hints got disabled"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "Should clear visible hints when hints got disabled"
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
+                "Should update its allowed hint kinds even when hints got disabled"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor should update the cache version after hints got disabled"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should not load new hints when they got disabled"
+            );
+            assert!(cached_hint_labels(editor).is_empty());
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor should not update the cache version after /refresh query without updates"
+            );
+        });
+
+        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
+        edits_made += 1;
+        update_test_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: final_allowed_hint_kinds
+                    .contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: final_allowed_hint_kinds.contains(&None),
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                3,
+                "Should query for new hints when they got reenabled"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Should get its cached hints fully repopulated after the hints got reenabled"
+            );
+            assert_eq!(
+                vec!["parameter hint".to_string()],
+                visible_hint_labels(editor, cx),
+                "Should get its visible hints repopulated and filtered after the h"
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
+                "Cache should update editor settings when hints got reenabled"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Cache should update its version after hints got reenabled"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                4,
+                "Should query for new hints again"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+            );
+            assert_eq!(
+                vec!["parameter hint".to_string()],
+                visible_hint_labels(editor, cx),
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, edits_made);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let fake_server = Arc::new(fake_server);
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+
+        let mut expected_changes = Vec::new();
+        for change_after_opening in [
+            "initial change #1",
+            "initial change #2",
+            "initial change #3",
+        ] {
+            editor.update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input(change_after_opening, cx);
+            });
+            expected_changes.push(change_after_opening);
+        }
+
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+            );
+            let expected_hints = vec!["2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 1,
+                "Only one update should be registered in the cache after all cancellations"
+            );
+        });
+
+        let mut edits = Vec::new();
+        for async_later_change in [
+            "another change #1",
+            "another change #2",
+            "another change #3",
+        ] {
+            expected_changes.push(async_later_change);
+            let task_editor = editor.clone();
+            let mut task_cx = cx.clone();
+            edits.push(cx.foreground().spawn(async move {
+                task_editor.update(&mut task_cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                    editor.handle_input(async_later_change, cx);
+                });
+            }));
+        }
+        let _ = futures::future::join_all(edits).await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::SeqCst),
+                3,
+                "Should query new hints one more time, for the last edit only"
+            );
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Should update the cache version once more, for the new change"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+
+                    task_lsp_request_ranges.lock().push(params.range);
+                    let query_start = params.range.start;
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: query_start,
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|range| range.start);
+            assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+            assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
+            assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
+
+            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
+                "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+            let expected_layers = vec!["1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should have hints from both LSP requests made for a big file"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Both LSP queries should've bumped the cache version"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
+            editor.handle_input("++++more text++++", cx);
+        });
+
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|range| range.start);
+            assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
+            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+            assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
+            assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
+            assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
+            assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
+            assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
+
+            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
+                "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
+            let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "Should have hints from the new LSP response after edit");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
+        });
+    }
+
+    #[gpui::test]
+    async fn test_multiple_excerpts_large_multibuffer(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let language = Arc::new(language);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::clone(&language))
+        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "main.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "other.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(2, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(11, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 0)..Point::new(33, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 0)..Point::new(55, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 0)..Point::new(66, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 0)..Point::new(77, 0),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 1)..Point::new(2, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 1)..Point::new(11, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 1)..Point::new(33, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 1)..Point::new(55, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 1)..Point::new(66, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 1)..Point::new(77, 1),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        deterministic.run_until_parked();
+        cx.foreground().run_until_parked();
+        let (_, editor) =
+            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+        let editor_edited = Arc::new(AtomicBool::new(false));
+        let fake_server = fake_servers.next().await.unwrap();
+        let closure_editor_edited = Arc::clone(&editor_edited);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_editor_edited = Arc::clone(&closure_editor_edited);
+                async move {
+                    let hint_text = if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/main.rs").unwrap()
+                    {
+                        "main hint"
+                    } else if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/other.rs").unwrap()
+                    {
+                        "other hint"
+                    } else {
+                        panic!("unexpected uri: {:?}", params.text_document.uri);
+                    };
+
+                    let positions = [
+                        lsp::Position::new(0, 2),
+                        lsp::Position::new(4, 2),
+                        lsp::Position::new(22, 2),
+                        lsp::Position::new(44, 2),
+                        lsp::Position::new(56, 2),
+                        lsp::Position::new(67, 2),
+                    ];
+                    let out_of_range_hint = lsp::InlayHint {
+                        position: lsp::Position::new(
+                            params.range.start.line + 99,
+                            params.range.start.character + 99,
+                        ),
+                        label: lsp::InlayHintLabel::String(
+                            "out of excerpt range, should be ignored".to_string(),
+                        ),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    };
+
+                    let edited = task_editor_edited.load(Ordering::Acquire);
+                    Ok(Some(
+                        std::iter::once(out_of_range_hint)
+                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
+                                lsp::InlayHint {
+                                    position,
+                                    label: lsp::InlayHintLabel::String(format!(
+                                        "{hint_text}{} #{i}",
+                                        if edited { "(edited)" } else { "" },
+                                    )),
+                                    kind: None,
+                                    text_edits: None,
+                                    tooltip: None,
+                                    padding_left: None,
+                                    padding_right: None,
+                                    data: None,
+                                }
+                            }))
+                            .collect(),
+                    ))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+            ];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+            });
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
+            });
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 9);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 12);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
+        });
+
+        editor_edited.store(true, Ordering::Release);
+        editor.update(cx, |editor, cx| {
+            editor.handle_input("++++more text++++", cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint(edited) #0".to_string(),
+                "main hint(edited) #1".to_string(),
+                "main hint(edited) #2".to_string(),
+                "main hint(edited) #3".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
+unedited (2nd) buffer should have the same hint");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 16);
+        });
+    }
+
+    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        cx.foreground().forbid_parking();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            client::init_settings(cx);
+            language::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
+            crate::init(cx);
+        });
+
+        update_test_settings(cx, f);
+    }
+
+    async fn prepare_test_objects(
+        cx: &mut TestAppContext,
+    ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        ("/a/main.rs", editor, fake_server)
+    }
+
+    fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+        let mut labels = Vec::new();
+        for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+            let excerpt_hints = excerpt_hints.read();
+            for (_, inlay) in excerpt_hints.hints.iter() {
+                match &inlay.label {
+                    project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+                    _ => unreachable!(),
+                }
+            }
+        }
+
+        labels.sort();
+        labels
+    }
+
+    fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+        let mut hints = editor
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .map(|hint| hint.text.to_string())
+            .collect::<Vec<_>>();
+        hints.sort();
+        hints
+    }
+}

crates/editor/src/multi_buffer.rs πŸ”—

@@ -1010,7 +1010,7 @@ impl MultiBuffer {
 
         let suffix = cursor.suffix(&());
         let changed_trailing_excerpt = suffix.is_empty();
-        new_excerpts.push_tree(suffix, &());
+        new_excerpts.append(suffix, &());
         drop(cursor);
         snapshot.excerpts = new_excerpts;
         snapshot.excerpt_ids = new_excerpt_ids;
@@ -1193,7 +1193,7 @@ impl MultiBuffer {
         while let Some(excerpt_id) = excerpt_ids.next() {
             // Seek to the next excerpt to remove, preserving any preceding excerpts.
             let locator = snapshot.excerpt_locator_for_id(excerpt_id);
-            new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
+            new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
 
             if let Some(mut excerpt) = cursor.item() {
                 if excerpt.id != excerpt_id {
@@ -1245,7 +1245,7 @@ impl MultiBuffer {
         }
         let suffix = cursor.suffix(&());
         let changed_trailing_excerpt = suffix.is_empty();
-        new_excerpts.push_tree(suffix, &());
+        new_excerpts.append(suffix, &());
         drop(cursor);
         snapshot.excerpts = new_excerpts;
 
@@ -1509,7 +1509,7 @@ impl MultiBuffer {
         let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
 
         for (locator, buffer, buffer_edited) in excerpts_to_edit {
-            new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
+            new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
             let old_excerpt = cursor.item().unwrap();
             let buffer = buffer.read(cx);
             let buffer_id = buffer.remote_id();
@@ -1549,7 +1549,7 @@ impl MultiBuffer {
             new_excerpts.push(new_excerpt, &());
             cursor.next(&());
         }
-        new_excerpts.push_tree(cursor.suffix(&()), &());
+        new_excerpts.append(cursor.suffix(&()), &());
 
         drop(cursor);
         snapshot.excerpts = new_excerpts;

crates/editor/src/multi_buffer/anchor.rs πŸ”—

@@ -49,6 +49,10 @@ impl Anchor {
         }
     }
 
+    pub fn bias(&self) -> Bias {
+        self.text_anchor.bias
+    }
+
     pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         if self.text_anchor.bias != Bias::Left {
             if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
@@ -81,6 +85,19 @@ impl Anchor {
     {
         snapshot.summary_for_anchor(self)
     }
+
+    pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
+        if *self == Anchor::min() || *self == Anchor::max() {
+            true
+        } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
+            excerpt.contains(self)
+                && (self.text_anchor == excerpt.range.context.start
+                    || self.text_anchor == excerpt.range.context.end
+                    || self.text_anchor.is_valid(&excerpt.buffer))
+        } else {
+            false
+        }
+    }
 }
 
 impl ToOffset for Anchor {

crates/editor/src/scroll.rs πŸ”—

@@ -13,13 +13,14 @@ use gpui::{
 };
 use language::{Bias, Point};
 use util::ResultExt;
-use workspace::WorkspaceId;
+use workspace::{item::Item, WorkspaceId};
 
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
     persistence::DB,
-    Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
+    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
+    ToPoint,
 };
 
 use self::{
@@ -293,8 +294,19 @@ impl Editor {
         self.scroll_manager.visible_line_count
     }
 
-    pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
-        self.scroll_manager.visible_line_count = Some(lines)
+    pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
+        let opened_first_time = self.scroll_manager.visible_line_count.is_none();
+        self.scroll_manager.visible_line_count = Some(lines);
+        if opened_first_time {
+            cx.spawn(|editor, mut cx| async move {
+                editor
+                    .update(&mut cx, |editor, cx| {
+                        editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
+                    })
+                    .ok()
+            })
+            .detach()
+        }
     }
 
     pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
@@ -320,6 +332,10 @@ impl Editor {
             workspace_id,
             cx,
         );
+
+        if !self.is_singleton(cx) {
+            self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
+        }
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -368,7 +384,7 @@ impl Editor {
         }
 
         let cur_position = self.scroll_position(cx);
-        let new_pos = cur_position + vec2f(0., amount.lines(self) - 1.);
+        let new_pos = cur_position + vec2f(0., amount.lines(self));
         self.set_scroll_position(new_pos, cx);
     }
 

crates/editor/src/scroll/actions.rs πŸ”—

@@ -27,22 +27,22 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::scroll_cursor_center);
     cx.add_action(Editor::scroll_cursor_bottom);
     cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
-        this.scroll_screen(&ScrollAmount::LineDown, cx)
+        this.scroll_screen(&ScrollAmount::Line(1.), cx)
     });
     cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
-        this.scroll_screen(&ScrollAmount::LineUp, cx)
+        this.scroll_screen(&ScrollAmount::Line(-1.), cx)
     });
     cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
-        this.scroll_screen(&ScrollAmount::HalfPageDown, cx)
+        this.scroll_screen(&ScrollAmount::Page(0.5), cx)
     });
     cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
-        this.scroll_screen(&ScrollAmount::HalfPageUp, cx)
+        this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
     });
     cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
-        this.scroll_screen(&ScrollAmount::PageDown, cx)
+        this.scroll_screen(&ScrollAmount::Page(1.), cx)
     });
     cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
-        this.scroll_screen(&ScrollAmount::PageUp, cx)
+        this.scroll_screen(&ScrollAmount::Page(-1.), cx)
     });
 }
 

crates/editor/src/scroll/scroll_amount.rs πŸ”—

@@ -6,12 +6,10 @@ use crate::Editor;
 
 #[derive(Clone, PartialEq, Deserialize)]
 pub enum ScrollAmount {
-    LineUp,
-    LineDown,
-    HalfPageUp,
-    HalfPageDown,
-    PageUp,
-    PageDown,
+    // Scroll N lines (positive is towards the end of the document)
+    Line(f32),
+    // Scroll N pages (positive is towards the end of the document)
+    Page(f32),
 }
 
 impl ScrollAmount {
@@ -24,10 +22,10 @@ impl ScrollAmount {
             let context_menu = editor.context_menu.as_mut()?;
 
             match self {
-                Self::LineDown | Self::HalfPageDown => context_menu.select_next(cx),
-                Self::LineUp | Self::HalfPageUp => context_menu.select_prev(cx),
-                Self::PageDown => context_menu.select_last(cx),
-                Self::PageUp => context_menu.select_first(cx),
+                Self::Line(c) if *c > 0. => context_menu.select_next(cx),
+                Self::Line(_) => context_menu.select_prev(cx),
+                Self::Page(c) if *c > 0. => context_menu.select_last(cx),
+                Self::Page(_) => context_menu.select_first(cx),
             }
             .then_some(())
         })
@@ -36,13 +34,13 @@ impl ScrollAmount {
 
     pub fn lines(&self, editor: &mut Editor) -> f32 {
         match self {
-            Self::LineDown => 1.,
-            Self::LineUp => -1.,
-            Self::HalfPageDown => editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
-            Self::HalfPageUp => -editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
-            // Minus 1. here so that there is a pivot line that stays on the screen
-            Self::PageDown => editor.visible_line_count().unwrap_or(1.) - 1.,
-            Self::PageUp => -editor.visible_line_count().unwrap_or(1.) - 1.,
+            Self::Line(count) => *count,
+            Self::Page(count) => editor
+                .visible_line_count()
+                // subtract one to leave an anchor line
+                // round towards zero (so page-up and page-down are symmetric)
+                .map(|l| ((l - 1.) * count).trunc())
+                .unwrap_or(0.),
         }
     }
 }

crates/feedback/src/deploy_feedback_button.rs πŸ”—

@@ -41,7 +41,8 @@ impl View for DeployFeedbackButton {
                         .status_bar
                         .panel_buttons
                         .button
-                        .style_for(state, active);
+                        .in_state(active)
+                        .style_for(state);
 
                     Svg::new("icons/feedback_16.svg")
                         .with_color(style.icon_color)

crates/feedback/src/submit_feedback_button.rs πŸ”—

@@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton {
         let theme = theme::current(cx).clone();
         enum SubmitFeedbackButton {}
         MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
-            let style = theme.feedback.submit_button.style_for(state, false);
+            let style = theme.feedback.submit_button.style_for(state);
             Label::new("Submit as Markdown", style.text.clone())
                 .contained()
                 .with_style(style.container)

crates/file_finder/src/file_finder.rs πŸ”—

@@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate {
             .get(ix)
             .expect("Invalid matches state: no element for index {ix}");
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let (file_name, file_name_positions, full_path, full_path_positions) =
             self.labels_for_match(path_match, cx, ix);
         Flex::column()

crates/fs/Cargo.toml πŸ”—

@@ -31,6 +31,10 @@ serde_derive.workspace = true
 serde_json.workspace = true
 log.workspace = true
 libc = "0.2"
+time.workspace = true
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
 
 [features]
 test-support = []

crates/fs/src/fs.rs πŸ”—

@@ -108,6 +108,7 @@ pub trait Fs: Send + Sync {
     async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
     async fn is_file(&self, path: &Path) -> bool;
     async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
+    async fn read_link(&self, path: &Path) -> Result<PathBuf>;
     async fn read_dir(
         &self,
         path: &Path,
@@ -278,6 +279,9 @@ impl Fs for RealFs {
 
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
         let buffer_size = text.summary().len.min(10 * 1024);
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
         let file = smol::fs::File::create(path).await?;
         let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
         for chunk in chunks(text, line_ending) {
@@ -323,6 +327,11 @@ impl Fs for RealFs {
         }))
     }
 
+    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
+        let path = smol::fs::read_link(path).await?;
+        Ok(path)
+    }
+
     async fn read_dir(
         &self,
         path: &Path,
@@ -382,6 +391,8 @@ struct FakeFsState {
     event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
     events_paused: bool,
     buffered_events: Vec<fsevent::Event>,
+    metadata_call_count: usize,
+    read_dir_call_count: usize,
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -407,46 +418,51 @@ enum FakeFsEntry {
 impl FakeFsState {
     fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
         Ok(self
-            .try_read_path(target)
+            .try_read_path(target, true)
             .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
             .0)
     }
 
-    fn try_read_path<'a>(&'a self, target: &Path) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
+    fn try_read_path<'a>(
+        &'a self,
+        target: &Path,
+        follow_symlink: bool,
+    ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
         let mut path = target.to_path_buf();
-        let mut real_path = PathBuf::new();
+        let mut canonical_path = PathBuf::new();
         let mut entry_stack = Vec::new();
         'outer: loop {
-            let mut path_components = path.components().collect::<collections::VecDeque<_>>();
-            while let Some(component) = path_components.pop_front() {
+            let mut path_components = path.components().peekable();
+            while let Some(component) = path_components.next() {
                 match component {
                     Component::Prefix(_) => panic!("prefix paths aren't supported"),
                     Component::RootDir => {
                         entry_stack.clear();
                         entry_stack.push(self.root.clone());
-                        real_path.clear();
-                        real_path.push("/");
+                        canonical_path.clear();
+                        canonical_path.push("/");
                     }
                     Component::CurDir => {}
                     Component::ParentDir => {
                         entry_stack.pop()?;
-                        real_path.pop();
+                        canonical_path.pop();
                     }
                     Component::Normal(name) => {
                         let current_entry = entry_stack.last().cloned()?;
                         let current_entry = current_entry.lock();
                         if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
                             let entry = entries.get(name.to_str().unwrap()).cloned()?;
-                            let _entry = entry.lock();
-                            if let FakeFsEntry::Symlink { target, .. } = &*_entry {
-                                let mut target = target.clone();
-                                target.extend(path_components);
-                                path = target;
-                                continue 'outer;
-                            } else {
-                                entry_stack.push(entry.clone());
-                                real_path.push(name);
+                            if path_components.peek().is_some() || follow_symlink {
+                                let entry = entry.lock();
+                                if let FakeFsEntry::Symlink { target, .. } = &*entry {
+                                    let mut target = target.clone();
+                                    target.extend(path_components);
+                                    path = target;
+                                    continue 'outer;
+                                }
                             }
+                            entry_stack.push(entry.clone());
+                            canonical_path.push(name);
                         } else {
                             return None;
                         }
@@ -455,7 +471,7 @@ impl FakeFsState {
             }
             break;
         }
-        entry_stack.pop().map(|entry| (entry, real_path))
+        Some((entry_stack.pop()?, canonical_path))
     }
 
     fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
@@ -525,6 +541,8 @@ impl FakeFs {
                 event_txs: Default::default(),
                 buffered_events: Vec::new(),
                 events_paused: false,
+                read_dir_call_count: 0,
+                metadata_call_count: 0,
             }),
         })
     }
@@ -761,6 +779,16 @@ impl FakeFs {
         result
     }
 
+    /// How many `read_dir` calls have been issued.
+    pub fn read_dir_call_count(&self) -> usize {
+        self.state.lock().read_dir_call_count
+    }
+
+    /// How many `metadata` calls have been issued.
+    pub fn metadata_call_count(&self) -> usize {
+        self.state.lock().metadata_call_count
+    }
+
     async fn simulate_random_delay(&self) {
         self.executor
             .upgrade()
@@ -776,6 +804,10 @@ impl FakeFsEntry {
         matches!(self, Self::File { .. })
     }
 
+    fn is_symlink(&self) -> bool {
+        matches!(self, Self::Symlink { .. })
+    }
+
     fn file_content(&self, path: &Path) -> Result<&String> {
         if let Self::File { content, .. } = self {
             Ok(content)
@@ -1048,6 +1080,9 @@ impl Fs for FakeFs {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
         let content = chunks(text, line_ending).collect();
+        if let Some(path) = path.parent() {
+            self.create_dir(path).await?;
+        }
         self.write_file_internal(path, content)?;
         Ok(())
     }
@@ -1056,8 +1091,8 @@ impl Fs for FakeFs {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
         let state = self.state.lock();
-        if let Some((_, real_path)) = state.try_read_path(&path) {
-            Ok(real_path)
+        if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
+            Ok(canonical_path)
         } else {
             Err(anyhow!("path does not exist: {}", path.display()))
         }
@@ -1067,7 +1102,7 @@ impl Fs for FakeFs {
         let path = normalize_path(path);
         self.simulate_random_delay().await;
         let state = self.state.lock();
-        if let Some((entry, _)) = state.try_read_path(&path) {
+        if let Some((entry, _)) = state.try_read_path(&path, true) {
             entry.lock().is_file()
         } else {
             false
@@ -1077,11 +1112,19 @@ impl Fs for FakeFs {
     async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
-        let state = self.state.lock();
-        if let Some((entry, real_path)) = state.try_read_path(&path) {
-            let entry = entry.lock();
-            let is_symlink = real_path != path;
+        let mut state = self.state.lock();
+        state.metadata_call_count += 1;
+        if let Some((mut entry, _)) = state.try_read_path(&path, false) {
+            let is_symlink = entry.lock().is_symlink();
+            if is_symlink {
+                if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
+                    entry = e;
+                } else {
+                    return Ok(None);
+                }
+            }
 
+            let entry = entry.lock();
             Ok(Some(match &*entry {
                 FakeFsEntry::File { inode, mtime, .. } => Metadata {
                     inode: *inode,
@@ -1102,13 +1145,30 @@ impl Fs for FakeFs {
         }
     }
 
+    async fn read_link(&self, path: &Path) -> Result<PathBuf> {
+        self.simulate_random_delay().await;
+        let path = normalize_path(path);
+        let state = self.state.lock();
+        if let Some((entry, _)) = state.try_read_path(&path, false) {
+            let entry = entry.lock();
+            if let FakeFsEntry::Symlink { target } = &*entry {
+                Ok(target.clone())
+            } else {
+                Err(anyhow!("not a symlink: {}", path.display()))
+            }
+        } else {
+            Err(anyhow!("path does not exist: {}", path.display()))
+        }
+    }
+
     async fn read_dir(
         &self,
         path: &Path,
     ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
         self.simulate_random_delay().await;
         let path = normalize_path(path);
-        let state = self.state.lock();
+        let mut state = self.state.lock();
+        state.read_dir_call_count += 1;
         let entry = state.read_path(&path)?;
         let mut entry = entry.lock();
         let children = entry.dir_entries(&path)?;

crates/fs/src/repository.rs πŸ”—

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use collections::HashMap;
-use git2::ErrorCode;
+use git2::{BranchType, ErrorCode};
 use parking_lot::Mutex;
 use rpc::proto;
 use serde_derive::{Deserialize, Serialize};
@@ -16,6 +16,12 @@ use util::ResultExt;
 
 pub use git2::Repository as LibGitRepository;
 
+#[derive(Clone, Debug, Hash, PartialEq)]
+pub struct Branch {
+    pub name: Box<str>,
+    /// Timestamp of most recent commit, normalized to Unix Epoch format.
+    pub unix_timestamp: Option<i64>,
+}
 #[async_trait::async_trait]
 pub trait GitRepository: Send {
     fn reload_index(&self);
@@ -27,6 +33,12 @@ pub trait GitRepository: Send {
     fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
 
     fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
+    fn branches(&self) -> Result<Vec<Branch>> {
+        Ok(vec![])
+    }
+    fn change_branch(&self, _: &str) -> Result<()> {
+        Ok(())
+    }
 }
 
 impl std::fmt::Debug for dyn GitRepository {
@@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository {
             }
         }
     }
+    fn branches(&self) -> Result<Vec<Branch>> {
+        let local_branches = self.branches(Some(BranchType::Local))?;
+        let valid_branches = local_branches
+            .filter_map(|branch| {
+                branch.ok().and_then(|(branch, _)| {
+                    let name = branch.name().ok().flatten().map(Box::from)?;
+                    let timestamp = branch.get().peel_to_commit().ok()?.time();
+                    let unix_timestamp = timestamp.seconds();
+                    let timezone_offset = timestamp.offset_minutes();
+                    let utc_offset =
+                        time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
+                    let unix_timestamp =
+                        time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
+                    Some(Branch {
+                        name,
+                        unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
+                    })
+                })
+            })
+            .collect();
+        Ok(valid_branches)
+    }
+    fn change_branch(&self, name: &str) -> Result<()> {
+        let revision = self.find_branch(name, BranchType::Local)?;
+        let revision = revision.get();
+        let as_tree = revision.peel_to_tree()?;
+        self.checkout_tree(as_tree.as_object(), None)?;
+        self.set_head(
+            revision
+                .name()
+                .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
+        )?;
+        Ok(())
+    }
 }
 
 fn read_status(status: git2::Status) -> Option<GitFileStatus> {

crates/go_to_line/src/go_to_line.rs πŸ”—

@@ -24,6 +24,7 @@ pub struct GoToLine {
     prev_scroll_position: Option<Vector2F>,
     cursor_point: Point,
     max_point: Point,
+    has_focus: bool,
 }
 
 pub enum Event {
@@ -57,6 +58,7 @@ impl GoToLine {
             prev_scroll_position: scroll_position,
             cursor_point,
             max_point,
+            has_focus: false,
         }
     }
 
@@ -178,11 +180,20 @@ impl View for GoToLine {
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
         cx.focus(&self.line_editor);
     }
+
+    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
 }
 
 impl Modal for GoToLine {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
     fn dismiss_on_event(event: &Self::Event) -> bool {
         matches!(event, Event::Dismissed)
     }

crates/gpui/src/app.rs πŸ”—

@@ -152,6 +152,29 @@ impl App {
             asset_source,
         ))));
 
+        foreground_platform.on_event(Box::new({
+            let cx = app.0.clone();
+            move |event| {
+                if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event {
+                    // Allow system menu "cmd-?" shortcut to be overridden
+                    if keystroke.cmd
+                        && !keystroke.shift
+                        && !keystroke.alt
+                        && !keystroke.function
+                        && keystroke.key == "?"
+                    {
+                        if cx
+                            .borrow_mut()
+                            .update_active_window(|cx| cx.dispatch_keystroke(keystroke))
+                            .unwrap_or(false)
+                        {
+                            return true;
+                        }
+                    }
+                }
+                false
+            }
+        }));
         foreground_platform.on_quit(Box::new({
             let cx = app.0.clone();
             move || {
@@ -445,7 +468,7 @@ type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut WindowContext
 type KeystrokeCallback =
     Box<dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool>;
 type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
-type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
+type DeserializeActionCallback = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
 type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
 
 pub struct AppContext {
@@ -624,14 +647,14 @@ impl AppContext {
     pub fn deserialize_action(
         &self,
         name: &str,
-        argument: Option<&str>,
+        argument: Option<serde_json::Value>,
     ) -> Result<Box<dyn Action>> {
         let callback = self
             .action_deserializers
             .get(name)
             .ok_or_else(|| anyhow!("unknown action {}", name))?
             .1;
-        callback(argument.unwrap_or("{}"))
+        callback(argument.unwrap_or_else(|| serde_json::Value::Object(Default::default())))
             .with_context(|| format!("invalid data for action {}", name))
     }
 
@@ -2948,14 +2971,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     }
 
     pub fn focus(&mut self, handle: &AnyViewHandle) {
-        self.window_context
-            .focus(handle.window_id, Some(handle.view_id));
+        self.window_context.focus(Some(handle.view_id));
     }
 
     pub fn focus_self(&mut self) {
-        let window_id = self.window_id;
         let view_id = self.view_id;
-        self.window_context.focus(window_id, Some(view_id));
+        self.window_context.focus(Some(view_id));
     }
 
     pub fn is_self_focused(&self) -> bool {
@@ -2974,8 +2995,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
     }
 
     pub fn blur(&mut self) {
-        let window_id = self.window_id;
-        self.window_context.focus(window_id, None);
+        self.window_context.focus(None);
     }
 
     pub fn on_window_should_close<F>(&mut self, mut callback: F)
@@ -3281,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
         let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
         MouseState {
             hovered: self.window.hovered_region_ids.contains(&region_id),
-            clicked: self
-                .window
-                .clicked_region_ids
-                .get(&region_id)
-                .and_then(|_| self.window.clicked_button),
+            clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
+                if region_id == clicked_region_id {
+                    Some(button)
+                } else {
+                    None
+                }
+            } else {
+                None
+            },
             accessed_hovered: false,
             accessed_clicked: false,
         }
@@ -5573,7 +5597,7 @@ mod tests {
         let action1 = cx
             .deserialize_action(
                 "test::something::ComplexAction",
-                Some(r#"{"arg": "a", "count": 5}"#),
+                Some(serde_json::from_str(r#"{"arg": "a", "count": 5}"#).unwrap()),
             )
             .unwrap();
         let action2 = cx

crates/gpui/src/app/action.rs πŸ”—

@@ -11,7 +11,7 @@ pub trait Action: 'static {
     fn qualified_name() -> &'static str
     where
         Self: Sized;
-    fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
+    fn from_json_str(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
     where
         Self: Sized;
 }
@@ -38,7 +38,7 @@ macro_rules! actions {
             $crate::__impl_action! {
                 $namespace,
                 $name,
-                fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                fn from_json_str(_: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
                     Ok(Box::new(Self))
                 }
             }
@@ -58,8 +58,8 @@ macro_rules! impl_actions {
             $crate::__impl_action! {
                 $namespace,
                 $name,
-                fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
-                    Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
+                fn from_json_str(json: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
+                    Ok(Box::new($crate::serde_json::from_value::<Self>(json)?))
                 }
             }
         )*

crates/gpui/src/app/window.rs πŸ”—

@@ -8,14 +8,14 @@ use crate::{
         MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
     },
     scene::{
-        CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
-        MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
+        CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
+        MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
     },
     text_layout::TextLayoutCache,
     util::post_inc,
     Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
-    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
-    View, ViewContext, ViewHandle, WindowInvalidation,
+    Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
+    Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
 };
 use anyhow::{anyhow, bail, Result};
 use collections::{HashMap, HashSet};
@@ -53,7 +53,7 @@ pub struct Window {
     last_mouse_moved_event: Option<Event>,
     pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
     pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
-    pub(crate) clicked_button: Option<MouseButton>,
+    pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
     mouse_position: Vector2F,
     text_layout_cache: TextLayoutCache,
 }
@@ -86,7 +86,7 @@ impl Window {
             last_mouse_moved_event: None,
             hovered_region_ids: Default::default(),
             clicked_region_ids: Default::default(),
-            clicked_button: None,
+            clicked_region: None,
             mouse_position: vec2f(0., 0.),
             titlebar_height,
             appearance,
@@ -394,7 +394,7 @@ impl<'a> WindowContext<'a> {
             .iter()
             .filter_map(move |(name, (type_id, deserialize))| {
                 if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
-                    let action = deserialize("{}").ok()?;
+                    let action = deserialize(serde_json::Value::Object(Default::default())).ok()?;
                     let bindings = self
                         .keystroke_matcher
                         .bindings_for_action_type(*type_id)
@@ -434,7 +434,11 @@ impl<'a> WindowContext<'a> {
                 MatchResult::None => false,
                 MatchResult::Pending => true,
                 MatchResult::Matches(matches) => {
+                    let no_action_id = (NoAction {}).id();
                     for (view_id, action) in matches {
+                        if action.id() == no_action_id {
+                            return false;
+                        }
                         if self.dispatch_action(Some(*view_id), action.as_ref()) {
                             self.keystroke_matcher.clear_pending();
                             handled_by = Some(action.boxed_clone());
@@ -480,8 +484,8 @@ impl<'a> WindowContext<'a> {
                 // specific ancestor element that contained both [positions]'
                 // So we need to store the overlapping regions on mouse down.
 
-                // If there is already clicked_button stored, don't replace it.
-                if self.window.clicked_button.is_none() {
+                // If there is already region being clicked, don't replace it.
+                if self.window.clicked_region.is_none() {
                     self.window.clicked_region_ids = self
                         .window
                         .mouse_regions
@@ -495,7 +499,17 @@ impl<'a> WindowContext<'a> {
                         })
                         .collect();
 
-                    self.window.clicked_button = Some(e.button);
+                    let mut highest_z_index = 0;
+                    let mut clicked_region_id = None;
+                    for (region, z_index) in self.window.mouse_regions.iter() {
+                        if region.bounds.contains_point(e.position) && *z_index >= highest_z_index {
+                            highest_z_index = *z_index;
+                            clicked_region_id = Some(region.id());
+                        }
+                    }
+
+                    self.window.clicked_region =
+                        clicked_region_id.map(|region_id| (region_id, e.button));
                 }
 
                 mouse_events.push(MouseEvent::Down(MouseDown {
@@ -524,6 +538,10 @@ impl<'a> WindowContext<'a> {
                     region: Default::default(),
                     platform_event: e.clone(),
                 }));
+                mouse_events.push(MouseEvent::ClickOut(MouseClickOut {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
             }
 
             Event::MouseMoved(
@@ -556,7 +574,7 @@ impl<'a> WindowContext<'a> {
                             prev_mouse_position: self.window.mouse_position,
                             platform_event: e.clone(),
                         }));
-                    } else if let Some(clicked_button) = self.window.clicked_button {
+                    } else if let Some((_, clicked_button)) = self.window.clicked_region {
                         // Mouse up event happened outside the current window. Simulate mouse up button event
                         let button_event = e.to_button_event(clicked_button);
                         mouse_events.push(MouseEvent::Up(MouseUp {
@@ -679,8 +697,8 @@ impl<'a> WindowContext<'a> {
                     // Only raise click events if the released button is the same as the one stored
                     if self
                         .window
-                        .clicked_button
-                        .map(|clicked_button| clicked_button == e.button)
+                        .clicked_region
+                        .map(|(_, clicked_button)| clicked_button == e.button)
                         .unwrap_or(false)
                     {
                         // Clear clicked regions and clicked button
@@ -688,7 +706,7 @@ impl<'a> WindowContext<'a> {
                             &mut self.window.clicked_region_ids,
                             Default::default(),
                         );
-                        self.window.clicked_button = None;
+                        self.window.clicked_region = None;
 
                         // Find regions which still overlap with the mouse since the last MouseDown happened
                         for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
@@ -712,7 +730,10 @@ impl<'a> WindowContext<'a> {
                     }
                 }
 
-                MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
+                MouseEvent::MoveOut(_)
+                | MouseEvent::UpOut(_)
+                | MouseEvent::DownOut(_)
+                | MouseEvent::ClickOut(_) => {
                     for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
                         // NOT contains
                         if !mouse_region
@@ -860,18 +881,10 @@ impl<'a> WindowContext<'a> {
         }
         for view_id in &invalidation.updated {
             let titlebar_height = self.window.titlebar_height;
-            let hovered_region_ids = self.window.hovered_region_ids.clone();
-            let clicked_region_ids = self
-                .window
-                .clicked_button
-                .map(|button| (self.window.clicked_region_ids.clone(), button));
-
             let element = self
                 .render_view(RenderParams {
                     view_id: *view_id,
                     titlebar_height,
-                    hovered_region_ids,
-                    clicked_region_ids,
                     refreshing: false,
                     appearance,
                 })
@@ -1085,6 +1098,10 @@ impl<'a> WindowContext<'a> {
         self.window.focused_view_id
     }
 
+    pub fn focus(&mut self, view_id: Option<usize>) {
+        self.app_context.focus(self.window_id, view_id);
+    }
+
     pub fn window_bounds(&self) -> WindowBounds {
         self.window.platform_window.bounds()
     }
@@ -1176,8 +1193,6 @@ impl<'a> WindowContext<'a> {
 pub struct RenderParams {
     pub view_id: usize,
     pub titlebar_height: f32,
-    pub hovered_region_ids: HashSet<MouseRegionId>,
-    pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
     pub refreshing: bool,
     pub appearance: Appearance,
 }

crates/gpui/src/color.rs πŸ”—

@@ -6,15 +6,16 @@ use std::{
 
 use crate::json::ToJson;
 use pathfinder_color::{ColorF, ColorU};
+use schemars::JsonSchema;
 use serde::{
     de::{self, Unexpected},
     Deserialize, Deserializer,
 };
 use serde_json::json;
 
-#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
 #[repr(transparent)]
-pub struct Color(ColorU);
+pub struct Color(#[schemars(with = "String")] ColorU);
 
 impl Color {
     pub fn transparent_black() -> Self {

crates/gpui/src/elements.rs πŸ”—

@@ -41,13 +41,7 @@ use collections::HashMap;
 use core::panic;
 use json::ToJson;
 use smallvec::SmallVec;
-use std::{
-    any::Any,
-    borrow::Cow,
-    marker::PhantomData,
-    mem,
-    ops::{Deref, DerefMut, Range},
-};
+use std::{any::Any, borrow::Cow, mem, ops::Range};
 
 pub trait Element<V: View>: 'static {
     type LayoutState;
@@ -567,90 +561,6 @@ impl<V: View> RootElement<V> {
     }
 }
 
-pub trait Component<V: View>: 'static {
-    fn render(&self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
-}
-
-pub struct ComponentHost<V: View, C: Component<V>> {
-    component: C,
-    view_type: PhantomData<V>,
-}
-
-impl<V: View, C: Component<V>> ComponentHost<V, C> {
-    pub fn new(c: C) -> Self {
-        Self {
-            component: c,
-            view_type: PhantomData,
-        }
-    }
-}
-
-impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
-    type Target = C;
-
-    fn deref(&self) -> &Self::Target {
-        &self.component
-    }
-}
-
-impl<V: View, C: Component<V>> DerefMut for ComponentHost<V, C> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.component
-    }
-}
-
-impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
-    type LayoutState = AnyElement<V>;
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        view: &mut V,
-        cx: &mut LayoutContext<V>,
-    ) -> (Vector2F, AnyElement<V>) {
-        let mut element = self.component.render(view, cx);
-        let size = element.layout(constraint, view, cx);
-        (size, element)
-    }
-
-    fn paint(
-        &mut self,
-        scene: &mut SceneBuilder,
-        bounds: RectF,
-        visible_bounds: RectF,
-        element: &mut AnyElement<V>,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) {
-        element.paint(scene, bounds.origin(), visible_bounds, view, cx);
-    }
-
-    fn rect_for_text_range(
-        &self,
-        range_utf16: Range<usize>,
-        _: RectF,
-        _: RectF,
-        element: &AnyElement<V>,
-        _: &(),
-        view: &V,
-        cx: &ViewContext<V>,
-    ) -> Option<RectF> {
-        element.rect_for_text_range(range_utf16, view, cx)
-    }
-
-    fn debug(
-        &self,
-        _: RectF,
-        element: &AnyElement<V>,
-        _: &(),
-        view: &V,
-        cx: &ViewContext<V>,
-    ) -> serde_json::Value {
-        element.debug(view, cx)
-    }
-}
-
 pub trait AnyRootElement {
     fn layout(
         &mut self,

crates/gpui/src/elements/container.rs πŸ”—

@@ -12,10 +12,11 @@ use crate::{
     scene::{self, Border, CursorRegion, Quad},
     AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
 };
+use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_json::json;
 
-#[derive(Clone, Copy, Debug, Default, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
 pub struct ContainerStyle {
     #[serde(default)]
     pub margin: Margin,
@@ -332,7 +333,7 @@ impl ToJson for ContainerStyle {
     }
 }
 
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, JsonSchema)]
 pub struct Margin {
     pub top: f32,
     pub left: f32,
@@ -359,7 +360,7 @@ impl ToJson for Margin {
     }
 }
 
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default, JsonSchema)]
 pub struct Padding {
     pub top: f32,
     pub left: f32,
@@ -486,9 +487,10 @@ impl ToJson for Padding {
     }
 }
 
-#[derive(Clone, Copy, Debug, Default, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
 pub struct Shadow {
     #[serde(default, deserialize_with = "deserialize_vec2f")]
+    #[schemars(with = "Vec::<f32>")]
     offset: Vector2F,
     #[serde(default)]
     blur: f32,

crates/gpui/src/elements/image.rs πŸ”—

@@ -8,6 +8,7 @@ use crate::{
     scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
     ViewContext,
 };
+use schemars::JsonSchema;
 use serde::Deserialize;
 use std::{ops::Range, sync::Arc};
 
@@ -21,7 +22,7 @@ pub struct Image {
     style: ImageStyle,
 }
 
-#[derive(Copy, Clone, Default, Deserialize)]
+#[derive(Copy, Clone, Default, Deserialize, JsonSchema)]
 pub struct ImageStyle {
     #[serde(default)]
     pub border: Border,

crates/gpui/src/elements/label.rs πŸ”—

@@ -10,6 +10,7 @@ use crate::{
     text_layout::{Line, RunStyle},
     Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
 };
+use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_json::json;
 use smallvec::{smallvec, SmallVec};
@@ -20,7 +21,7 @@ pub struct Label {
     highlight_indices: Vec<usize>,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct LabelStyle {
     pub text: TextStyle,
     pub highlight_text: Option<TextStyle>,
@@ -164,6 +165,7 @@ impl<V: View> Element<V> for Label {
         _: &mut V,
         cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
+        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
         line.paint(
             scene,
             bounds.origin(),

crates/gpui/src/elements/list.rs πŸ”—

@@ -211,7 +211,7 @@ impl<V: View> Element<V> for List<V> {
         let mut cursor = old_items.cursor::<Count>();
 
         if state.rendered_range.start < new_rendered_range.start {
-            new_items.push_tree(
+            new_items.append(
                 cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
                 &(),
             );
@@ -221,7 +221,7 @@ impl<V: View> Element<V> for List<V> {
                 cursor.next(&());
             }
         }
-        new_items.push_tree(
+        new_items.append(
             cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
             &(),
         );
@@ -230,7 +230,7 @@ impl<V: View> Element<V> for List<V> {
         cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
 
         if new_rendered_range.end < state.rendered_range.start {
-            new_items.push_tree(
+            new_items.append(
                 cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
                 &(),
             );
@@ -240,7 +240,7 @@ impl<V: View> Element<V> for List<V> {
             cursor.next(&());
         }
 
-        new_items.push_tree(cursor.suffix(&()), &());
+        new_items.append(cursor.suffix(&()), &());
 
         state.items = new_items;
         state.rendered_range = new_rendered_range;
@@ -413,7 +413,7 @@ impl<V: View> ListState<V> {
         old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
 
         new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
-        new_heights.push_tree(old_heights.suffix(&()), &());
+        new_heights.append(old_heights.suffix(&()), &());
         drop(old_heights);
         state.items = new_heights;
     }

crates/gpui/src/elements/mouse_event_handler.rs πŸ”—

@@ -7,8 +7,8 @@ use crate::{
     platform::CursorStyle,
     platform::MouseButton,
     scene::{
-        CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
-        MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
+        CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
+        MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
     },
     AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
     SizeConstraint, View, ViewContext,
@@ -136,6 +136,15 @@ impl<Tag, V: View> MouseEventHandler<Tag, V> {
         self
     }
 
+    pub fn on_click_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_click_out(button, handler);
+        self
+    }
+
     pub fn on_down_out(
         mut self,
         button: MouseButton,

crates/gpui/src/elements/svg.rs πŸ”—

@@ -1,7 +1,5 @@
-use std::{borrow::Cow, ops::Range};
-
-use serde_json::json;
-
+use super::constrain_size_preserving_aspect_ratio;
+use crate::json::ToJson;
 use crate::{
     color::Color,
     geometry::{
@@ -10,6 +8,10 @@ use crate::{
     },
     scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
 };
+use schemars::JsonSchema;
+use serde_derive::Deserialize;
+use serde_json::json;
+use std::{borrow::Cow, ops::Range};
 
 pub struct Svg {
     path: Cow<'static, str>,
@@ -24,6 +26,14 @@ impl Svg {
         }
     }
 
+    pub fn for_style<V: View>(style: SvgStyle) -> impl Element<V> {
+        Self::new(style.asset)
+            .with_color(style.color)
+            .constrained()
+            .with_width(style.dimensions.width)
+            .with_height(style.dimensions.height)
+    }
+
     pub fn with_color(mut self, color: Color) -> Self {
         self.color = color;
         self
@@ -105,9 +115,24 @@ impl<V: View> Element<V> for Svg {
     }
 }
 
-use crate::json::ToJson;
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct SvgStyle {
+    pub color: Color,
+    pub asset: String,
+    pub dimensions: Dimensions,
+}
 
-use super::constrain_size_preserving_aspect_ratio;
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct Dimensions {
+    pub width: f32,
+    pub height: f32,
+}
+
+impl Dimensions {
+    pub fn to_vec(&self) -> Vector2F {
+        vec2f(self.width, self.height)
+    }
+}
 
 fn from_usvg_rect(rect: usvg::Rect) -> RectF {
     RectF::new(

crates/gpui/src/elements/tooltip.rs πŸ”—

@@ -9,6 +9,7 @@ use crate::{
     Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
     ViewContext,
 };
+use schemars::JsonSchema;
 use serde::Deserialize;
 use std::{
     cell::{Cell, RefCell},
@@ -33,7 +34,7 @@ struct TooltipState {
     debounce: RefCell<Option<Task<()>>>,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct TooltipStyle {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -42,7 +43,7 @@ pub struct TooltipStyle {
     pub max_text_width: Option<f32>,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct KeystrokeStyle {
     #[serde(flatten)]
     container: ContainerStyle,

crates/gpui/src/executor.rs πŸ”—

@@ -7,6 +7,7 @@ use std::{
     fmt::{self, Display},
     marker::PhantomData,
     mem,
+    panic::Location,
     pin::Pin,
     rc::Rc,
     sync::Arc,
@@ -965,10 +966,12 @@ impl<T> Task<T> {
 }
 
 impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
+    #[track_caller]
     pub fn detach_and_log_err(self, cx: &mut AppContext) {
+        let caller = Location::caller();
         cx.spawn(|_| async move {
             if let Err(err) = self.await {
-                log::error!("{:#}", err);
+                log::error!("{}:{}: {:#}", caller.file(), caller.line(), err);
             }
         })
         .detach();

crates/gpui/src/font_cache.rs πŸ”—

@@ -7,13 +7,14 @@ use crate::{
 use anyhow::{anyhow, Result};
 use ordered_float::OrderedFloat;
 use parking_lot::{RwLock, RwLockUpgradableReadGuard};
+use schemars::JsonSchema;
 use std::{
     collections::HashMap,
     ops::{Deref, DerefMut},
     sync::Arc,
 };
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
 pub struct FamilyId(usize);
 
 struct Family {

crates/gpui/src/fonts.rs πŸ”—

@@ -16,7 +16,7 @@ use serde::{de, Deserialize, Serialize};
 use serde_json::Value;
 use std::{cell::RefCell, sync::Arc};
 
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
 pub struct FontId(pub usize);
 
 pub type GlyphId = u32;
@@ -59,20 +59,44 @@ pub struct Features {
     pub zero: Option<bool>,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, JsonSchema)]
 pub struct TextStyle {
     pub color: Color,
     pub font_family_name: Arc<str>,
     pub font_family_id: FamilyId,
     pub font_id: FontId,
     pub font_size: f32,
+    #[schemars(with = "PropertiesDef")]
     pub font_properties: Properties,
     pub underline: Underline,
 }
 
-#[derive(Copy, Clone, Debug, Default, PartialEq)]
+#[derive(JsonSchema)]
+#[serde(remote = "Properties")]
+pub struct PropertiesDef {
+    /// The font style, as defined in CSS.
+    pub style: StyleDef,
+    /// The font weight, as defined in CSS.
+    pub weight: f32,
+    /// The font stretchiness, as defined in CSS.
+    pub stretch: f32,
+}
+
+#[derive(JsonSchema)]
+#[schemars(remote = "Style")]
+pub enum StyleDef {
+    /// A face that is neither italic not obliqued.
+    Normal,
+    /// A form that is generally cursive in nature.
+    Italic,
+    /// A typically-sloped version of the regular face.
+    Oblique,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
 pub struct HighlightStyle {
     pub color: Option<Color>,
+    #[schemars(with = "Option::<f32>")]
     pub weight: Option<Weight>,
     pub italic: Option<bool>,
     pub underline: Option<Underline>,
@@ -81,9 +105,10 @@ pub struct HighlightStyle {
 
 impl Eq for HighlightStyle {}
 
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
 pub struct Underline {
     pub color: Option<Color>,
+    #[schemars(with = "f32")]
     pub thickness: OrderedFloat<f32>,
     pub squiggly: bool,
 }

crates/gpui/src/gpui.rs πŸ”—

@@ -26,8 +26,10 @@ pub mod color;
 pub mod json;
 pub mod keymap_matcher;
 pub mod platform;
-pub use gpui_macros::test;
+pub use gpui_macros::{test, Element};
 pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
 
 pub use anyhow;
 pub use serde_json;
+
+actions!(zed, [NoAction]);

crates/gpui/src/platform.rs πŸ”—

@@ -25,6 +25,7 @@ use anyhow::{anyhow, bail, Result};
 use async_task::Runnable;
 pub use event::*;
 use postage::oneshot;
+use schemars::JsonSchema;
 use serde::Deserialize;
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
@@ -282,7 +283,7 @@ pub enum PromptLevel {
     Critical,
 }
 
-#[derive(Copy, Clone, Debug, Deserialize)]
+#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
 pub enum CursorStyle {
     Arrow,
     ResizeLeftRight,

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -786,7 +786,7 @@ impl platform::Platform for MacPlatform {
 
     fn set_cursor_style(&self, style: CursorStyle) {
         unsafe {
-            let cursor: id = match style {
+            let new_cursor: id = match style {
                 CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
                 CursorStyle::ResizeLeftRight => {
                     msg_send![class!(NSCursor), resizeLeftRightCursor]
@@ -795,7 +795,11 @@ impl platform::Platform for MacPlatform {
                 CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
                 CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
             };
-            let _: () = msg_send![cursor, set];
+
+            let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
+            if new_cursor != old_cursor {
+                let _: () = msg_send![new_cursor, set];
+            }
         }
     }
 
@@ -935,7 +939,6 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
                 }
             }
         }
-
         msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
     }
 }

crates/gpui/src/scene.rs πŸ”—

@@ -3,6 +3,7 @@ mod mouse_region;
 
 #[cfg(debug_assertions)]
 use collections::HashSet;
+use schemars::JsonSchema;
 use serde::Deserialize;
 use serde_json::json;
 use std::{borrow::Cow, sync::Arc};
@@ -99,7 +100,7 @@ pub struct Icon {
     pub color: Color,
 }
 
-#[derive(Clone, Copy, Default, Debug)]
+#[derive(Clone, Copy, Default, Debug, JsonSchema)]
 pub struct Border {
     pub width: f32,
     pub color: Color,

crates/gpui/src/scene/mouse_event.rs πŸ”—

@@ -99,6 +99,20 @@ impl Deref for MouseClick {
     }
 }
 
+#[derive(Debug, Default, Clone)]
+pub struct MouseClickOut {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for MouseClickOut {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
 #[derive(Debug, Default, Clone)]
 pub struct MouseDownOut {
     pub region: RectF,
@@ -150,6 +164,7 @@ pub enum MouseEvent {
     Down(MouseDown),
     Up(MouseUp),
     Click(MouseClick),
+    ClickOut(MouseClickOut),
     DownOut(MouseDownOut),
     UpOut(MouseUpOut),
     ScrollWheel(MouseScrollWheel),
@@ -165,6 +180,7 @@ impl MouseEvent {
             MouseEvent::Down(r) => r.region = region,
             MouseEvent::Up(r) => r.region = region,
             MouseEvent::Click(r) => r.region = region,
+            MouseEvent::ClickOut(r) => r.region = region,
             MouseEvent::DownOut(r) => r.region = region,
             MouseEvent::UpOut(r) => r.region = region,
             MouseEvent::ScrollWheel(r) => r.region = region,
@@ -182,6 +198,7 @@ impl MouseEvent {
             MouseEvent::Down(_) => true,
             MouseEvent::Up(_) => true,
             MouseEvent::Click(_) => true,
+            MouseEvent::ClickOut(_) => true,
             MouseEvent::DownOut(_) => false,
             MouseEvent::UpOut(_) => false,
             MouseEvent::ScrollWheel(_) => true,
@@ -222,6 +239,10 @@ impl MouseEvent {
         discriminant(&MouseEvent::Click(Default::default()))
     }
 
+    pub fn click_out_disc() -> Discriminant<MouseEvent> {
+        discriminant(&MouseEvent::ClickOut(Default::default()))
+    }
+
     pub fn down_out_disc() -> Discriminant<MouseEvent> {
         discriminant(&MouseEvent::DownOut(Default::default()))
     }
@@ -239,6 +260,7 @@ impl MouseEvent {
             MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
             MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
             MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
+            MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)),
             MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
             MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
             MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),

crates/gpui/src/scene/mouse_region.rs πŸ”—

@@ -14,7 +14,7 @@ use super::{
         MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
         MouseUpOut,
     },
-    MouseMoveOut, MouseScrollWheel,
+    MouseClickOut, MouseMoveOut, MouseScrollWheel,
 };
 
 #[derive(Clone)]
@@ -89,6 +89,15 @@ impl MouseRegion {
         self
     }
 
+    pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
+    where
+        V: View,
+        F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+    {
+        self.handlers = self.handlers.on_click_out(button, handler);
+        self
+    }
+
     pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
         V: View,
@@ -246,6 +255,10 @@ impl HandlerSet {
                 HandlerKey::new(MouseEvent::click_disc(), Some(button)),
                 SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
             );
+            set.insert(
+                HandlerKey::new(MouseEvent::click_out_disc(), Some(button)),
+                SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
+            );
             set.insert(
                 HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
                 SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
@@ -405,6 +418,28 @@ impl HandlerSet {
         self
     }
 
+    pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
+    where
+        V: View,
+        F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
+    {
+        self.insert(MouseEvent::click_out_disc(), Some(button),
+            Rc::new(move |region_event, view, cx, view_id| {
+                if let MouseEvent::ClickOut(e) = region_event {
+                    let view = view.downcast_mut().unwrap();
+                    let mut cx = ViewContext::mutable(cx, view_id);
+                    let mut cx = EventContext::new(&mut cx);
+                    handler(e, view, &mut cx);
+                    cx.handled
+                } else {
+                    panic!(
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}",
+                        region_event);
+                }
+            }));
+        self
+    }
+
     pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
     where
         V: View,

crates/gpui_macros/src/gpui_macros.rs πŸ”—

@@ -3,8 +3,8 @@ use proc_macro2::Ident;
 use quote::{format_ident, quote};
 use std::mem;
 use syn::{
-    parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, FnArg, ItemFn, Lit, Meta,
-    NestedMeta, Type,
+    parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg,
+    ItemFn, Lit, Meta, NestedMeta, Type,
 };
 
 #[proc_macro_attribute]
@@ -275,3 +275,68 @@ fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
 
     result.map_err(|err| TokenStream::from(err.into_compile_error()))
 }
+
+#[proc_macro_derive(Element)]
+pub fn element_derive(input: TokenStream) -> TokenStream {
+    // Parse the input tokens into a syntax tree
+    let input = parse_macro_input!(input as DeriveInput);
+
+    // The name of the struct/enum
+    let name = input.ident;
+
+    let expanded = quote! {
+        impl<V: gpui::View> gpui::elements::Element<V> for #name {
+            type LayoutState = gpui::elements::AnyElement<V>;
+            type PaintState = ();
+
+            fn layout(
+                &mut self,
+                constraint: gpui::SizeConstraint,
+                view: &mut V,
+                cx: &mut gpui::LayoutContext<V>,
+            ) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
+                let mut element = self.render(view, cx);
+                let size = element.layout(constraint, view, cx);
+                (size, element)
+            }
+
+            fn paint(
+                &mut self,
+                scene: &mut gpui::SceneBuilder,
+                bounds: gpui::geometry::rect::RectF,
+                visible_bounds: gpui::geometry::rect::RectF,
+                element: &mut gpui::elements::AnyElement<V>,
+                view: &mut V,
+                cx: &mut gpui::ViewContext<V>,
+            ) {
+                element.paint(scene, bounds.origin(), visible_bounds, view, cx);
+            }
+
+            fn rect_for_text_range(
+                &self,
+                range_utf16: std::ops::Range<usize>,
+                _: gpui::geometry::rect::RectF,
+                _: gpui::geometry::rect::RectF,
+                element: &gpui::elements::AnyElement<V>,
+                _: &(),
+                view: &V,
+                cx: &gpui::ViewContext<V>,
+            ) -> Option<gpui::geometry::rect::RectF> {
+                element.rect_for_text_range(range_utf16, view, cx)
+            }
+
+            fn debug(
+                &self,
+                _: gpui::geometry::rect::RectF,
+                element: &gpui::elements::AnyElement<V>,
+                _: &(),
+                view: &V,
+                cx: &gpui::ViewContext<V>,
+            ) -> serde_json::Value {
+                element.debug(view, cx)
+            }
+        }
+    };
+    // Return generated code
+    TokenStream::from(expanded)
+}

crates/language/src/language.rs πŸ”—

@@ -17,10 +17,10 @@ use futures::{
     future::{BoxFuture, Shared},
     FutureExt, TryFutureExt as _,
 };
-use gpui::{executor::Background, AppContext, Task};
+use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
-use lsp::CodeActionKind;
+use lsp::{CodeActionKind, LanguageServerBinary};
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use regex::Regex;
@@ -30,7 +30,6 @@ use std::{
     any::Any,
     borrow::Cow,
     cell::RefCell,
-    ffi::OsString,
     fmt::Debug,
     hash::Hash,
     mem,
@@ -86,12 +85,6 @@ pub trait ToLspPosition {
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct LanguageServerName(pub Arc<str>);
 
-#[derive(Debug, Clone, Deserialize)]
-pub struct LanguageServerBinary {
-    pub path: PathBuf,
-    pub arguments: Vec<OsString>,
-}
-
 /// Represents a Language Server, with certain cached sync properties.
 /// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
 /// once at startup, and caches the results.
@@ -125,27 +118,57 @@ impl CachedLspAdapter {
 
     pub async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        self.adapter.fetch_latest_server_version(http).await
+        self.adapter.fetch_latest_server_version(delegate).await
+    }
+
+    pub fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        self.adapter.will_fetch_server(delegate, cx)
+    }
+
+    pub fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        self.adapter.will_start_server(delegate, cx)
     }
 
     pub async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         self.adapter
-            .fetch_server_binary(version, http, container_dir)
+            .fetch_server_binary(version, container_dir, delegate)
             .await
     }
 
     pub async fn cached_server_binary(
         &self,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        self.adapter
+            .cached_server_binary(container_dir, delegate)
+            .await
+    }
+
+    pub fn can_be_reinstalled(&self) -> bool {
+        self.adapter.can_be_reinstalled()
+    }
+
+    pub async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        self.adapter.cached_server_binary(container_dir).await
+        self.adapter.installation_test_binary(container_dir).await
     }
 
     pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -187,23 +210,57 @@ impl CachedLspAdapter {
     }
 }
 
+pub trait LspAdapterDelegate: Send + Sync {
+    fn show_notification(&self, message: &str, cx: &mut AppContext);
+    fn http_client(&self) -> Arc<dyn HttpClient>;
+}
+
 #[async_trait]
 pub trait LspAdapter: 'static + Send + Sync {
     async fn name(&self) -> LanguageServerName;
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>>;
 
+    fn will_fetch_server(
+        &self,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        None
+    }
+
+    fn will_start_server(
+        &self,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        None
+    }
+
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary>;
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary>;
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary>;
+
+    fn can_be_reinstalled(&self) -> bool {
+        true
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary>;
 
     async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
@@ -513,10 +570,7 @@ pub struct LanguageRegistry {
     login_shell_env_loaded: Shared<Task<()>>,
     #[allow(clippy::type_complexity)]
     lsp_binary_paths: Mutex<
-        HashMap<
-            LanguageServerName,
-            Shared<BoxFuture<'static, Result<LanguageServerBinary, Arc<anyhow::Error>>>>,
-        >,
+        HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
     >,
     executor: Option<Arc<Background>>,
 }
@@ -535,7 +589,8 @@ struct LanguageRegistryState {
 
 pub struct PendingLanguageServer {
     pub server_id: LanguageServerId,
-    pub task: Task<Result<lsp::LanguageServer>>,
+    pub task: Task<Result<Option<lsp::LanguageServer>>>,
+    pub container_dir: Option<Arc<Path>>,
 }
 
 impl LanguageRegistry {
@@ -807,17 +862,17 @@ impl LanguageRegistry {
         self.state.read().languages.iter().cloned().collect()
     }
 
-    pub fn start_language_server(
+    pub fn create_pending_language_server(
         self: &Arc<Self>,
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
         root_path: Arc<Path>,
-        http_client: Arc<dyn HttpClient>,
+        delegate: Arc<dyn LspAdapterDelegate>,
         cx: &mut AppContext,
     ) -> Option<PendingLanguageServer> {
         let server_id = self.state.write().next_language_server_id();
         log::info!(
-            "starting language server name:{}, path:{root_path:?}, id:{server_id}",
+            "starting language server {:?}, path: {root_path:?}, id: {server_id}",
             adapter.name.0
         );
 
@@ -847,61 +902,81 @@ impl LanguageRegistry {
                         }
                     })
                     .detach();
-                Ok(server)
+
+                Ok(Some(server))
             });
 
-            return Some(PendingLanguageServer { server_id, task });
+            return Some(PendingLanguageServer {
+                server_id,
+                task,
+                container_dir: None,
+            });
         }
 
         let download_dir = self
             .language_server_download_dir
             .clone()
-            .ok_or_else(|| anyhow!("language server download directory has not been assigned"))
+            .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
             .log_err()?;
         let this = self.clone();
         let language = language.clone();
-        let http_client = http_client.clone();
-        let download_dir = download_dir.clone();
+        let container_dir: Arc<Path> = 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 task = cx.spawn(|cx| async move {
-            login_shell_env_loaded.await;
-
-            let mut lock = this.lsp_binary_paths.lock();
-            let entry = lock
-                .entry(adapter.name.clone())
-                .or_insert_with(|| {
-                    get_binary(
-                        adapter.clone(),
-                        language.clone(),
-                        http_client,
-                        download_dir,
-                        lsp_binary_statuses,
-                    )
-                    .map_err(Arc::new)
-                    .boxed()
-                    .shared()
-                })
-                .clone();
-            drop(lock);
-            let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
+        let task = {
+            let container_dir = container_dir.clone();
+            cx.spawn(|mut cx| async move {
+                login_shell_env_loaded.await;
 
-            let server = lsp::LanguageServer::new(
-                server_id,
-                &binary.path,
-                &binary.arguments,
-                &root_path,
-                adapter.code_action_kinds(),
-                cx,
-            )?;
-
-            Ok(server)
-        });
+                let mut lock = this.lsp_binary_paths.lock();
+                let entry = lock
+                    .entry(adapter.name.clone())
+                    .or_insert_with(|| {
+                        cx.spawn(|cx| {
+                            get_binary(
+                                adapter.clone(),
+                                language.clone(),
+                                delegate.clone(),
+                                container_dir,
+                                lsp_binary_statuses,
+                                cx,
+                            )
+                            .map_err(Arc::new)
+                        })
+                        .shared()
+                    })
+                    .clone();
+                drop(lock);
+
+                let binary = match entry.clone().await.log_err() {
+                    Some(binary) => binary,
+                    None => return Ok(None),
+                };
+
+                if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
+                    if task.await.log_err().is_none() {
+                        return Ok(None);
+                    }
+                }
+
+                Ok(Some(lsp::LanguageServer::new(
+                    server_id,
+                    binary,
+                    &root_path,
+                    adapter.code_action_kinds(),
+                    cx,
+                )?))
+            })
+        };
 
-        Some(PendingLanguageServer { server_id, task })
+        Some(PendingLanguageServer {
+            server_id,
+            task,
+            container_dir: Some(container_dir),
+        })
     }
 
     pub fn language_server_binary_statuses(
@@ -909,6 +984,30 @@ impl LanguageRegistry {
     ) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
         self.lsp_binary_statuses_rx.clone()
     }
+
+    pub fn delete_server_container(
+        &self,
+        adapter: Arc<CachedLspAdapter>,
+        cx: &mut AppContext,
+    ) -> Task<()> {
+        log::info!("deleting server container");
+
+        let mut lock = self.lsp_binary_paths.lock();
+        lock.remove(&adapter.name);
+
+        let download_dir = self
+            .language_server_download_dir
+            .clone()
+            .expect("language server download directory has not been assigned before deleting server container");
+
+        cx.spawn(|_| async move {
+            let container_dir = download_dir.join(adapter.name.0.as_ref());
+            smol::fs::remove_dir_all(container_dir)
+                .await
+                .context("server container removal")
+                .log_err();
+        })
+    }
 }
 
 impl LanguageRegistryState {
@@ -958,32 +1057,39 @@ impl Default for LanguageRegistry {
 async fn get_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
-    http_client: Arc<dyn HttpClient>,
-    download_dir: Arc<Path>,
+    delegate: Arc<dyn LspAdapterDelegate>,
+    container_dir: Arc<Path>,
     statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    mut cx: AsyncAppContext,
 ) -> Result<LanguageServerBinary> {
-    let container_dir = download_dir.join(adapter.name.0.as_ref());
     if !container_dir.exists() {
         smol::fs::create_dir_all(&container_dir)
             .await
             .context("failed to create container directory")?;
     }
 
+    if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) {
+        task.await?;
+    }
+
     let binary = fetch_latest_binary(
         adapter.clone(),
         language.clone(),
-        http_client,
+        delegate.as_ref(),
         &container_dir,
         statuses.clone(),
     )
     .await;
 
     if let Err(error) = binary.as_ref() {
-        if let Some(cached) = adapter.cached_server_binary(container_dir).await {
+        if let Some(binary) = adapter
+            .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
+            .await
+        {
             statuses
                 .broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
                 .await?;
-            return Ok(cached);
+            return Ok(binary);
         } else {
             statuses
                 .broadcast((
@@ -995,13 +1101,14 @@ async fn get_binary(
                 .await?;
         }
     }
+
     binary
 }
 
 async fn fetch_latest_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
-    http_client: Arc<dyn HttpClient>,
+    delegate: &dyn LspAdapterDelegate,
     container_dir: &Path,
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
 ) -> Result<LanguageServerBinary> {
@@ -1012,18 +1119,19 @@ async fn fetch_latest_binary(
             LanguageServerBinaryStatus::CheckingForUpdate,
         ))
         .await?;
-    let version_info = adapter
-        .fetch_latest_server_version(http_client.clone())
-        .await?;
+
+    let version_info = adapter.fetch_latest_server_version(delegate).await?;
     lsp_binary_statuses_tx
         .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
         .await?;
+
     let binary = adapter
-        .fetch_server_binary(version_info, http_client, container_dir.to_path_buf())
+        .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
         .await?;
     lsp_binary_statuses_tx
         .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
         .await?;
+
     Ok(binary)
 }
 
@@ -1543,7 +1651,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         unreachable!();
     }
@@ -1551,13 +1659,21 @@ impl LspAdapter for Arc<FakeLspAdapter> {
     async fn fetch_server_binary(
         &self,
         _: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         _: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         unreachable!();
     }
 
-    async fn cached_server_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        unreachable!();
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
         unreachable!();
     }
 

crates/language/src/language_settings.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{File, Language};
 use anyhow::Result;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use globset::GlobMatcher;
 use gpui::AppContext;
 use schemars::{
@@ -52,6 +52,7 @@ pub struct LanguageSettings {
     pub show_copilot_suggestions: bool,
     pub show_whitespaces: ShowWhitespaceSetting,
     pub extend_comment_on_newline: bool,
+    pub inlay_hints: InlayHintSettings,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
     pub show_whitespaces: Option<ShowWhitespaceSetting>,
     #[serde(default)]
     pub extend_comment_on_newline: Option<bool>,
+    #[serde(default)]
+    pub inlay_hints: Option<InlayHintSettings>,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -150,6 +153,38 @@ pub enum Formatter {
     },
 }
 
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct InlayHintSettings {
+    #[serde(default)]
+    pub enabled: bool,
+    #[serde(default = "default_true")]
+    pub show_type_hints: bool,
+    #[serde(default = "default_true")]
+    pub show_parameter_hints: bool,
+    #[serde(default = "default_true")]
+    pub show_other_hints: bool,
+}
+
+fn default_true() -> bool {
+    true
+}
+
+impl InlayHintSettings {
+    pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
+        let mut kinds = HashSet::default();
+        if self.show_type_hints {
+            kinds.insert(Some(InlayHintKind::Type));
+        }
+        if self.show_parameter_hints {
+            kinds.insert(Some(InlayHintKind::Parameter));
+        }
+        if self.show_other_hints {
+            kinds.insert(None);
+        }
+        kinds
+    }
+}
+
 impl AllLanguageSettings {
     pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
         if let Some(name) = language_name {
@@ -184,6 +219,29 @@ impl AllLanguageSettings {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum InlayHintKind {
+    Type,
+    Parameter,
+}
+
+impl InlayHintKind {
+    pub fn from_name(name: &str) -> Option<Self> {
+        match name {
+            "type" => Some(InlayHintKind::Type),
+            "parameter" => Some(InlayHintKind::Parameter),
+            _ => None,
+        }
+    }
+
+    pub fn name(&self) -> &'static str {
+        match self {
+            InlayHintKind::Type => "type",
+            InlayHintKind::Parameter => "parameter",
+        }
+    }
+}
+
 impl settings::Setting for AllLanguageSettings {
     const KEY: Option<&'static str> = None;
 
@@ -347,6 +405,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
         &mut settings.extend_comment_on_newline,
         src.extend_comment_on_newline,
     );
+    merge(&mut settings.inlay_hints, src.inlay_hints);
     fn merge<T>(target: &mut T, value: Option<T>) {
         if let Some(value) = value {
             *target = value;

crates/language/src/syntax_map.rs πŸ”—

@@ -11,7 +11,7 @@ use std::{
     cell::RefCell,
     cmp::{self, Ordering, Reverse},
     collections::BinaryHeap,
-    iter,
+    fmt, iter,
     ops::{Deref, DerefMut, Range},
     sync::Arc,
 };
@@ -288,7 +288,7 @@ impl SyntaxSnapshot {
                 };
                 if target.cmp(&cursor.start(), text).is_gt() {
                     let slice = cursor.slice(&target, Bias::Left, text);
-                    layers.push_tree(slice, text);
+                    layers.append(slice, text);
                 }
             }
             // If this layer follows all of the edits, then preserve it and any
@@ -303,7 +303,7 @@ impl SyntaxSnapshot {
                     Bias::Left,
                     text,
                 );
-                layers.push_tree(slice, text);
+                layers.append(slice, text);
                 continue;
             };
 
@@ -369,7 +369,7 @@ impl SyntaxSnapshot {
             cursor.next(text);
         }
 
-        layers.push_tree(cursor.suffix(&text), &text);
+        layers.append(cursor.suffix(&text), &text);
         drop(cursor);
         self.layers = layers;
     }
@@ -428,6 +428,8 @@ impl SyntaxSnapshot {
         invalidated_ranges: Vec<Range<usize>>,
         registry: Option<&Arc<LanguageRegistry>>,
     ) {
+        log::trace!("reparse. invalidated ranges:{:?}", invalidated_ranges);
+
         let max_depth = self.layers.summary().max_depth;
         let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
         cursor.next(&text);
@@ -478,7 +480,7 @@ impl SyntaxSnapshot {
                 if bounded_position.cmp(&cursor.start(), &text).is_gt() {
                     let slice = cursor.slice(&bounded_position, Bias::Left, text);
                     if !slice.is_empty() {
-                        layers.push_tree(slice, &text);
+                        layers.append(slice, &text);
                         if changed_regions.prune(cursor.end(text), text) {
                             done = false;
                         }
@@ -489,6 +491,15 @@ impl SyntaxSnapshot {
                     let Some(layer) = cursor.item() else { break };
 
                     if changed_regions.intersects(&layer, text) {
+                        if let SyntaxLayerContent::Parsed { language, .. } = &layer.content {
+                            log::trace!(
+                                "discard layer. language:{}, range:{:?}. changed_regions:{:?}",
+                                language.name(),
+                                LogAnchorRange(&layer.range, text),
+                                LogChangedRegions(&changed_regions, text),
+                            );
+                        }
+
                         changed_regions.insert(
                             ChangedRegion {
                                 depth: layer.depth + 1,
@@ -541,26 +552,24 @@ impl SyntaxSnapshot {
                             .to_ts_point();
                     }
 
-                    if included_ranges.is_empty() {
-                        included_ranges.push(tree_sitter::Range {
-                            start_byte: 0,
-                            end_byte: 0,
-                            start_point: Default::default(),
-                            end_point: Default::default(),
-                        });
-                    }
-
-                    if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) =
-                        old_layer.map(|layer| &layer.content)
+                    if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) =
+                        old_layer.map(|layer| (&layer.content, layer.range.start))
                     {
+                        log::trace!(
+                            "existing layer. language:{}, start:{:?}, ranges:{:?}",
+                            language.name(),
+                            LogPoint(layer_start.to_point(&text)),
+                            LogIncludedRanges(&old_tree.included_ranges())
+                        );
+
                         if let ParseMode::Combined {
                             mut parent_layer_changed_ranges,
                             ..
                         } = step.mode
                         {
                             for range in &mut parent_layer_changed_ranges {
-                                range.start -= step_start_byte;
-                                range.end -= step_start_byte;
+                                range.start = range.start.saturating_sub(step_start_byte);
+                                range.end = range.end.saturating_sub(step_start_byte);
                             }
 
                             included_ranges = splice_included_ranges(
@@ -570,6 +579,22 @@ impl SyntaxSnapshot {
                             );
                         }
 
+                        if included_ranges.is_empty() {
+                            included_ranges.push(tree_sitter::Range {
+                                start_byte: 0,
+                                end_byte: 0,
+                                start_point: Default::default(),
+                                end_point: Default::default(),
+                            });
+                        }
+
+                        log::trace!(
+                            "update layer. language:{}, start:{:?}, ranges:{:?}",
+                            language.name(),
+                            LogAnchorRange(&step.range, text),
+                            LogIncludedRanges(&included_ranges),
+                        );
+
                         tree = parse_text(
                             grammar,
                             text.as_rope(),
@@ -586,6 +611,22 @@ impl SyntaxSnapshot {
                             }),
                         );
                     } else {
+                        if included_ranges.is_empty() {
+                            included_ranges.push(tree_sitter::Range {
+                                start_byte: 0,
+                                end_byte: 0,
+                                start_point: Default::default(),
+                                end_point: Default::default(),
+                            });
+                        }
+
+                        log::trace!(
+                            "create layer. language:{}, range:{:?}, included_ranges:{:?}",
+                            language.name(),
+                            LogAnchorRange(&step.range, text),
+                            LogIncludedRanges(&included_ranges),
+                        );
+
                         tree = parse_text(
                             grammar,
                             text.as_rope(),
@@ -613,6 +654,7 @@ impl SyntaxSnapshot {
                         get_injections(
                             config,
                             text,
+                            step.range.clone(),
                             tree.root_node_with_offset(
                                 step_start_byte,
                                 step_start_point.to_ts_point(),
@@ -1117,6 +1159,7 @@ fn parse_text(
 fn get_injections(
     config: &InjectionConfig,
     text: &BufferSnapshot,
+    outer_range: Range<Anchor>,
     node: Node,
     language_registry: &Arc<LanguageRegistry>,
     depth: usize,
@@ -1153,16 +1196,17 @@ fn get_injections(
                 continue;
             }
 
-            // Avoid duplicate matches if two changed ranges intersect the same injection.
             let content_range =
                 content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte;
-            if let Some((last_pattern_ix, last_range)) = &prev_match {
-                if mat.pattern_index == *last_pattern_ix && content_range == *last_range {
+
+            // Avoid duplicate matches if two changed ranges intersect the same injection.
+            if let Some((prev_pattern_ix, prev_range)) = &prev_match {
+                if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range {
                     continue;
                 }
             }
-            prev_match = Some((mat.pattern_index, content_range.clone()));
 
+            prev_match = Some((mat.pattern_index, content_range.clone()));
             let combined = config.patterns[mat.pattern_index].combined;
 
             let mut language_name = None;
@@ -1218,11 +1262,10 @@ fn get_injections(
 
     for (language, mut included_ranges) in combined_injection_ranges.drain() {
         included_ranges.sort_unstable();
-        let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte());
         queue.push(ParseStep {
             depth,
             language: ParseStepLanguage::Loaded { language },
-            range,
+            range: outer_range.clone(),
             included_ranges,
             mode: ParseMode::Combined {
                 parent_layer_range: node.start_byte()..node.end_byte(),
@@ -1234,72 +1277,77 @@ fn get_injections(
 
 pub(crate) fn splice_included_ranges(
     mut ranges: Vec<tree_sitter::Range>,
-    changed_ranges: &[Range<usize>],
+    removed_ranges: &[Range<usize>],
     new_ranges: &[tree_sitter::Range],
 ) -> Vec<tree_sitter::Range> {
-    let mut changed_ranges = changed_ranges.into_iter().peekable();
-    let mut new_ranges = new_ranges.into_iter().peekable();
+    let mut removed_ranges = removed_ranges.iter().cloned().peekable();
+    let mut new_ranges = new_ranges.into_iter().cloned().peekable();
     let mut ranges_ix = 0;
     loop {
-        let new_range = new_ranges.peek();
-        let mut changed_range = changed_ranges.peek();
-
-        // Remove ranges that have changed before inserting any new ranges
-        // into those ranges.
-        if let Some((changed, new)) = changed_range.zip(new_range) {
-            if new.end_byte < changed.start {
-                changed_range = None;
-            }
-        }
-
-        if let Some(changed) = changed_range {
-            let mut start_ix = ranges_ix
-                + match ranges[ranges_ix..].binary_search_by_key(&changed.start, |r| r.end_byte) {
-                    Ok(ix) | Err(ix) => ix,
-                };
-            let mut end_ix = ranges_ix
-                + match ranges[ranges_ix..].binary_search_by_key(&changed.end, |r| r.start_byte) {
-                    Ok(ix) => ix + 1,
-                    Err(ix) => ix,
-                };
+        let next_new_range = new_ranges.peek();
+        let next_removed_range = removed_ranges.peek();
 
-            // If there are empty ranges, then there may be multiple ranges with the same
-            // start or end. Expand the splice to include any adjacent ranges that touch
-            // the changed range.
-            while start_ix > 0 {
-                if ranges[start_ix - 1].end_byte == changed.start {
-                    start_ix -= 1;
-                } else {
-                    break;
-                }
-            }
-            while let Some(range) = ranges.get(end_ix) {
-                if range.start_byte == changed.end {
-                    end_ix += 1;
+        let (remove, insert) = match (next_removed_range, next_new_range) {
+            (None, None) => break,
+            (Some(_), None) => (removed_ranges.next().unwrap(), None),
+            (Some(next_removed_range), Some(next_new_range)) => {
+                if next_removed_range.end < next_new_range.start_byte {
+                    (removed_ranges.next().unwrap(), None)
                 } else {
-                    break;
+                    let mut start = next_new_range.start_byte;
+                    let mut end = next_new_range.end_byte;
+
+                    while let Some(next_removed_range) = removed_ranges.peek() {
+                        if next_removed_range.start > next_new_range.end_byte {
+                            break;
+                        }
+                        let next_removed_range = removed_ranges.next().unwrap();
+                        start = cmp::min(start, next_removed_range.start);
+                        end = cmp::max(end, next_removed_range.end);
+                    }
+
+                    (start..end, Some(new_ranges.next().unwrap()))
                 }
             }
+            (None, Some(next_new_range)) => (
+                next_new_range.start_byte..next_new_range.end_byte,
+                Some(new_ranges.next().unwrap()),
+            ),
+        };
 
-            if end_ix > start_ix {
-                ranges.splice(start_ix..end_ix, []);
+        let mut start_ix = ranges_ix
+            + match ranges[ranges_ix..].binary_search_by_key(&remove.start, |r| r.end_byte) {
+                Ok(ix) => ix,
+                Err(ix) => ix,
+            };
+        let mut end_ix = ranges_ix
+            + match ranges[ranges_ix..].binary_search_by_key(&remove.end, |r| r.start_byte) {
+                Ok(ix) => ix + 1,
+                Err(ix) => ix,
+            };
+
+        // If there are empty ranges, then there may be multiple ranges with the same
+        // start or end. Expand the splice to include any adjacent ranges that touch
+        // the changed range.
+        while start_ix > 0 {
+            if ranges[start_ix - 1].end_byte == remove.start {
+                start_ix -= 1;
+            } else {
+                break;
+            }
+        }
+        while let Some(range) = ranges.get(end_ix) {
+            if range.start_byte == remove.end {
+                end_ix += 1;
+            } else {
+                break;
             }
-            changed_ranges.next();
-            ranges_ix = start_ix;
-        } else if let Some(new_range) = new_range {
-            let ix = ranges_ix
-                + match ranges[ranges_ix..]
-                    .binary_search_by_key(&new_range.start_byte, |r| r.start_byte)
-                {
-                    Ok(ix) | Err(ix) => ix,
-                };
-            ranges.insert(ix, **new_range);
-            new_ranges.next();
-            ranges_ix = ix + 1;
-        } else {
-            break;
         }
+
+        ranges.splice(start_ix..end_ix, insert);
+        ranges_ix = start_ix;
     }
+
     ranges
 }
 
@@ -1628,3 +1676,46 @@ impl ToTreeSitterPoint for Point {
         Point::new(point.row as u32, point.column as u32)
     }
 }
+
+struct LogIncludedRanges<'a>(&'a [tree_sitter::Range]);
+struct LogPoint(Point);
+struct LogAnchorRange<'a>(&'a Range<Anchor>, &'a text::BufferSnapshot);
+struct LogChangedRegions<'a>(&'a ChangeRegionSet, &'a text::BufferSnapshot);
+
+impl<'a> fmt::Debug for LogIncludedRanges<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_list()
+            .entries(self.0.iter().map(|range| {
+                let start = range.start_point;
+                let end = range.end_point;
+                (start.row, start.column)..(end.row, end.column)
+            }))
+            .finish()
+    }
+}
+
+impl<'a> fmt::Debug for LogAnchorRange<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let range = self.0.to_point(self.1);
+        (LogPoint(range.start)..LogPoint(range.end)).fmt(f)
+    }
+}
+
+impl<'a> fmt::Debug for LogChangedRegions<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_list()
+            .entries(
+                self.0
+                     .0
+                    .iter()
+                    .map(|region| LogAnchorRange(&region.range, self.1)),
+            )
+            .finish()
+    }
+}
+
+impl fmt::Debug for LogPoint {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        (self.0.row, self.0.column).fmt(f)
+    }
+}

crates/language/src/syntax_map/syntax_map_tests.rs πŸ”—

@@ -48,6 +48,13 @@ fn test_splice_included_ranges() {
     let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
     assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]);
 
+    // does not create overlapping ranges
+    let new_ranges = splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
+    assert_eq!(
+        new_ranges,
+        &[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
+    );
+
     fn ts_range(range: Range<usize>) -> tree_sitter::Range {
         tree_sitter::Range {
             start_byte: range.start,
@@ -624,6 +631,26 @@ fn test_combined_injections_splitting_some_injections() {
     );
 }
 
+#[gpui::test]
+fn test_combined_injections_editing_after_last_injection() {
+    test_edit_sequence(
+        "ERB",
+        &[
+            r#"
+                <% foo %>
+                <div></div>
+                <% bar %>
+            "#,
+            r#"
+                <% foo %>
+                <div></div>
+                <% bar %>Β«
+                more textΒ»
+            "#,
+        ],
+    );
+}
+
 #[gpui::test]
 fn test_combined_injections_inside_injections() {
     let (_buffer, _syntax_map) = test_edit_sequence(
@@ -974,13 +1001,16 @@ fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap
     mutated_syntax_map.reparse(language.clone(), &buffer);
 
     for (i, marked_string) in steps.into_iter().enumerate() {
-        buffer.edit_via_marked_text(&marked_string.unindent());
+        let marked_string = marked_string.unindent();
+        log::info!("incremental parse {i}: {marked_string:?}");
+        buffer.edit_via_marked_text(&marked_string);
 
         // Reparse the syntax map
         mutated_syntax_map.interpolate(&buffer);
         mutated_syntax_map.reparse(language.clone(), &buffer);
 
         // Create a second syntax map from scratch
+        log::info!("fresh parse {i}: {marked_string:?}");
         let mut reference_syntax_map = SyntaxMap::new();
         reference_syntax_map.set_language_registry(registry.clone());
         reference_syntax_map.reparse(language.clone(), &buffer);
@@ -1133,6 +1163,7 @@ fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
     start..start + text.len()
 }
 
+#[track_caller]
 fn assert_layers_for_range(
     syntax_map: &SyntaxMap,
     buffer: &BufferSnapshot,

crates/language_selector/src/active_buffer_language.rs πŸ”—

@@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage {
 
             MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
                 let theme = &theme::current(cx).workspace.status_bar;
-                let style = theme.active_language.style_for(state, false);
+                let style = theme.active_language.style_for(state);
                 Label::new(active_language_text, style.text.clone())
                     .contained()
                     .with_style(style.container)

crates/language_selector/src/language_selector.rs πŸ”—

@@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
         let mat = &self.matches[ix];
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
         let mut label = mat.string.clone();
         if buffer_language_name.as_deref() == Some(mat.string.as_str()) {

crates/language_tools/src/lsp_log.rs πŸ”—

@@ -681,7 +681,7 @@ impl LspLogToolbarItemView {
                     )
                 })
                 .unwrap_or_else(|| "No server selected".into());
-            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
+            let style = theme.toolbar_dropdown_menu.header.style_for(state);
             Label::new(label, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -722,7 +722,8 @@ impl LspLogToolbarItemView {
                     let style = theme
                         .toolbar_dropdown_menu
                         .item
-                        .style_for(state, logs_selected);
+                        .in_state(logs_selected)
+                        .style_for(state);
                     Label::new(SERVER_LOGS, style.text.clone())
                         .contained()
                         .with_style(style.container)
@@ -739,7 +740,8 @@ impl LspLogToolbarItemView {
                     let style = theme
                         .toolbar_dropdown_menu
                         .item
-                        .style_for(state, rpc_trace_selected);
+                        .in_state(rpc_trace_selected)
+                        .style_for(state);
                     Flex::row()
                         .with_child(
                             Label::new(RPC_MESSAGES, style.text.clone())

crates/language_tools/src/syntax_tree_view.rs πŸ”—

@@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView {
     ) -> impl Element<Self> {
         enum ToggleMenu {}
         MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
-            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
+            let style = theme.toolbar_dropdown_menu.header.style_for(state);
             Flex::row()
                 .with_child(
                     Label::new(active_layer.language.name().to_string(), style.text.clone())
@@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView {
             let style = theme
                 .toolbar_dropdown_menu
                 .item
-                .style_for(state, is_selected);
+                .in_state(is_selected)
+                .style_for(state);
             Flex::row()
                 .with_child(
                     Label::new(layer.language.name().to_string(), style.text.clone())

crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift πŸ”—

@@ -6,19 +6,31 @@ import ScreenCaptureKit
 class LKRoomDelegate: RoomDelegate {
     var data: UnsafeRawPointer
     var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
+    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
+    var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
+    var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
+    var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
     var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
     var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
-    
+
     init(
         data: UnsafeRawPointer,
         onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
+        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+        onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
+        onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
+        onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
         onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
         onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void)
     {
         self.data = data
         self.onDidDisconnect = onDidDisconnect
+        self.onDidSubscribeToRemoteAudioTrack = onDidSubscribeToRemoteAudioTrack
+        self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack
         self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
         self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
+        self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
+        self.onActiveSpeakersChanged = onActiveSpeakersChanged
     }
 
     func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
@@ -30,12 +42,27 @@ class LKRoomDelegate: RoomDelegate {
     func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
         if track.kind == .video {
             self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
+        } else if track.kind == .audio {
+            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
         }
     }
+
+    func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) {
+        if publication.kind == .audio {
+            self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
+        }
+    }
+    
+    func room(_ room: Room, didUpdate speakers: [Participant]) {
+        guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
+        self.onActiveSpeakersChanged(self.data, speaker_ids)
+    }
     
     func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
         if track.kind == .video {
             self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString)
+        } else if track.kind == .audio {
+            self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)
         }
     }
 }
@@ -77,12 +104,20 @@ class LKVideoRenderer: NSObject, VideoRenderer {
 public func LKRoomDelegateCreate(
     data: UnsafeRawPointer,
     onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
+    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+    onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
+    onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
+    onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
     onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
     onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
 ) -> UnsafeMutableRawPointer {
     let delegate = LKRoomDelegate(
         data: data,
         onDidDisconnect: onDidDisconnect,
+        onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack,
+        onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack,
+        onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
+        onActiveSpeakersChanged: onActiveSpeakerChanged,
         onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
         onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack
     )
@@ -123,6 +158,18 @@ public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPoin
     }
 }
 
+@_cdecl("LKRoomPublishAudioTrack")
+public func LKRoomPublishAudioTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    let track = Unmanaged<LocalAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    room.localParticipant?.publishAudioTrack(track: track).then { publication in
+        callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil)
+    }.catch { error in
+        callback(callback_data, nil, error.localizedDescription as CFString)
+    }
+}
+
+
 @_cdecl("LKRoomUnpublishTrack")
 public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawPointer) {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
@@ -130,6 +177,32 @@ public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawP
     let _ = room.localParticipant?.unpublish(publication: publication)
 }
 
+@_cdecl("LKRoomAudioTracksForRemoteParticipant")
+public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    
+    for (_, participant) in room.remoteParticipants {
+        if participant.identity == participantId as String {
+            return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray?
+        }
+    }
+    
+    return nil;
+}
+
+@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
+public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    
+    for (_, participant) in room.remoteParticipants {
+        if participant.identity == participantId as String {
+            return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
+        }
+    }
+    
+    return nil;
+}
+
 @_cdecl("LKRoomVideoTracksForRemoteParticipant")
 public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
@@ -143,6 +216,17 @@ public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, partic
     return nil;
 }
 
+@_cdecl("LKLocalAudioTrackCreateTrack")
+public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer {
+    let track = LocalAudioTrack.createTrack(options: AudioCaptureOptions(
+      echoCancellation: true,
+      noiseSuppression: true
+    ))
+    
+    return Unmanaged.passRetained(track).toOpaque()
+}
+
+
 @_cdecl("LKCreateScreenShareTrackForDisplay")
 public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
     let display = Unmanaged<MacOSDisplay>.fromOpaque(display).takeUnretainedValue()
@@ -169,6 +253,12 @@ public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString {
     return track.sid! as CFString
 }
 
+@_cdecl("LKRemoteAudioTrackGetSid")
+public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString {
+    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    return track.sid! as CFString
+}
+
 @_cdecl("LKDisplaySources")
 public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) {
     MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in
@@ -177,3 +267,43 @@ public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @conven
         callback(data, nil, error.localizedDescription as CFString)
     }
 }
+
+@_cdecl("LKLocalTrackPublicationSetMute")
+public func LKLocalTrackPublicationSetMute(
+    publication: UnsafeRawPointer,
+    muted: Bool,
+    on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
+    callback_data: UnsafeRawPointer
+) {
+    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+    
+    if muted {
+        publication.mute().then {
+            on_complete(callback_data, nil)
+        }.catch { error in
+            on_complete(callback_data, error.localizedDescription as CFString)
+        }
+    } else {
+        publication.unmute().then {
+            on_complete(callback_data, nil)
+        }.catch { error in
+            on_complete(callback_data, error.localizedDescription as CFString)
+        }
+    }
+}
+
+@_cdecl("LKRemoteTrackPublicationSetEnabled")
+public func LKRemoteTrackPublicationSetEnabled(
+    publication: UnsafeRawPointer,
+    enabled: Bool,
+    on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
+    callback_data: UnsafeRawPointer
+) {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    publication.set(enabled: enabled).then {
+        on_complete(callback_data, nil)
+    }.catch { error in
+        on_complete(callback_data, error.localizedDescription as CFString)
+    }
+}

crates/live_kit_client/examples/test_app.rs πŸ”—

@@ -1,6 +1,10 @@
+use std::time::Duration;
+
 use futures::StreamExt;
 use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem};
-use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
+use live_kit_client::{
+    LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
+};
 use live_kit_server::token::{self, VideoGrant};
 use log::LevelFilter;
 use simplelog::SimpleLogger;
@@ -11,6 +15,12 @@ fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
     gpui::App::new(()).unwrap().run(|cx| {
+        #[cfg(any(test, feature = "test-support"))]
+        println!("USING TEST LIVEKIT");
+
+        #[cfg(not(any(test, feature = "test-support")))]
+        println!("USING REAL LIVEKIT");
+
         cx.platform().activate(true);
         cx.add_global_action(quit);
 
@@ -49,35 +59,107 @@ fn main() {
             let room_b = Room::new();
             room_b.connect(&live_kit_url, &user2_token).await.unwrap();
 
-            let mut track_changes = room_b.remote_video_track_updates();
+            let mut audio_track_updates = room_b.remote_audio_track_updates();
+            let audio_track = LocalAudioTrack::create();
+            let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap();
+
+            if let RemoteAudioTrackUpdate::Subscribed(track) =
+                audio_track_updates.next().await.unwrap()
+            {
+                let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
+                assert_eq!(remote_tracks.len(), 1);
+                assert_eq!(remote_tracks[0].publisher_id(), "test-participant-1");
+                assert_eq!(track.publisher_id(), "test-participant-1");
+            } else {
+                panic!("unexpected message");
+            }
+
+            audio_track_publication.set_mute(true).await.unwrap();
+
+            println!("waiting for mute changed!");
+            if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
+                audio_track_updates.next().await.unwrap()
+            {
+                let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
+                assert_eq!(remote_tracks[0].sid(), track_id);
+                assert_eq!(muted, true);
+            } else {
+                panic!("unexpected message");
+            }
+
+            audio_track_publication.set_mute(false).await.unwrap();
+
+            if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
+                audio_track_updates.next().await.unwrap()
+            {
+                let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
+                assert_eq!(remote_tracks[0].sid(), track_id);
+                assert_eq!(muted, false);
+            } else {
+                panic!("unexpected message");
+            }
+
+            println!("Pausing for 5 seconds to test audio, make some noise!");
+            let timer = cx.background().timer(Duration::from_secs(5));
+            timer.await;
+            let remote_audio_track = room_b
+                .remote_audio_tracks("test-participant-1")
+                .pop()
+                .unwrap();
+            room_a.unpublish_track(audio_track_publication);
+
+            // Clear out any active speakers changed messages
+            let mut next = audio_track_updates.next().await.unwrap();
+            while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next {
+                println!("Speakers changed: {:?}", speakers);
+                next = audio_track_updates.next().await.unwrap();
+            }
+
+            if let RemoteAudioTrackUpdate::Unsubscribed {
+                publisher_id,
+                track_id,
+            } = next
+            {
+                assert_eq!(publisher_id, "test-participant-1");
+                assert_eq!(remote_audio_track.sid(), track_id);
+                assert_eq!(room_b.remote_audio_tracks("test-participant-1").len(), 0);
+            } else {
+                panic!("unexpected message");
+            }
 
+            let mut video_track_updates = room_b.remote_video_track_updates();
             let displays = room_a.display_sources().await.unwrap();
             let display = displays.into_iter().next().unwrap();
 
-            let track_a = LocalVideoTrack::screen_share_for_display(&display);
-            let track_a_publication = room_a.publish_video_track(&track_a).await.unwrap();
+            let local_video_track = LocalVideoTrack::screen_share_for_display(&display);
+            let local_video_track_publication = room_a
+                .publish_video_track(&local_video_track)
+                .await
+                .unwrap();
 
-            if let RemoteVideoTrackUpdate::Subscribed(track) = track_changes.next().await.unwrap() {
-                let remote_tracks = room_b.remote_video_tracks("test-participant-1");
-                assert_eq!(remote_tracks.len(), 1);
-                assert_eq!(remote_tracks[0].publisher_id(), "test-participant-1");
+            if let RemoteVideoTrackUpdate::Subscribed(track) =
+                video_track_updates.next().await.unwrap()
+            {
+                let remote_video_tracks = room_b.remote_video_tracks("test-participant-1");
+                assert_eq!(remote_video_tracks.len(), 1);
+                assert_eq!(remote_video_tracks[0].publisher_id(), "test-participant-1");
                 assert_eq!(track.publisher_id(), "test-participant-1");
             } else {
                 panic!("unexpected message");
             }
 
-            let remote_track = room_b
+            let remote_video_track = room_b
                 .remote_video_tracks("test-participant-1")
                 .pop()
                 .unwrap();
-            room_a.unpublish_track(track_a_publication);
+            room_a.unpublish_track(local_video_track_publication);
             if let RemoteVideoTrackUpdate::Unsubscribed {
                 publisher_id,
                 track_id,
-            } = track_changes.next().await.unwrap()
+            } = video_track_updates.next().await.unwrap()
             {
                 assert_eq!(publisher_id, "test-participant-1");
-                assert_eq!(remote_track.sid(), track_id);
+                assert_eq!(remote_video_track.sid(), track_id);
                 assert_eq!(room_b.remote_video_tracks("test-participant-1").len(), 0);
             } else {
                 panic!("unexpected message");

crates/live_kit_client/src/prod.rs πŸ”—

@@ -21,6 +21,26 @@ extern "C" {
     fn LKRoomDelegateCreate(
         callback_data: *mut c_void,
         on_did_disconnect: extern "C" fn(callback_data: *mut c_void),
+        on_did_subscribe_to_remote_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publisher_id: CFStringRef,
+            track_id: CFStringRef,
+            remote_track: *const c_void,
+        ),
+        on_did_unsubscribe_from_remote_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publisher_id: CFStringRef,
+            track_id: CFStringRef,
+        ),
+        on_mute_changed_from_remote_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            track_id: CFStringRef,
+            muted: bool,
+        ),
+        on_active_speakers_changed: extern "C" fn(
+            callback_data: *mut c_void,
+            participants: CFArrayRef,
+        ),
         on_did_subscribe_to_remote_video_track: extern "C" fn(
             callback_data: *mut c_void,
             publisher_id: CFStringRef,
@@ -49,7 +69,23 @@ extern "C" {
         callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef),
         callback_data: *mut c_void,
     );
+    fn LKRoomPublishAudioTrack(
+        room: *const c_void,
+        track: *const c_void,
+        callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef),
+        callback_data: *mut c_void,
+    );
     fn LKRoomUnpublishTrack(room: *const c_void, publication: *const c_void);
+    fn LKRoomAudioTracksForRemoteParticipant(
+        room: *const c_void,
+        participant_id: CFStringRef,
+    ) -> CFArrayRef;
+
+    fn LKRoomAudioTrackPublicationsForRemoteParticipant(
+        room: *const c_void,
+        participant_id: CFStringRef,
+    ) -> CFArrayRef;
+
     fn LKRoomVideoTracksForRemoteParticipant(
         room: *const c_void,
         participant_id: CFStringRef,
@@ -61,6 +97,7 @@ extern "C" {
         on_drop: extern "C" fn(callback_data: *mut c_void),
     ) -> *const c_void;
 
+    fn LKRemoteAudioTrackGetSid(track: *const c_void) -> CFStringRef;
     fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
     fn LKRemoteVideoTrackGetSid(track: *const c_void) -> CFStringRef;
 
@@ -73,6 +110,21 @@ extern "C" {
         ),
     );
     fn LKCreateScreenShareTrackForDisplay(display: *const c_void) -> *const c_void;
+    fn LKLocalAudioTrackCreateTrack() -> *const c_void;
+
+    fn LKLocalTrackPublicationSetMute(
+        publication: *const c_void,
+        muted: bool,
+        on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
+        callback_data: *mut c_void,
+    );
+
+    fn LKRemoteTrackPublicationSetEnabled(
+        publication: *const c_void,
+        enabled: bool,
+        on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
+        callback_data: *mut c_void,
+    );
 }
 
 pub type Sid = String;
@@ -89,6 +141,7 @@ pub struct Room {
         watch::Sender<ConnectionState>,
         watch::Receiver<ConnectionState>,
     )>,
+    remote_audio_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteAudioTrackUpdate>>>,
     remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
     _delegate: RoomDelegate,
 }
@@ -100,6 +153,7 @@ impl Room {
             Self {
                 native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
                 connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
+                remote_audio_track_subscribers: Default::default(),
                 remote_video_track_subscribers: Default::default(),
                 _delegate: delegate,
             }
@@ -174,7 +228,7 @@ impl Room {
             let tx =
                 unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
             if error.is_null() {
-                let _ = tx.send(Ok(LocalTrackPublication(publication)));
+                let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
             } else {
                 let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
                 let _ = tx.send(Err(anyhow!(error)));
@@ -191,6 +245,32 @@ impl Room {
         async { rx.await.unwrap().context("error publishing video track") }
     }
 
+    pub fn publish_audio_track(
+        self: &Arc<Self>,
+        track: &LocalAudioTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
+        extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) {
+            let tx =
+                unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
+            if error.is_null() {
+                let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                let _ = tx.send(Err(anyhow!(error)));
+            }
+        }
+        unsafe {
+            LKRoomPublishAudioTrack(
+                self.native_room,
+                track.0,
+                callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            );
+        }
+        async { rx.await.unwrap().context("error publishing audio track") }
+    }
+
     pub fn unpublish_track(&self, publication: LocalTrackPublication) {
         unsafe {
             LKRoomUnpublishTrack(self.native_room, publication.0);
@@ -226,12 +306,112 @@ impl Room {
         }
     }
 
+    pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
+        unsafe {
+            let tracks = LKRoomAudioTracksForRemoteParticipant(
+                self.native_room,
+                CFString::new(participant_id).as_concrete_TypeRef(),
+            );
+
+            if tracks.is_null() {
+                Vec::new()
+            } else {
+                let tracks = CFArray::wrap_under_get_rule(tracks);
+                tracks
+                    .into_iter()
+                    .map(|native_track| {
+                        let native_track = *native_track;
+                        let id =
+                            CFString::wrap_under_get_rule(LKRemoteAudioTrackGetSid(native_track))
+                                .to_string();
+                        Arc::new(RemoteAudioTrack::new(
+                            native_track,
+                            id,
+                            participant_id.into(),
+                        ))
+                    })
+                    .collect()
+            }
+        }
+    }
+
+    pub fn remote_audio_track_publications(
+        &self,
+        participant_id: &str,
+    ) -> Vec<Arc<RemoteTrackPublication>> {
+        unsafe {
+            let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
+                self.native_room,
+                CFString::new(participant_id).as_concrete_TypeRef(),
+            );
+
+            if tracks.is_null() {
+                Vec::new()
+            } else {
+                let tracks = CFArray::wrap_under_get_rule(tracks);
+                tracks
+                    .into_iter()
+                    .map(|native_track_publication| {
+                        let native_track_publication = *native_track_publication;
+                        Arc::new(RemoteTrackPublication::new(native_track_publication))
+                    })
+                    .collect()
+            }
+        }
+    }
+
+    pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteAudioTrackUpdate> {
+        let (tx, rx) = mpsc::unbounded();
+        self.remote_audio_track_subscribers.lock().push(tx);
+        rx
+    }
+
     pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteVideoTrackUpdate> {
         let (tx, rx) = mpsc::unbounded();
         self.remote_video_track_subscribers.lock().push(tx);
         rx
     }
 
+    fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) {
+        let track = Arc::new(track);
+        self.remote_audio_track_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone()))
+                .is_ok()
+        });
+    }
+
+    fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) {
+        self.remote_audio_track_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed {
+                publisher_id: publisher_id.clone(),
+                track_id: track_id.clone(),
+            })
+            .is_ok()
+        });
+    }
+
+    fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {
+        self.remote_audio_track_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged {
+                track_id: track_id.clone(),
+                muted,
+            })
+            .is_ok()
+        });
+    }
+
+    // A vec of publisher IDs
+    fn active_speakers_changed(&self, speakers: Vec<String>) {
+        self.remote_audio_track_subscribers
+            .lock()
+            .retain(move |tx| {
+                tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged {
+                    speakers: speakers.clone(),
+                })
+                .is_ok()
+            });
+    }
+
     fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
         let track = Arc::new(track);
         self.remote_video_track_subscribers.lock().retain(|tx| {
@@ -294,6 +474,10 @@ impl RoomDelegate {
             LKRoomDelegateCreate(
                 weak_room as *mut c_void,
                 Self::on_did_disconnect,
+                Self::on_did_subscribe_to_remote_audio_track,
+                Self::on_did_unsubscribe_from_remote_audio_track,
+                Self::on_mute_change_from_remote_audio_track,
+                Self::on_active_speakers_changed,
                 Self::on_did_subscribe_to_remote_video_track,
                 Self::on_did_unsubscribe_from_remote_video_track,
             )
@@ -312,6 +496,72 @@ impl RoomDelegate {
         let _ = Weak::into_raw(room);
     }
 
+    extern "C" fn on_did_subscribe_to_remote_audio_track(
+        room: *mut c_void,
+        publisher_id: CFStringRef,
+        track_id: CFStringRef,
+        track: *const c_void,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        let track = RemoteAudioTrack::new(track, track_id, publisher_id);
+        if let Some(room) = room.upgrade() {
+            room.did_subscribe_to_remote_audio_track(track);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_unsubscribe_from_remote_audio_track(
+        room: *mut c_void,
+        publisher_id: CFStringRef,
+        track_id: CFStringRef,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        if let Some(room) = room.upgrade() {
+            room.did_unsubscribe_from_remote_audio_track(publisher_id, track_id);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_mute_change_from_remote_audio_track(
+        room: *mut c_void,
+        track_id: CFStringRef,
+        muted: bool,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        if let Some(room) = room.upgrade() {
+            room.mute_changed_from_remote_audio_track(track_id, muted);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_active_speakers_changed(room: *mut c_void, participants: CFArrayRef) {
+        if participants.is_null() {
+            return;
+        }
+
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let speakers = unsafe {
+            CFArray::wrap_under_get_rule(participants)
+                .into_iter()
+                .map(
+                    |speaker: core_foundation::base::ItemRef<'_, *const c_void>| {
+                        CFString::wrap_under_get_rule(*speaker as CFStringRef).to_string()
+                    },
+                )
+                .collect()
+        };
+
+        if let Some(room) = room.upgrade() {
+            room.active_speakers_changed(speakers);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
     extern "C" fn on_did_subscribe_to_remote_video_track(
         room: *mut c_void,
         publisher_id: CFStringRef,
@@ -352,6 +602,20 @@ impl Drop for RoomDelegate {
     }
 }
 
+pub struct LocalAudioTrack(*const c_void);
+
+impl LocalAudioTrack {
+    pub fn create() -> Self {
+        Self(unsafe { LKLocalAudioTrackCreateTrack() })
+    }
+}
+
+impl Drop for LocalAudioTrack {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0) }
+    }
+}
+
 pub struct LocalVideoTrack(*const c_void);
 
 impl LocalVideoTrack {
@@ -368,12 +632,124 @@ impl Drop for LocalVideoTrack {
 
 pub struct LocalTrackPublication(*const c_void);
 
+impl LocalTrackPublication {
+    pub fn new(native_track_publication: *const c_void) -> Self {
+        unsafe {
+            CFRetain(native_track_publication);
+        }
+        Self(native_track_publication)
+    }
+
+    pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+
+        extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
+            let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
+            if error.is_null() {
+                tx.send(Ok(())).ok();
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                tx.send(Err(anyhow!(error))).ok();
+            }
+        }
+
+        unsafe {
+            LKLocalTrackPublicationSetMute(
+                self.0,
+                muted,
+                complete_callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            )
+        }
+
+        async move { rx.await.unwrap() }
+    }
+}
+
 impl Drop for LocalTrackPublication {
     fn drop(&mut self) {
         unsafe { CFRelease(self.0) }
     }
 }
 
+pub struct RemoteTrackPublication(*const c_void);
+
+impl RemoteTrackPublication {
+    pub fn new(native_track_publication: *const c_void) -> Self {
+        unsafe {
+            CFRetain(native_track_publication);
+        }
+        Self(native_track_publication)
+    }
+
+    pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+
+        extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
+            let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
+            if error.is_null() {
+                tx.send(Ok(())).ok();
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                tx.send(Err(anyhow!(error))).ok();
+            }
+        }
+
+        unsafe {
+            LKRemoteTrackPublicationSetEnabled(
+                self.0,
+                enabled,
+                complete_callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            )
+        }
+
+        async move { rx.await.unwrap() }
+    }
+}
+
+impl Drop for RemoteTrackPublication {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0) }
+    }
+}
+
+#[derive(Debug)]
+pub struct RemoteAudioTrack {
+    _native_track: *const c_void,
+    sid: Sid,
+    publisher_id: String,
+}
+
+impl RemoteAudioTrack {
+    fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
+        unsafe {
+            CFRetain(native_track);
+        }
+        Self {
+            _native_track: native_track,
+            sid,
+            publisher_id,
+        }
+    }
+
+    pub fn sid(&self) -> &str {
+        &self.sid
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.publisher_id
+    }
+
+    pub fn enable(&self) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+
+    pub fn disable(&self) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+}
+
 #[derive(Debug)]
 pub struct RemoteVideoTrack {
     native_track: *const c_void,
@@ -453,6 +829,13 @@ pub enum RemoteVideoTrackUpdate {
     Unsubscribed { publisher_id: Sid, track_id: Sid },
 }
 
+pub enum RemoteAudioTrackUpdate {
+    ActiveSpeakersChanged { speakers: Vec<Sid> },
+    MuteChanged { track_id: Sid, muted: bool },
+    Subscribed(Arc<RemoteAudioTrack>),
+    Unsubscribed { publisher_id: Sid, track_id: Sid },
+}
+
 pub struct MacOSDisplay(*const c_void);
 
 impl MacOSDisplay {

crates/live_kit_client/src/test.rs πŸ”—

@@ -67,7 +67,7 @@ impl TestServer {
         }
     }
 
-    async fn create_room(&self, room: String) -> Result<()> {
+    pub async fn create_room(&self, room: String) -> Result<()> {
         self.background.simulate_random_delay().await;
         let mut server_rooms = self.rooms.lock();
         if server_rooms.contains_key(&room) {
@@ -104,7 +104,7 @@ impl TestServer {
                 room_name
             ))
         } else {
-            for track in &room.tracks {
+            for track in &room.video_tracks {
                 client_room
                     .0
                     .lock()
@@ -182,7 +182,7 @@ impl TestServer {
             frames_rx: local_track.frames_rx.clone(),
         });
 
-        room.tracks.push(track.clone());
+        room.video_tracks.push(track.clone());
 
         for (id, client_room) in &room.client_rooms {
             if *id != identity {
@@ -199,6 +199,43 @@ impl TestServer {
         Ok(())
     }
 
+    async fn publish_audio_track(
+        &self,
+        token: String,
+        _local_track: &LocalAudioTrack,
+    ) -> Result<()> {
+        self.background.simulate_random_delay().await;
+        let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+        let identity = claims.sub.unwrap().to_string();
+        let room_name = claims.video.room.unwrap();
+
+        let mut server_rooms = self.rooms.lock();
+        let room = server_rooms
+            .get_mut(&*room_name)
+            .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
+
+        let track = Arc::new(RemoteAudioTrack {
+            sid: nanoid::nanoid!(17),
+            publisher_id: identity.clone(),
+        });
+
+        room.audio_tracks.push(track.clone());
+
+        for (id, client_room) in &room.client_rooms {
+            if *id != identity {
+                let _ = client_room
+                    .0
+                    .lock()
+                    .audio_track_updates
+                    .0
+                    .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone()))
+                    .unwrap();
+            }
+        }
+
+        Ok(())
+    }
+
     fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
@@ -207,14 +244,26 @@ impl TestServer {
         let room = server_rooms
             .get_mut(&*room_name)
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
-        Ok(room.tracks.clone())
+        Ok(room.video_tracks.clone())
+    }
+
+    fn audio_tracks(&self, token: String) -> Result<Vec<Arc<RemoteAudioTrack>>> {
+        let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+        let room_name = claims.video.room.unwrap();
+
+        let mut server_rooms = self.rooms.lock();
+        let room = server_rooms
+            .get_mut(&*room_name)
+            .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
+        Ok(room.audio_tracks.clone())
     }
 }
 
 #[derive(Default)]
 struct TestServerRoom {
     client_rooms: HashMap<Sid, Arc<Room>>,
-    tracks: Vec<Arc<RemoteVideoTrack>>,
+    video_tracks: Vec<Arc<RemoteVideoTrack>>,
+    audio_tracks: Vec<Arc<RemoteAudioTrack>>,
 }
 
 impl TestServerRoom {}
@@ -266,6 +315,10 @@ struct RoomState {
         watch::Receiver<ConnectionState>,
     ),
     display_sources: Vec<MacOSDisplay>,
+    audio_track_updates: (
+        async_broadcast::Sender<RemoteAudioTrackUpdate>,
+        async_broadcast::Receiver<RemoteAudioTrackUpdate>,
+    ),
     video_track_updates: (
         async_broadcast::Sender<RemoteVideoTrackUpdate>,
         async_broadcast::Receiver<RemoteVideoTrackUpdate>,
@@ -286,6 +339,7 @@ impl Room {
             connection: watch::channel_with(ConnectionState::Disconnected),
             display_sources: Default::default(),
             video_track_updates: async_broadcast::broadcast(128),
+            audio_track_updates: async_broadcast::broadcast(128),
         })))
     }
 
@@ -327,8 +381,51 @@ impl Room {
             Ok(LocalTrackPublication)
         }
     }
+    pub fn publish_audio_track(
+        self: &Arc<Self>,
+        track: &LocalAudioTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let this = self.clone();
+        let track = track.clone();
+        async move {
+            this.test_server()
+                .publish_audio_track(this.token(), &track)
+                .await?;
+            Ok(LocalTrackPublication)
+        }
+    }
+
+    pub fn unpublish_track(&self, _publication: LocalTrackPublication) {}
 
-    pub fn unpublish_track(&self, _: LocalTrackPublication) {}
+    pub fn remote_audio_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
+        if !self.is_connected() {
+            return Vec::new();
+        }
+
+        self.test_server()
+            .audio_tracks(self.token())
+            .unwrap()
+            .into_iter()
+            .filter(|track| track.publisher_id() == publisher_id)
+            .collect()
+    }
+
+    pub fn remote_audio_track_publications(
+        &self,
+        publisher_id: &str,
+    ) -> Vec<Arc<RemoteTrackPublication>> {
+        if !self.is_connected() {
+            return Vec::new();
+        }
+
+        self.test_server()
+            .audio_tracks(self.token())
+            .unwrap()
+            .into_iter()
+            .filter(|track| track.publisher_id() == publisher_id)
+            .map(|_track| Arc::new(RemoteTrackPublication {}))
+            .collect()
+    }
 
     pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
         if !self.is_connected() {
@@ -343,6 +440,10 @@ impl Room {
             .collect()
     }
 
+    pub fn remote_audio_track_updates(&self) -> impl Stream<Item = RemoteAudioTrackUpdate> {
+        self.0.lock().audio_track_updates.1.clone()
+    }
+
     pub fn remote_video_track_updates(&self) -> impl Stream<Item = RemoteVideoTrackUpdate> {
         self.0.lock().video_track_updates.1.clone()
     }
@@ -391,6 +492,20 @@ impl Drop for Room {
 
 pub struct LocalTrackPublication;
 
+impl LocalTrackPublication {
+    pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+}
+
+pub struct RemoteTrackPublication;
+
+impl RemoteTrackPublication {
+    pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+}
+
 #[derive(Clone)]
 pub struct LocalVideoTrack {
     frames_rx: async_broadcast::Receiver<Frame>,
@@ -404,6 +519,15 @@ impl LocalVideoTrack {
     }
 }
 
+#[derive(Clone)]
+pub struct LocalAudioTrack;
+
+impl LocalAudioTrack {
+    pub fn create() -> Self {
+        Self
+    }
+}
+
 pub struct RemoteVideoTrack {
     sid: Sid,
     publisher_id: Sid,
@@ -424,12 +548,44 @@ impl RemoteVideoTrack {
     }
 }
 
+#[derive(Debug)]
+pub struct RemoteAudioTrack {
+    sid: Sid,
+    publisher_id: Sid,
+}
+
+impl RemoteAudioTrack {
+    pub fn sid(&self) -> &str {
+        &self.sid
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.publisher_id
+    }
+
+    pub fn enable(&self) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+
+    pub fn disable(&self) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+}
+
 #[derive(Clone)]
 pub enum RemoteVideoTrackUpdate {
     Subscribed(Arc<RemoteVideoTrack>),
     Unsubscribed { publisher_id: Sid, track_id: Sid },
 }
 
+#[derive(Clone)]
+pub enum RemoteAudioTrackUpdate {
+    ActiveSpeakersChanged { speakers: Vec<Sid> },
+    MuteChanged { track_id: Sid, muted: bool },
+    Subscribed(Arc<RemoteAudioTrack>),
+    Unsubscribed { publisher_id: Sid, track_id: Sid },
+}
+
 #[derive(Clone)]
 pub struct MacOSDisplay {
     frames: (

crates/lsp/src/lsp.rs πŸ”—

@@ -16,6 +16,7 @@ use smol::{
     process::{self, Child},
 };
 use std::{
+    ffi::OsString,
     fmt,
     future::Future,
     io::Write,
@@ -33,9 +34,15 @@ const JSON_RPC_VERSION: &str = "2.0";
 const CONTENT_LEN_HEADER: &str = "Content-Length: ";
 
 type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
-type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
+type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
 type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
 
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageServerBinary {
+    pub path: PathBuf,
+    pub arguments: Vec<OsString>,
+}
+
 pub struct LanguageServer {
     server_id: LanguageServerId,
     next_id: AtomicUsize,
@@ -51,7 +58,7 @@ pub struct LanguageServer {
     io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
     output_done_rx: Mutex<Option<barrier::Receiver>>,
     root_path: PathBuf,
-    _server: Option<Child>,
+    _server: Option<Mutex<Child>>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -103,14 +110,14 @@ struct Notification<'a, T> {
     params: T,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Clone, Deserialize)]
 struct AnyNotification<'a> {
     #[serde(default)]
     id: Option<usize>,
     #[serde(borrow)]
     method: &'a str,
-    #[serde(borrow)]
-    params: &'a RawValue,
+    #[serde(borrow, default)]
+    params: Option<&'a RawValue>,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -119,10 +126,9 @@ struct Error {
 }
 
 impl LanguageServer {
-    pub fn new<T: AsRef<std::ffi::OsStr>>(
+    pub fn new(
         server_id: LanguageServerId,
-        binary_path: &Path,
-        arguments: &[T],
+        binary: LanguageServerBinary,
         root_path: &Path,
         code_action_kinds: Option<Vec<CodeActionKind>>,
         cx: AsyncAppContext,
@@ -133,9 +139,9 @@ impl LanguageServer {
             root_path.parent().unwrap_or_else(|| Path::new("/"))
         };
 
-        let mut server = process::Command::new(binary_path)
+        let mut server = process::Command::new(&binary.path)
             .current_dir(working_dir)
-            .args(arguments)
+            .args(binary.arguments)
             .stdin(Stdio::piped())
             .stdout(Stdio::piped())
             .stderr(Stdio::inherit())
@@ -157,16 +163,20 @@ impl LanguageServer {
                     "unhandled notification {}:\n{}",
                     notification.method,
                     serde_json::to_string_pretty(
-                        &Value::from_str(notification.params.get()).unwrap()
+                        &notification
+                            .params
+                            .and_then(|params| Value::from_str(params.get()).ok())
+                            .unwrap_or(Value::Null)
                     )
-                    .unwrap()
+                    .unwrap(),
                 );
             },
         );
 
-        if let Some(name) = binary_path.file_name() {
+        if let Some(name) = binary.path.file_name() {
             server.name = name.to_string_lossy().to_string();
         }
+
         Ok(server)
     }
 
@@ -228,7 +238,7 @@ impl LanguageServer {
             io_tasks: Mutex::new(Some((input_task, output_task))),
             output_done_rx: Mutex::new(Some(output_done_rx)),
             root_path: root_path.to_path_buf(),
-            _server: server,
+            _server: server.map(|server| Mutex::new(server)),
         }
     }
 
@@ -279,7 +289,11 @@ impl LanguageServer {
 
             if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
                 if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
-                    handler(msg.id, msg.params.get(), cx.clone());
+                    handler(
+                        msg.id,
+                        &msg.params.map(|params| params.get()).unwrap_or("null"),
+                        cx.clone(),
+                    );
                 } else {
                     on_unhandled_notification(msg);
                 }
@@ -295,9 +309,9 @@ impl LanguageServer {
                     if let Some(error) = error {
                         handler(Err(error));
                     } else if let Some(result) = result {
-                        handler(Ok(result.get()));
+                        handler(Ok(result.get().into()));
                     } else {
-                        handler(Ok("null"));
+                        handler(Ok("null".into()));
                     }
                 }
             } else {
@@ -374,6 +388,9 @@ impl LanguageServer {
                         resolve_support: None,
                         ..WorkspaceSymbolClientCapabilities::default()
                     }),
+                    inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
+                        refresh_support: Some(true),
+                    }),
                     ..Default::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
@@ -415,6 +432,10 @@ impl LanguageServer {
                         content_format: Some(vec![MarkupKind::Markdown]),
                         ..Default::default()
                     }),
+                    inlay_hint: Some(InlayHintClientCapabilities {
+                        resolve_support: None,
+                        dynamic_registration: Some(false),
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({
@@ -450,11 +471,13 @@ impl LanguageServer {
             let response_handlers = self.response_handlers.clone();
             let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
             let outbound_tx = self.outbound_tx.clone();
+            let executor = self.executor.clone();
             let mut output_done = self.output_done_rx.lock().take().unwrap();
             let shutdown_request = Self::request_internal::<request::Shutdown>(
                 &next_id,
                 &response_handlers,
                 &outbound_tx,
+                &executor,
                 (),
             );
             let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ());
@@ -591,6 +614,7 @@ impl LanguageServer {
                                 })
                                 .detach();
                         }
+
                         Err(error) => {
                             log::error!(
                                 "error deserializing {} request: {:?}, message: {:?}",
@@ -651,6 +675,7 @@ impl LanguageServer {
             &self.next_id,
             &self.response_handlers,
             &self.outbound_tx,
+            &self.executor,
             params,
         )
     }
@@ -659,6 +684,7 @@ impl LanguageServer {
         next_id: &AtomicUsize,
         response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
         outbound_tx: &channel::Sender<String>,
+        executor: &Arc<executor::Background>,
         params: T::Params,
     ) -> impl 'static + Future<Output = Result<T::Result>>
     where
@@ -679,15 +705,20 @@ impl LanguageServer {
             .as_mut()
             .ok_or_else(|| anyhow!("server shut down"))
             .map(|handlers| {
+                let executor = executor.clone();
                 handlers.insert(
                     id,
                     Box::new(move |result| {
-                        let response = match result {
-                            Ok(response) => serde_json::from_str(response)
-                                .context("failed to deserialize response"),
-                            Err(error) => Err(anyhow!("{}", error.message)),
-                        };
-                        let _ = tx.send(response);
+                        executor
+                            .spawn(async move {
+                                let response = match result {
+                                    Ok(response) => serde_json::from_str(&response)
+                                        .context("failed to deserialize response"),
+                                    Err(error) => Err(anyhow!("{}", error.message)),
+                                };
+                                _ = tx.send(response);
+                            })
+                            .detach();
                     }),
                 );
             });
@@ -828,7 +859,13 @@ impl LanguageServer {
                 cx,
                 move |msg| {
                     notifications_tx
-                        .try_send((msg.method.to_string(), msg.params.get().to_string()))
+                        .try_send((
+                            msg.method.to_string(),
+                            msg.params
+                                .map(|raw_value| raw_value.get())
+                                .unwrap_or("null")
+                                .to_string(),
+                        ))
                         .ok();
                 },
             )),

crates/node_runtime/src/node_runtime.rs πŸ”—

@@ -1,21 +1,24 @@
 use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
+use futures::lock::Mutex;
 use futures::{future::Shared, FutureExt};
 use gpui::{executor::Background, Task};
-use parking_lot::Mutex;
 use serde::Deserialize;
 use smol::{fs, io::BufReader, process::Command};
+use std::process::Output;
 use std::{
     env::consts,
     path::{Path, PathBuf},
-    sync::Arc,
+    sync::{Arc, OnceLock},
 };
-use util::http::HttpClient;
+use util::{http::HttpClient, ResultExt};
 
 const VERSION: &str = "v18.15.0";
 
-#[derive(Deserialize)]
+static RUNTIME_INSTANCE: OnceLock<Arc<NodeRuntime>> = OnceLock::new();
+
+#[derive(Debug, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 pub struct NpmInfo {
     #[serde(default)]
@@ -23,7 +26,7 @@ pub struct NpmInfo {
     versions: Vec<String>,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Debug, Deserialize, Default)]
 pub struct NpmInfoDistTags {
     latest: Option<String>,
 }
@@ -35,12 +38,16 @@ pub struct NodeRuntime {
 }
 
 impl NodeRuntime {
-    pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
-        Arc::new(NodeRuntime {
-            http,
-            background,
-            installation_path: Mutex::new(None),
-        })
+    pub fn instance(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
+        RUNTIME_INSTANCE
+            .get_or_init(|| {
+                Arc::new(NodeRuntime {
+                    http,
+                    background,
+                    installation_path: Mutex::new(None),
+                })
+            })
+            .clone()
     }
 
     pub async fn binary_path(&self) -> Result<PathBuf> {
@@ -50,55 +57,74 @@ impl NodeRuntime {
 
     pub async fn run_npm_subcommand(
         &self,
-        directory: &Path,
+        directory: Option<&Path>,
         subcommand: &str,
         args: &[&str],
-    ) -> Result<()> {
+    ) -> Result<Output> {
+        let attempt = |installation_path: PathBuf| async move {
+            let node_binary = installation_path.join("bin/node");
+            let npm_file = installation_path.join("bin/npm");
+
+            if smol::fs::metadata(&node_binary).await.is_err() {
+                return Err(anyhow!("missing node binary file"));
+            }
+
+            if smol::fs::metadata(&npm_file).await.is_err() {
+                return Err(anyhow!("missing npm file"));
+            }
+
+            let mut command = Command::new(node_binary);
+            command.arg(npm_file).arg(subcommand).args(args);
+
+            if let Some(directory) = directory {
+                command.current_dir(directory);
+            }
+
+            command.output().await.map_err(|e| anyhow!("{e}"))
+        };
+
         let installation_path = self.install_if_needed().await?;
-        let node_binary = installation_path.join("bin/node");
-        let npm_file = installation_path.join("bin/npm");
-
-        let output = Command::new(node_binary)
-            .arg(npm_file)
-            .arg(subcommand)
-            .args(args)
-            .current_dir(directory)
-            .output()
-            .await?;
+        let mut output = attempt(installation_path).await;
+        if output.is_err() {
+            let installation_path = self.reinstall().await?;
+            output = attempt(installation_path).await;
+            if output.is_err() {
+                return Err(anyhow!(
+                    "failed to launch npm subcommand {subcommand} subcommand"
+                ));
+            }
+        }
 
-        if !output.status.success() {
-            return Err(anyhow!(
-                "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
-                String::from_utf8_lossy(&output.stdout),
-                String::from_utf8_lossy(&output.stderr)
-            ));
+        if let Ok(output) = &output {
+            if !output.status.success() {
+                return Err(anyhow!(
+                    "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
+                    String::from_utf8_lossy(&output.stdout),
+                    String::from_utf8_lossy(&output.stderr)
+                ));
+            }
         }
 
-        Ok(())
+        output.map_err(|e| anyhow!("{e}"))
     }
 
     pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
-        let installation_path = self.install_if_needed().await?;
-        let node_binary = installation_path.join("bin/node");
-        let npm_file = installation_path.join("bin/npm");
-
-        let output = Command::new(node_binary)
-            .arg(npm_file)
-            .args(["-fetch-retry-mintimeout", "2000"])
-            .args(["-fetch-retry-maxtimeout", "5000"])
-            .args(["-fetch-timeout", "5000"])
-            .args(["info", name, "--json"])
-            .output()
-            .await
-            .context("failed to run npm info")?;
-
-        if !output.status.success() {
-            return Err(anyhow!(
-                "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
-                String::from_utf8_lossy(&output.stdout),
-                String::from_utf8_lossy(&output.stderr)
-            ));
-        }
+        let output = self
+            .run_npm_subcommand(
+                None,
+                "info",
+                &[
+                    name,
+                    "--json",
+                    "-fetch-retry-mintimeout",
+                    "2000",
+                    "-fetch-retry-maxtimeout",
+                    "5000",
+                    "-fetch-timeout",
+                    "5000",
+                ],
+            )
+            .await?;
 
         let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
         info.dist_tags
@@ -112,41 +138,54 @@ impl NodeRuntime {
         directory: &Path,
         packages: impl IntoIterator<Item = (&str, &str)>,
     ) -> Result<()> {
-        let installation_path = self.install_if_needed().await?;
-        let node_binary = installation_path.join("bin/node");
-        let npm_file = installation_path.join("bin/npm");
-
-        let output = Command::new(node_binary)
-            .arg(npm_file)
-            .args(["-fetch-retry-mintimeout", "2000"])
-            .args(["-fetch-retry-maxtimeout", "5000"])
-            .args(["-fetch-timeout", "5000"])
-            .arg("install")
-            .arg("--prefix")
-            .arg(directory)
-            .args(
-                packages
-                    .into_iter()
-                    .map(|(name, version)| format!("{name}@{version}")),
-            )
-            .output()
-            .await
-            .context("failed to run npm install")?;
-
-        if !output.status.success() {
-            return Err(anyhow!(
-                "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
-                String::from_utf8_lossy(&output.stdout),
-                String::from_utf8_lossy(&output.stderr)
-            ));
-        }
+        let packages: Vec<_> = packages
+            .into_iter()
+            .map(|(name, version)| format!("{name}@{version}"))
+            .collect();
+
+        let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
+        arguments.extend_from_slice(&[
+            "-fetch-retry-mintimeout",
+            "2000",
+            "-fetch-retry-maxtimeout",
+            "5000",
+            "-fetch-timeout",
+            "5000",
+        ]);
+
+        self.run_npm_subcommand(Some(directory), "install", &arguments)
+            .await?;
         Ok(())
     }
 
+    async fn reinstall(&self) -> Result<PathBuf> {
+        log::info!("beginnning to reinstall Node runtime");
+        let mut installation_path = self.installation_path.lock().await;
+
+        if let Some(task) = installation_path.as_ref().cloned() {
+            if let Ok(installation_path) = task.await {
+                smol::fs::remove_dir_all(&installation_path)
+                    .await
+                    .context("node dir removal")
+                    .log_err();
+            }
+        }
+
+        let http = self.http.clone();
+        let task = self
+            .background
+            .spawn(async move { Self::install(http).await.map_err(Arc::new) })
+            .shared();
+
+        *installation_path = Some(task.clone());
+        task.await.map_err(|e| anyhow!("{}", e))
+    }
+
     async fn install_if_needed(&self) -> Result<PathBuf> {
         let task = self
             .installation_path
             .lock()
+            .await
             .get_or_insert_with(|| {
                 let http = self.http.clone();
                 self.background
@@ -155,13 +194,11 @@ impl NodeRuntime {
             })
             .clone();
 
-        match task.await {
-            Ok(path) => Ok(path),
-            Err(error) => Err(anyhow!("{}", error)),
-        }
+        task.await.map_err(|e| anyhow!("{}", e))
     }
 
     async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
+        log::info!("installing Node runtime");
         let arch = match consts::ARCH {
             "x86_64" => "x64",
             "aarch64" => "arm64",

crates/outline/src/outline.rs πŸ”—

@@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate {
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let string_match = &self.matches[ix];
         let outline_item = &self.outline.items[string_match.candidate_id];
 

crates/picker/src/picker.rs πŸ”—

@@ -25,6 +25,7 @@ pub struct Picker<D: PickerDelegate> {
     theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
     confirmed: bool,
     pending_update_matches: Task<Option<()>>,
+    has_focus: bool,
 }
 
 pub trait PickerDelegate: Sized + 'static {
@@ -45,6 +46,18 @@ pub trait PickerDelegate: Sized + 'static {
     fn center_selection_after_match_updates(&self) -> bool {
         false
     }
+    fn render_header(
+        &self,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
+        None
+    }
+    fn render_footer(
+        &self,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<AnyElement<Picker<Self>>> {
+        None
+    }
 }
 
 impl<D: PickerDelegate> Entity for Picker<D> {
@@ -77,6 +90,7 @@ impl<D: PickerDelegate> View for Picker<D> {
                     .contained()
                     .with_style(editor_style),
             )
+            .with_children(self.delegate.render_header(cx))
             .with_children(if match_count == 0 {
                 if query.is_empty() {
                     None
@@ -118,6 +132,7 @@ impl<D: PickerDelegate> View for Picker<D> {
                     .into_any(),
                 )
             })
+            .with_children(self.delegate.render_footer(cx))
             .contained()
             .with_style(container_style)
             .constrained()
@@ -132,13 +147,22 @@ impl<D: PickerDelegate> View for Picker<D> {
     }
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
         if cx.is_self_focused() {
             cx.focus(&self.query_editor);
         }
     }
+
+    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
 }
 
 impl<D: PickerDelegate> Modal for Picker<D> {
+    fn has_focus(&self) -> bool {
+        self.has_focus
+    }
+
     fn dismiss_on_event(event: &Self::Event) -> bool {
         matches!(event, PickerEvent::Dismiss)
     }
@@ -183,6 +207,7 @@ impl<D: PickerDelegate> Picker<D> {
             theme,
             confirmed: false,
             pending_update_matches: Task::ready(None),
+            has_focus: false,
         };
         this.update_matches(String::new(), cx);
         this

crates/project/Cargo.toml πŸ”—

@@ -64,7 +64,7 @@ itertools = "0.10"
 [dev-dependencies]
 ctor.workspace = true
 env_logger.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
 client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
 db = { path = "../db", features = ["test-support"] }

crates/project/src/lsp_command.rs πŸ”—

@@ -1,14 +1,15 @@
 use crate::{
-    DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
-    ProjectTransaction,
+    DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
+    InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
+    MarkupContent, Project, ProjectTransaction,
 };
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use client::proto::{self, PeerId};
 use fs::LineEnding;
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
-    language_settings::language_settings,
+    language_settings::{language_settings, InlayHintKind},
     point_from_lsp, point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
@@ -126,6 +127,10 @@ pub(crate) struct OnTypeFormatting {
     pub push_to_history: bool,
 }
 
+pub(crate) struct InlayHints {
+    pub range: Range<Anchor>,
+}
+
 pub(crate) struct FormattingOptions {
     tab_size: u32,
 }
@@ -1780,3 +1785,343 @@ impl LspCommand for OnTypeFormatting {
         message.buffer_id
     }
 }
+
+#[async_trait(?Send)]
+impl LspCommand for InlayHints {
+    type Response = Vec<InlayHint>;
+    type LspRequest = lsp::InlayHintRequest;
+    type ProtoRequest = proto::InlayHints;
+
+    fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
+        let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false };
+        match inlay_hint_provider {
+            lsp::OneOf::Left(enabled) => *enabled,
+            lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
+                lsp::InlayHintServerCapabilities::Options(_) => true,
+                lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false,
+            },
+        }
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        buffer: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::InlayHintParams {
+        lsp::InlayHintParams {
+            text_document: lsp::TextDocumentIdentifier {
+                uri: lsp::Url::from_file_path(path).unwrap(),
+            },
+            range: range_to_lsp(self.range.to_point_utf16(buffer)),
+            work_done_progress_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<Vec<lsp::InlayHint>>,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        server_id: LanguageServerId,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<InlayHint>> {
+        let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+        // `typescript-language-server` adds padding to the left for type hints, turning
+        // `const foo: boolean` into `const foo : boolean` which looks odd.
+        // `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
+        //
+        // We could trim the whole string, but being pessimistic on par with the situation above,
+        // there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
+        // Hence let's use a heuristic first to handle the most awkward case and look for more.
+        let force_no_type_left_padding =
+            lsp_adapter.name.0.as_ref() == "typescript-language-server";
+        cx.read(|cx| {
+            let origin_buffer = buffer.read(cx);
+            Ok(message
+                .unwrap_or_default()
+                .into_iter()
+                .map(|lsp_hint| {
+                    let kind = lsp_hint.kind.and_then(|kind| match kind {
+                        lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
+                        lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
+                        _ => None,
+                    });
+                    let position = origin_buffer
+                        .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+                    let padding_left =
+                        if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
+                            false
+                        } else {
+                            lsp_hint.padding_left.unwrap_or(false)
+                        };
+                    InlayHint {
+                        buffer_id: origin_buffer.remote_id(),
+                        position: if kind == Some(InlayHintKind::Parameter) {
+                            origin_buffer.anchor_before(position)
+                        } else {
+                            origin_buffer.anchor_after(position)
+                        },
+                        padding_left,
+                        padding_right: lsp_hint.padding_right.unwrap_or(false),
+                        label: match lsp_hint.label {
+                            lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
+                            lsp::InlayHintLabel::LabelParts(lsp_parts) => {
+                                InlayHintLabel::LabelParts(
+                                    lsp_parts
+                                        .into_iter()
+                                        .map(|label_part| InlayHintLabelPart {
+                                            value: label_part.value,
+                                            tooltip: label_part.tooltip.map(
+                                                |tooltip| {
+                                                    match tooltip {
+                                        lsp::InlayHintLabelPartTooltip::String(s) => {
+                                            InlayHintLabelPartTooltip::String(s)
+                                        }
+                                        lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                            markup_content,
+                                        ) => InlayHintLabelPartTooltip::MarkupContent(
+                                            MarkupContent {
+                                                kind: format!("{:?}", markup_content.kind),
+                                                value: markup_content.value,
+                                            },
+                                        ),
+                                    }
+                                                },
+                                            ),
+                                            location: label_part.location.map(|lsp_location| {
+                                                let target_start = origin_buffer.clip_point_utf16(
+                                                    point_from_lsp(lsp_location.range.start),
+                                                    Bias::Left,
+                                                );
+                                                let target_end = origin_buffer.clip_point_utf16(
+                                                    point_from_lsp(lsp_location.range.end),
+                                                    Bias::Left,
+                                                );
+                                                Location {
+                                                    buffer: buffer.clone(),
+                                                    range: origin_buffer.anchor_after(target_start)
+                                                        ..origin_buffer.anchor_before(target_end),
+                                                }
+                                            }),
+                                        })
+                                        .collect(),
+                                )
+                            }
+                        },
+                        kind,
+                        tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
+                            lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
+                            lsp::InlayHintTooltip::MarkupContent(markup_content) => {
+                                InlayHintTooltip::MarkupContent(MarkupContent {
+                                    kind: format!("{:?}", markup_content.kind),
+                                    value: markup_content.value,
+                                })
+                            }
+                        }),
+                    }
+                })
+                .collect())
+        })
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
+        proto::InlayHints {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            start: Some(language::proto::serialize_anchor(&self.range.start)),
+            end: Some(language::proto::serialize_anchor(&self.range.end)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::InlayHints,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let start = message
+            .start
+            .and_then(language::proto::deserialize_anchor)
+            .context("invalid start")?;
+        let end = message
+            .end
+            .and_then(language::proto::deserialize_anchor)
+            .context("invalid end")?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+
+        Ok(Self { range: start..end })
+    }
+
+    fn response_to_proto(
+        response: Vec<InlayHint>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::InlayHintsResponse {
+        proto::InlayHintsResponse {
+            hints: response
+                .into_iter()
+                .map(|response_hint| proto::InlayHint {
+                    position: Some(language::proto::serialize_anchor(&response_hint.position)),
+                    padding_left: response_hint.padding_left,
+                    padding_right: response_hint.padding_right,
+                    label: Some(proto::InlayHintLabel {
+                        label: Some(match response_hint.label {
+                            InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+                            InlayHintLabel::LabelParts(label_parts) => {
+                                proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+                                    parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
+                                        value: label_part.value,
+                                        tooltip: label_part.tooltip.map(|tooltip| {
+                                            let proto_tooltip = match tooltip {
+                                                InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+                                                InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+                                                    kind: markup_content.kind,
+                                                    value: markup_content.value,
+                                                }),
+                                            };
+                                            proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+                                        }),
+                                        location: label_part.location.map(|location| proto::Location {
+                                            start: Some(serialize_anchor(&location.range.start)),
+                                            end: Some(serialize_anchor(&location.range.end)),
+                                            buffer_id: location.buffer.read(cx).remote_id(),
+                                        }),
+                                    }).collect()
+                                })
+                            }
+                        }),
+                    }),
+                    kind: response_hint.kind.map(|kind| kind.name().to_string()),
+                    tooltip: response_hint.tooltip.map(|response_tooltip| {
+                        let proto_tooltip = match response_tooltip {
+                            InlayHintTooltip::String(s) => {
+                                proto::inlay_hint_tooltip::Content::Value(s)
+                            }
+                            InlayHintTooltip::MarkupContent(markup_content) => {
+                                proto::inlay_hint_tooltip::Content::MarkupContent(
+                                    proto::MarkupContent {
+                                        kind: markup_content.kind,
+                                        value: markup_content.value,
+                                    },
+                                )
+                            }
+                        };
+                        proto::InlayHintTooltip {
+                            content: Some(proto_tooltip),
+                        }
+                    }),
+                })
+                .collect(),
+            version: serialize_version(buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::InlayHintsResponse,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<InlayHint>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+
+        let mut hints = Vec::new();
+        for message_hint in message.hints {
+            let buffer_id = message_hint
+                .position
+                .as_ref()
+                .and_then(|location| location.buffer_id)
+                .context("missing buffer id")?;
+            let hint = InlayHint {
+                buffer_id,
+                position: message_hint
+                    .position
+                    .and_then(language::proto::deserialize_anchor)
+                    .context("invalid position")?,
+                label: match message_hint
+                    .label
+                    .and_then(|label| label.label)
+                    .context("missing label")?
+                {
+                    proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
+                    proto::inlay_hint_label::Label::LabelParts(parts) => {
+                        let mut label_parts = Vec::new();
+                        for part in parts.parts {
+                            label_parts.push(InlayHintLabelPart {
+                                value: part.value,
+                                tooltip: part.tooltip.map(|tooltip| match tooltip.content {
+                                    Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s),
+                                    Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+                                        kind: markup_content.kind,
+                                        value: markup_content.value,
+                                    }),
+                                    None => InlayHintLabelPartTooltip::String(String::new()),
+                                }),
+                                location: match part.location {
+                                    Some(location) => {
+                                        let target_buffer = project
+                                            .update(&mut cx, |this, cx| {
+                                                this.wait_for_remote_buffer(location.buffer_id, cx)
+                                            })
+                                            .await?;
+                                        Some(Location {
+                                        range: location
+                                            .start
+                                            .and_then(language::proto::deserialize_anchor)
+                                            .context("invalid start")?
+                                            ..location
+                                                .end
+                                                .and_then(language::proto::deserialize_anchor)
+                                                .context("invalid end")?,
+                                        buffer: target_buffer,
+                                    })},
+                                    None => None,
+                                },
+                            });
+                        }
+
+                        InlayHintLabel::LabelParts(label_parts)
+                    }
+                },
+                padding_left: message_hint.padding_left,
+                padding_right: message_hint.padding_right,
+                kind: message_hint
+                    .kind
+                    .as_deref()
+                    .and_then(InlayHintKind::from_name),
+                tooltip: message_hint.tooltip.and_then(|tooltip| {
+                    Some(match tooltip.content? {
+                        proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
+                        proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
+                            InlayHintTooltip::MarkupContent(MarkupContent {
+                                kind: markup_content.kind,
+                                value: markup_content.value,
+                            })
+                        }
+                    })
+                }),
+            };
+
+            hints.push(hint);
+        }
+
+        Ok(hints)
+    }
+
+    fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 {
+        message.buffer_id
+    }
+}

crates/project/src/project.rs πŸ”—

@@ -29,23 +29,24 @@ use gpui::{
     AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
     ModelHandle, Task, WeakModelHandle,
 };
+use itertools::Itertools;
 use language::{
-    language_settings::{language_settings, FormatOnSave, Formatter},
+    language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version,
     },
-    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
+    range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
     Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
-    Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch,
-    PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
-    Unclipped,
+    Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt,
+    Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
+    ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
     DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
-    DocumentHighlightKind, LanguageServer, LanguageServerId,
+    DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
 };
 use lsp_command::*;
 use postage::watch;
@@ -64,7 +65,8 @@ use std::{
     mem,
     num::NonZeroU32,
     ops::Range,
-    path::{Component, Path, PathBuf},
+    path::{self, Component, Path, PathBuf},
+    process::Stdio,
     rc::Rc,
     str,
     sync::{
@@ -74,9 +76,10 @@ use std::{
     time::{Duration, Instant},
 };
 use terminals::Terminals;
+use text::Anchor;
 use util::{
-    debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
-    ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, merge_json_value_into,
+    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -223,6 +226,7 @@ enum OpenBuffer {
     Operations(Vec<Operation>),
 }
 
+#[derive(Clone)]
 enum WorktreeHandle {
     Strong(ModelHandle<Worktree>),
     Weak(WeakModelHandle<Worktree>),
@@ -252,6 +256,7 @@ pub enum Event {
     LanguageServerAdded(LanguageServerId),
     LanguageServerRemoved(LanguageServerId),
     LanguageServerLog(LanguageServerId, String),
+    Notification(String),
     ActiveEntryChanged(Option<ProjectEntryId>),
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
@@ -274,10 +279,12 @@ pub enum Event {
         new_peer_id: proto::PeerId,
     },
     CollaboratorLeft(proto::PeerId),
+    RefreshInlays,
 }
 
 pub enum LanguageServerState {
     Starting(Task<Option<Arc<LanguageServer>>>),
+
     Running {
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
@@ -315,12 +322,63 @@ pub struct DiagnosticSummary {
     pub warning_count: usize,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Location {
     pub buffer: ModelHandle<Buffer>,
     pub range: Range<language::Anchor>,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHint {
+    pub buffer_id: u64,
+    pub position: language::Anchor,
+    pub label: InlayHintLabel,
+    pub kind: Option<InlayHintKind>,
+    pub padding_left: bool,
+    pub padding_right: bool,
+    pub tooltip: Option<InlayHintTooltip>,
+}
+
+impl InlayHint {
+    pub fn text(&self) -> String {
+        match &self.label {
+            InlayHintLabel::String(s) => s.to_owned(),
+            InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabel {
+    String(String),
+    LabelParts(Vec<InlayHintLabelPart>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHintLabelPart {
+    pub value: String,
+    pub tooltip: Option<InlayHintLabelPartTooltip>,
+    pub location: Option<Location>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintTooltip {
+    String(String),
+    MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabelPartTooltip {
+    String(String),
+    MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct MarkupContent {
+    pub kind: String,
+    pub value: String,
+}
+
 #[derive(Debug, Clone)]
 pub struct LocationLink {
     pub origin: Option<Location>,
@@ -435,6 +493,11 @@ pub enum FormatTrigger {
     Manual,
 }
 
+struct ProjectLspAdapterDelegate {
+    project: ModelHandle<Project>,
+    http_client: Arc<dyn HttpClient>,
+}
+
 impl FormatTrigger {
     fn from_proto(value: i32) -> FormatTrigger {
         match value {
@@ -472,9 +535,12 @@ impl Project {
         client.add_model_request_handler(Self::handle_rename_project_entry);
         client.add_model_request_handler(Self::handle_copy_project_entry);
         client.add_model_request_handler(Self::handle_delete_project_entry);
+        client.add_model_request_handler(Self::handle_expand_project_entry);
         client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
         client.add_model_request_handler(Self::handle_apply_code_action);
         client.add_model_request_handler(Self::handle_on_type_formatting);
+        client.add_model_request_handler(Self::handle_inlay_hints);
+        client.add_model_request_handler(Self::handle_refresh_inlay_hints);
         client.add_model_request_handler(Self::handle_reload_buffers);
         client.add_model_request_handler(Self::handle_synchronize_buffers);
         client.add_model_request_handler(Self::handle_format_buffers);
@@ -1066,6 +1132,40 @@ impl Project {
         }
     }
 
+    pub fn expand_entry(
+        &mut self,
+        worktree_id: WorktreeId,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let worktree = self.worktree_for_id(worktree_id, cx)?;
+        if self.is_local() {
+            worktree.update(cx, |worktree, cx| {
+                worktree.as_local_mut().unwrap().expand_entry(entry_id, cx)
+            })
+        } else {
+            let worktree = worktree.downgrade();
+            let request = self.client.request(proto::ExpandProjectEntry {
+                project_id: self.remote_id().unwrap(),
+                entry_id: entry_id.to_proto(),
+            });
+            Some(cx.spawn_weak(|_, mut cx| async move {
+                let response = request.await?;
+                if let Some(worktree) = worktree.upgrade(&cx) {
+                    worktree
+                        .update(&mut cx, |worktree, _| {
+                            worktree
+                                .as_remote_mut()
+                                .unwrap()
+                                .wait_for_snapshot(response.worktree_scan_id as usize)
+                        })
+                        .await?;
+                }
+                Ok(())
+            }))
+        }
+    }
+
     pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
         if self.client_state.is_some() {
             return Err(anyhow!("project was already shared"));
@@ -2383,348 +2483,524 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        if !language_settings(
-            Some(&language),
-            worktree
-                .update(cx, |tree, cx| tree.root_file(cx))
-                .map(|f| f as _)
-                .as_ref(),
-            cx,
-        )
-        .enable_language_server
-        {
+        let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx));
+        let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx);
+        if !settings.enable_language_server {
             return;
         }
 
         let worktree_id = worktree.read(cx).id();
         for adapter in language.lsp_adapters() {
-            let key = (worktree_id, adapter.name.clone());
-            if self.language_server_ids.contains_key(&key) {
-                continue;
-            }
-
-            let pending_server = match self.languages.start_language_server(
-                language.clone(),
-                adapter.clone(),
+            self.start_language_server(
+                worktree_id,
                 worktree_path.clone(),
-                self.client.http_client(),
-                cx,
-            ) {
-                Some(pending_server) => pending_server,
-                None => continue,
-            };
-
-            let lsp = settings::get::<ProjectSettings>(cx)
-                .lsp
-                .get(&adapter.name.0);
-            let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
-
-            let mut initialization_options = adapter.initialization_options.clone();
-            match (&mut initialization_options, override_options) {
-                (Some(initialization_options), Some(override_options)) => {
-                    merge_json_value_into(override_options, initialization_options);
-                }
-                (None, override_options) => initialization_options = override_options,
-                _ => {}
-            }
-
-            let server_id = pending_server.server_id;
-            let state = self.setup_pending_language_server(
-                initialization_options,
-                pending_server,
                 adapter.clone(),
                 language.clone(),
-                key.clone(),
                 cx,
             );
-            self.language_servers.insert(server_id, state);
-            self.language_server_ids.insert(key.clone(), server_id);
         }
     }
 
-    fn setup_pending_language_server(
+    fn start_language_server(
         &mut self,
-        initialization_options: Option<serde_json::Value>,
-        pending_server: PendingLanguageServer,
+        worktree_id: WorktreeId,
+        worktree_path: Arc<Path>,
         adapter: Arc<CachedLspAdapter>,
         language: Arc<Language>,
-        key: (WorktreeId, LanguageServerName),
-        cx: &mut ModelContext<Project>,
-    ) -> LanguageServerState {
+        cx: &mut ModelContext<Self>,
+    ) {
+        let key = (worktree_id, adapter.name.clone());
+        if self.language_server_ids.contains_key(&key) {
+            return;
+        }
+
+        let pending_server = match self.languages.create_pending_language_server(
+            language.clone(),
+            adapter.clone(),
+            worktree_path,
+            ProjectLspAdapterDelegate::new(self, cx),
+            cx,
+        ) {
+            Some(pending_server) => pending_server,
+            None => return,
+        };
+
+        let project_settings = settings::get::<ProjectSettings>(cx);
+        let lsp = project_settings.lsp.get(&adapter.name.0);
+        let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
+
+        let mut initialization_options = adapter.initialization_options.clone();
+        match (&mut initialization_options, override_options) {
+            (Some(initialization_options), Some(override_options)) => {
+                merge_json_value_into(override_options, initialization_options);
+            }
+            (None, override_options) => initialization_options = override_options,
+            _ => {}
+        }
+
         let server_id = pending_server.server_id;
-        let languages = self.languages.clone();
+        let container_dir = pending_server.container_dir.clone();
+        let state = LanguageServerState::Starting({
+            let adapter = adapter.clone();
+            let server_name = adapter.name.0.clone();
+            let languages = self.languages.clone();
+            let language = language.clone();
+            let key = key.clone();
 
-        LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
-            let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
-            let language_server = pending_server.task.await.log_err()?;
+            cx.spawn_weak(|this, mut cx| async move {
+                let result = Self::setup_and_insert_language_server(
+                    this,
+                    initialization_options,
+                    pending_server,
+                    adapter.clone(),
+                    languages,
+                    language.clone(),
+                    server_id,
+                    key,
+                    &mut cx,
+                )
+                .await;
+
+                match result {
+                    Ok(server) => server,
+
+                    Err(err) => {
+                        log::error!("failed to start language server {:?}: {}", server_name, err);
 
-            language_server
-                .on_notification::<lsp::notification::LogMessage, _>({
-                    move |params, mut cx| {
                         if let Some(this) = this.upgrade(&cx) {
-                            this.update(&mut cx, |_, cx| {
-                                cx.emit(Event::LanguageServerLog(server_id, params.message))
-                            });
-                        }
-                    }
-                })
-                .detach();
+                            if let Some(container_dir) = container_dir {
+                                let installation_test_binary = adapter
+                                    .installation_test_binary(container_dir.to_path_buf())
+                                    .await;
 
-            language_server
-                .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                    let adapter = adapter.clone();
-                    move |mut params, cx| {
-                        let adapter = adapter.clone();
-                        cx.spawn(|mut cx| async move {
-                            adapter.process_diagnostics(&mut params).await;
-                            if let Some(this) = this.upgrade(&cx) {
-                                this.update(&mut cx, |this, cx| {
-                                    this.update_diagnostics(
+                                this.update(&mut cx, |_, cx| {
+                                    Self::check_errored_server(
+                                        language,
+                                        adapter,
                                         server_id,
-                                        params,
-                                        &adapter.disk_based_diagnostic_sources,
+                                        installation_test_binary,
                                         cx,
                                     )
-                                    .log_err();
                                 });
                             }
-                        })
-                        .detach();
-                    }
-                })
-                .detach();
-
-            language_server
-                .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
-                    let languages = languages.clone();
-                    move |params, mut cx| {
-                        let languages = languages.clone();
-                        async move {
-                            let workspace_config =
-                                cx.update(|cx| languages.workspace_configuration(cx)).await;
-                            Ok(params
-                                .items
-                                .into_iter()
-                                .map(|item| {
-                                    if let Some(section) = &item.section {
-                                        workspace_config
-                                            .get(section)
-                                            .cloned()
-                                            .unwrap_or(serde_json::Value::Null)
-                                    } else {
-                                        workspace_config.clone()
-                                    }
-                                })
-                                .collect())
                         }
+
+                        None
                     }
-                })
-                .detach();
+                }
+            })
+        });
 
-            // Even though we don't have handling for these requests, respond to them to
-            // avoid stalling any language server like `gopls` which waits for a response
-            // to these requests when initializing.
-            language_server
-                .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
-                    move |params, mut cx| async move {
-                        if let Some(this) = this.upgrade(&cx) {
-                            this.update(&mut cx, |this, _| {
-                                if let Some(status) =
-                                    this.language_server_statuses.get_mut(&server_id)
-                                {
-                                    if let lsp::NumberOrString::String(token) = params.token {
-                                        status.progress_tokens.insert(token);
-                                    }
-                                }
-                            });
-                        }
-                        Ok(())
-                    },
-                )
-                .detach();
-            language_server
-                .on_request::<lsp::request::RegisterCapability, _, _>(
-                    move |params, mut cx| async move {
-                        let this = this
-                            .upgrade(&cx)
-                            .ok_or_else(|| anyhow!("project dropped"))?;
-                        for reg in params.registrations {
-                            if reg.method == "workspace/didChangeWatchedFiles" {
-                                if let Some(options) = reg.register_options {
-                                    let options = serde_json::from_value(options)?;
-                                    this.update(&mut cx, |this, cx| {
-                                        this.on_lsp_did_change_watched_files(
-                                            server_id, options, cx,
-                                        );
-                                    });
-                                }
-                            }
-                        }
-                        Ok(())
-                    },
-                )
-                .detach();
+        self.language_servers.insert(server_id, state);
+        self.language_server_ids.insert(key, server_id);
+    }
 
-            language_server
-                .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
-                    let adapter = adapter.clone();
-                    move |params, cx| {
-                        Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
-                    }
-                })
-                .detach();
+    fn reinstall_language_server(
+        &mut self,
+        language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
+        server_id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<()>> {
+        log::info!("beginning to reinstall server");
+
+        let existing_server = match self.language_servers.remove(&server_id) {
+            Some(LanguageServerState::Running { server, .. }) => Some(server),
+            _ => None,
+        };
 
-            let disk_based_diagnostics_progress_token =
-                adapter.disk_based_diagnostics_progress_token.clone();
+        for worktree in &self.worktrees {
+            if let Some(worktree) = worktree.upgrade(cx) {
+                let key = (worktree.read(cx).id(), adapter.name.clone());
+                self.language_server_ids.remove(&key);
+            }
+        }
 
-            language_server
-                .on_notification::<lsp::notification::Progress, _>({
-                    move |params, mut cx| {
+        Some(cx.spawn(move |this, mut cx| async move {
+            if let Some(task) = existing_server.and_then(|server| server.shutdown()) {
+                log::info!("shutting down existing server");
+                task.await;
+            }
+
+            // TODO: This is race-safe with regards to preventing new instances from
+            // starting while deleting, but existing instances in other projects are going
+            // to be very confused and messed up
+            this.update(&mut cx, |this, cx| {
+                this.languages.delete_server_container(adapter.clone(), cx)
+            })
+            .await;
+
+            this.update(&mut cx, |this, mut cx| {
+                let worktrees = this.worktrees.clone();
+                for worktree in worktrees {
+                    let worktree = match worktree.upgrade(cx) {
+                        Some(worktree) => worktree.read(cx),
+                        None => continue,
+                    };
+                    let worktree_id = worktree.id();
+                    let root_path = worktree.abs_path();
+
+                    this.start_language_server(
+                        worktree_id,
+                        root_path,
+                        adapter.clone(),
+                        language.clone(),
+                        &mut cx,
+                    );
+                }
+            })
+        }))
+    }
+
+    async fn setup_and_insert_language_server(
+        this: WeakModelHandle<Self>,
+        initialization_options: Option<serde_json::Value>,
+        pending_server: PendingLanguageServer,
+        adapter: Arc<CachedLspAdapter>,
+        languages: Arc<LanguageRegistry>,
+        language: Arc<Language>,
+        server_id: LanguageServerId,
+        key: (WorktreeId, LanguageServerName),
+        cx: &mut AsyncAppContext,
+    ) -> Result<Option<Arc<LanguageServer>>> {
+        let setup = Self::setup_pending_language_server(
+            this,
+            initialization_options,
+            pending_server,
+            adapter.clone(),
+            languages,
+            server_id,
+            cx,
+        );
+
+        let language_server = match setup.await? {
+            Some(language_server) => language_server,
+            None => return Ok(None),
+        };
+
+        let this = match this.upgrade(cx) {
+            Some(this) => this,
+            None => return Err(anyhow!("failed to upgrade project handle")),
+        };
+
+        this.update(cx, |this, cx| {
+            this.insert_newly_running_language_server(
+                language,
+                adapter,
+                language_server.clone(),
+                server_id,
+                key,
+                cx,
+            )
+        })?;
+
+        Ok(Some(language_server))
+    }
+
+    async fn setup_pending_language_server(
+        this: WeakModelHandle<Self>,
+        initialization_options: Option<serde_json::Value>,
+        pending_server: PendingLanguageServer,
+        adapter: Arc<CachedLspAdapter>,
+        languages: Arc<LanguageRegistry>,
+        server_id: LanguageServerId,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Option<Arc<LanguageServer>>> {
+        let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
+        let language_server = match pending_server.task.await? {
+            Some(server) => server.initialize(initialization_options).await?,
+            None => {
+                return Ok(None);
+            }
+        };
+
+        language_server
+            .on_notification::<lsp::notification::LogMessage, _>({
+                move |params, mut cx| {
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |_, cx| {
+                            cx.emit(Event::LanguageServerLog(server_id, params.message))
+                        });
+                    }
+                }
+            })
+            .detach();
+
+        language_server
+            .on_notification::<lsp::notification::PublishDiagnostics, _>({
+                let adapter = adapter.clone();
+                move |mut params, cx| {
+                    let this = this;
+                    let adapter = adapter.clone();
+                    cx.spawn(|mut cx| async move {
+                        adapter.process_diagnostics(&mut params).await;
                         if let Some(this) = this.upgrade(&cx) {
                             this.update(&mut cx, |this, cx| {
-                                this.on_lsp_progress(
-                                    params,
+                                this.update_diagnostics(
                                     server_id,
-                                    disk_based_diagnostics_progress_token.clone(),
+                                    params,
+                                    &adapter.disk_based_diagnostic_sources,
                                     cx,
-                                );
+                                )
+                                .log_err();
                             });
                         }
+                    })
+                    .detach();
+                }
+            })
+            .detach();
+
+        language_server
+            .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
+                let languages = languages.clone();
+                move |params, mut cx| {
+                    let languages = languages.clone();
+                    async move {
+                        let workspace_config =
+                            cx.update(|cx| languages.workspace_configuration(cx)).await;
+                        Ok(params
+                            .items
+                            .into_iter()
+                            .map(|item| {
+                                if let Some(section) = &item.section {
+                                    workspace_config
+                                        .get(section)
+                                        .cloned()
+                                        .unwrap_or(serde_json::Value::Null)
+                                } else {
+                                    workspace_config.clone()
+                                }
+                            })
+                            .collect())
+                    }
+                }
+            })
+            .detach();
+
+        // Even though we don't have handling for these requests, respond to them to
+        // avoid stalling any language server like `gopls` which waits for a response
+        // to these requests when initializing.
+        language_server
+            .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
+                move |params, mut cx| async move {
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, _| {
+                            if let Some(status) = this.language_server_statuses.get_mut(&server_id)
+                            {
+                                if let lsp::NumberOrString::String(token) = params.token {
+                                    status.progress_tokens.insert(token);
+                                }
+                            }
+                        });
+                    }
+                    Ok(())
+                },
+            )
+            .detach();
+        language_server
+            .on_request::<lsp::request::RegisterCapability, _, _>({
+                move |params, mut cx| async move {
+                    let this = this
+                        .upgrade(&cx)
+                        .ok_or_else(|| anyhow!("project dropped"))?;
+                    for reg in params.registrations {
+                        if reg.method == "workspace/didChangeWatchedFiles" {
+                            if let Some(options) = reg.register_options {
+                                let options = serde_json::from_value(options)?;
+                                this.update(&mut cx, |this, cx| {
+                                    this.on_lsp_did_change_watched_files(server_id, options, cx);
+                                });
+                            }
+                        }
                     }
-                })
-                .detach();
+                    Ok(())
+                }
+            })
+            .detach();
 
-            let language_server = language_server
-                .initialize(initialization_options)
-                .await
-                .log_err()?;
-            language_server
-                .notify::<lsp::notification::DidChangeConfiguration>(
-                    lsp::DidChangeConfigurationParams {
-                        settings: workspace_config,
-                    },
-                )
-                .ok();
+        language_server
+            .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+                let adapter = adapter.clone();
+                move |params, cx| {
+                    Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
+                }
+            })
+            .detach();
 
-            let this = this.upgrade(&cx)?;
-            this.update(&mut cx, |this, cx| {
-                // If the language server for this key doesn't match the server id, don't store the
-                // server. Which will cause it to be dropped, killing the process
-                if this
-                    .language_server_ids
-                    .get(&key)
-                    .map(|id| id != &server_id)
-                    .unwrap_or(false)
-                {
-                    return None;
+        language_server
+            .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
+                move |(), mut cx| async move {
+                    let this = this
+                        .upgrade(&cx)
+                        .ok_or_else(|| anyhow!("project dropped"))?;
+                    this.update(&mut cx, |project, cx| {
+                        cx.emit(Event::RefreshInlays);
+                        project.remote_id().map(|project_id| {
+                            project.client.send(proto::RefreshInlayHints { project_id })
+                        })
+                    })
+                    .transpose()?;
+                    Ok(())
                 }
+            })
+            .detach();
+
+        let disk_based_diagnostics_progress_token =
+            adapter.disk_based_diagnostics_progress_token.clone();
+
+        language_server
+            .on_notification::<lsp::notification::Progress, _>(move |params, mut cx| {
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.on_lsp_progress(
+                            params,
+                            server_id,
+                            disk_based_diagnostics_progress_token.clone(),
+                            cx,
+                        );
+                    });
+                }
+            })
+            .detach();
+
+        language_server
+            .notify::<lsp::notification::DidChangeConfiguration>(
+                lsp::DidChangeConfigurationParams {
+                    settings: workspace_config,
+                },
+            )
+            .ok();
+
+        Ok(Some(language_server))
+    }
+
+    fn insert_newly_running_language_server(
+        &mut self,
+        language: Arc<Language>,
+        adapter: Arc<CachedLspAdapter>,
+        language_server: Arc<LanguageServer>,
+        server_id: LanguageServerId,
+        key: (WorktreeId, LanguageServerName),
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        // If the language server for this key doesn't match the server id, don't store the
+        // server. Which will cause it to be dropped, killing the process
+        if self
+            .language_server_ids
+            .get(&key)
+            .map(|id| id != &server_id)
+            .unwrap_or(false)
+        {
+            return Ok(());
+        }
+
+        // Update language_servers collection with Running variant of LanguageServerState
+        // indicating that the server is up and running and ready
+        self.language_servers.insert(
+            server_id,
+            LanguageServerState::Running {
+                adapter: adapter.clone(),
+                language: language.clone(),
+                watched_paths: Default::default(),
+                server: language_server.clone(),
+                simulate_disk_based_diagnostics_completion: None,
+            },
+        );
+
+        self.language_server_statuses.insert(
+            server_id,
+            LanguageServerStatus {
+                name: language_server.name().to_string(),
+                pending_work: Default::default(),
+                has_pending_diagnostic_updates: false,
+                progress_tokens: Default::default(),
+            },
+        );
 
-                // Update language_servers collection with Running variant of LanguageServerState
-                // indicating that the server is up and running and ready
-                this.language_servers.insert(
-                    server_id,
-                    LanguageServerState::Running {
-                        adapter: adapter.clone(),
-                        language: language.clone(),
-                        watched_paths: Default::default(),
-                        server: language_server.clone(),
-                        simulate_disk_based_diagnostics_completion: None,
-                    },
-                );
-                this.language_server_statuses.insert(
-                    server_id,
-                    LanguageServerStatus {
-                        name: language_server.name().to_string(),
-                        pending_work: Default::default(),
-                        has_pending_diagnostic_updates: false,
-                        progress_tokens: Default::default(),
-                    },
-                );
+        cx.emit(Event::LanguageServerAdded(server_id));
 
-                cx.emit(Event::LanguageServerAdded(server_id));
+        if let Some(project_id) = self.remote_id() {
+            self.client.send(proto::StartLanguageServer {
+                project_id,
+                server: Some(proto::LanguageServer {
+                    id: server_id.0 as u64,
+                    name: language_server.name().to_string(),
+                }),
+            })?;
+        }
 
-                if let Some(project_id) = this.remote_id() {
-                    this.client
-                        .send(proto::StartLanguageServer {
-                            project_id,
-                            server: Some(proto::LanguageServer {
-                                id: server_id.0 as u64,
-                                name: language_server.name().to_string(),
-                            }),
-                        })
-                        .log_err();
+        // Tell the language server about every open buffer in the worktree that matches the language.
+        for buffer in self.opened_buffers.values() {
+            if let Some(buffer_handle) = buffer.upgrade(cx) {
+                let buffer = buffer_handle.read(cx);
+                let file = match File::from_dyn(buffer.file()) {
+                    Some(file) => file,
+                    None => continue,
+                };
+                let language = match buffer.language() {
+                    Some(language) => language,
+                    None => continue,
+                };
+
+                if file.worktree.read(cx).id() != key.0
+                    || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+                {
+                    continue;
                 }
 
-                // Tell the language server about every open buffer in the worktree that matches the language.
-                for buffer in this.opened_buffers.values() {
-                    if let Some(buffer_handle) = buffer.upgrade(cx) {
-                        let buffer = buffer_handle.read(cx);
-                        let file = match File::from_dyn(buffer.file()) {
-                            Some(file) => file,
-                            None => continue,
-                        };
-                        let language = match buffer.language() {
-                            Some(language) => language,
-                            None => continue,
-                        };
+                let file = match file.as_local() {
+                    Some(file) => file,
+                    None => continue,
+                };
 
-                        if file.worktree.read(cx).id() != key.0
-                            || !language.lsp_adapters().iter().any(|a| a.name == key.1)
-                        {
-                            continue;
-                        }
+                let versions = self
+                    .buffer_snapshots
+                    .entry(buffer.remote_id())
+                    .or_default()
+                    .entry(server_id)
+                    .or_insert_with(|| {
+                        vec![LspBufferSnapshot {
+                            version: 0,
+                            snapshot: buffer.text_snapshot(),
+                        }]
+                    });
 
-                        let file = file.as_local()?;
-                        let versions = this
-                            .buffer_snapshots
-                            .entry(buffer.remote_id())
-                            .or_default()
-                            .entry(server_id)
-                            .or_insert_with(|| {
-                                vec![LspBufferSnapshot {
-                                    version: 0,
-                                    snapshot: buffer.text_snapshot(),
-                                }]
-                            });
+                let snapshot = versions.last().unwrap();
+                let version = snapshot.version;
+                let initial_snapshot = &snapshot.snapshot;
+                let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+                language_server.notify::<lsp::notification::DidOpenTextDocument>(
+                    lsp::DidOpenTextDocumentParams {
+                        text_document: lsp::TextDocumentItem::new(
+                            uri,
+                            adapter
+                                .language_ids
+                                .get(language.name().as_ref())
+                                .cloned()
+                                .unwrap_or_default(),
+                            version,
+                            initial_snapshot.text(),
+                        ),
+                    },
+                )?;
 
-                        let snapshot = versions.last().unwrap();
-                        let version = snapshot.version;
-                        let initial_snapshot = &snapshot.snapshot;
-                        let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+                buffer_handle.update(cx, |buffer, cx| {
+                    buffer.set_completion_triggers(
                         language_server
-                            .notify::<lsp::notification::DidOpenTextDocument>(
-                                lsp::DidOpenTextDocumentParams {
-                                    text_document: lsp::TextDocumentItem::new(
-                                        uri,
-                                        adapter
-                                            .language_ids
-                                            .get(language.name().as_ref())
-                                            .cloned()
-                                            .unwrap_or_default(),
-                                        version,
-                                        initial_snapshot.text(),
-                                    ),
-                                },
-                            )
-                            .log_err()?;
-                        buffer_handle.update(cx, |buffer, cx| {
-                            buffer.set_completion_triggers(
-                                language_server
-                                    .capabilities()
-                                    .completion_provider
-                                    .as_ref()
-                                    .and_then(|provider| provider.trigger_characters.clone())
-                                    .unwrap_or_default(),
-                                cx,
-                            )
-                        });
-                    }
-                }
+                            .capabilities()
+                            .completion_provider
+                            .as_ref()
+                            .and_then(|provider| provider.trigger_characters.clone())
+                            .unwrap_or_default(),
+                        cx,
+                    )
+                });
+            }
+        }
 
-                cx.notify();
-                Some(language_server)
-            })
-        }))
+        cx.notify();
+        Ok(())
     }
 
     // Returns a list of all of the worktrees which no longer have a language server and the root path

crates/project/src/project_tests.rs πŸ”—

@@ -535,8 +535,28 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
     fs.insert_tree(
         "/the-root",
         json!({
-            "a.rs": "",
-            "b.rs": "",
+            ".gitignore": "target\n",
+            "src": {
+                "a.rs": "",
+                "b.rs": "",
+            },
+            "target": {
+                "x": {
+                    "out": {
+                        "x.rs": ""
+                    }
+                },
+                "y": {
+                    "out": {
+                        "y.rs": "",
+                    }
+                },
+                "z": {
+                    "out": {
+                        "z.rs": ""
+                    }
+                }
+            }
         }),
     )
     .await;
@@ -550,11 +570,34 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
     // Start the language server by opening a buffer with a compatible file extension.
     let _buffer = project
         .update(cx, |project, cx| {
-            project.open_local_buffer("/the-root/a.rs", cx)
+            project.open_local_buffer("/the-root/src/a.rs", cx)
         })
         .await
         .unwrap();
 
+    // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
+    project.read_with(cx, |project, cx| {
+        let worktree = project.worktrees(cx).next().unwrap();
+        assert_eq!(
+            worktree
+                .read(cx)
+                .snapshot()
+                .entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            &[
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+                (Path::new("target"), true),
+            ]
+        );
+    });
+
+    let prev_read_dir_count = fs.read_dir_call_count();
+
     // Keep track of the FS events reported to the language server.
     let fake_server = fake_servers.next().await.unwrap();
     let file_changes = Arc::new(Mutex::new(Vec::new()));
@@ -565,12 +608,26 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
                 method: "workspace/didChangeWatchedFiles".to_string(),
                 register_options: serde_json::to_value(
                     lsp::DidChangeWatchedFilesRegistrationOptions {
-                        watchers: vec![lsp::FileSystemWatcher {
-                            glob_pattern: lsp::GlobPattern::String(
-                                "/the-root/*.{rs,c}".to_string(),
-                            ),
-                            kind: None,
-                        }],
+                        watchers: vec![
+                            lsp::FileSystemWatcher {
+                                glob_pattern: lsp::GlobPattern::String(
+                                    "/the-root/Cargo.toml".to_string(),
+                                ),
+                                kind: None,
+                            },
+                            lsp::FileSystemWatcher {
+                                glob_pattern: lsp::GlobPattern::String(
+                                    "/the-root/src/*.{rs,c}".to_string(),
+                                ),
+                                kind: None,
+                            },
+                            lsp::FileSystemWatcher {
+                                glob_pattern: lsp::GlobPattern::String(
+                                    "/the-root/target/y/**/*.rs".to_string(),
+                                ),
+                                kind: None,
+                            },
+                        ],
                     },
                 )
                 .ok(),
@@ -588,17 +645,51 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
     });
 
     cx.foreground().run_until_parked();
-    assert_eq!(file_changes.lock().len(), 0);
+    assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
+    assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
+
+    // Now the language server has asked us to watch an ignored directory path,
+    // so we recursively load it.
+    project.read_with(cx, |project, cx| {
+        let worktree = project.worktrees(cx).next().unwrap();
+        assert_eq!(
+            worktree
+                .read(cx)
+                .snapshot()
+                .entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            &[
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+                (Path::new("target"), true),
+                (Path::new("target/x"), true),
+                (Path::new("target/y"), true),
+                (Path::new("target/y/out"), true),
+                (Path::new("target/y/out/y.rs"), true),
+                (Path::new("target/z"), true),
+            ]
+        );
+    });
 
     // Perform some file system mutations, two of which match the watched patterns,
     // and one of which does not.
-    fs.create_file("/the-root/c.rs".as_ref(), Default::default())
+    fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
+        .await
+        .unwrap();
+    fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
         .await
         .unwrap();
-    fs.create_file("/the-root/d.txt".as_ref(), Default::default())
+    fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
         .await
         .unwrap();
-    fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
+    fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
         .await
         .unwrap();
 
@@ -608,11 +699,15 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
         &*file_changes.lock(),
         &[
             lsp::FileEvent {
-                uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
+                uri: lsp::Url::from_file_path("/the-root/src/b.rs").unwrap(),
                 typ: lsp::FileChangeType::DELETED,
             },
             lsp::FileEvent {
-                uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
+                uri: lsp::Url::from_file_path("/the-root/src/c.rs").unwrap(),
+                typ: lsp::FileChangeType::CREATED,
+            },
+            lsp::FileEvent {
+                uri: lsp::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(),
                 typ: lsp::FileChangeType::CREATED,
             },
         ]
@@ -3846,6 +3941,14 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
     );
 }
 
+#[test]
+fn test_glob_literal_prefix() {
+    assert_eq!(glob_literal_prefix("**/*.js"), "");
+    assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
+    assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
+    assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
+}
+
 async fn search(
     project: &ModelHandle<Project>,
     query: SearchQuery,

crates/project/src/worktree.rs πŸ”—

@@ -5,7 +5,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{anyhow, Context, Result};
 use client::{proto, Client};
 use clock::ReplicaId;
-use collections::{HashMap, VecDeque};
+use collections::{HashMap, HashSet, VecDeque};
 use fs::{
     repository::{GitFileStatus, GitRepository, RepoPath},
     Fs, LineEnding,
@@ -67,7 +67,8 @@ pub enum Worktree {
 
 pub struct LocalWorktree {
     snapshot: LocalSnapshot,
-    path_changes_tx: channel::Sender<(Vec<PathBuf>, barrier::Sender)>,
+    scan_requests_tx: channel::Sender<ScanRequest>,
+    path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
     is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
     _background_scanner_task: Task<()>,
     share: Option<ShareState>,
@@ -84,6 +85,11 @@ pub struct LocalWorktree {
     visible: bool,
 }
 
+struct ScanRequest {
+    relative_paths: Vec<Arc<Path>>,
+    done: barrier::Sender,
+}
+
 pub struct RemoteWorktree {
     snapshot: Snapshot,
     background_snapshot: Arc<Mutex<Snapshot>>,
@@ -214,6 +220,9 @@ pub struct LocalSnapshot {
 
 struct BackgroundScannerState {
     snapshot: LocalSnapshot,
+    scanned_dirs: HashSet<ProjectEntryId>,
+    path_prefixes_to_scan: HashSet<Arc<Path>>,
+    paths_to_scan: HashSet<Arc<Path>>,
     /// The ids of all of the entries that were removed from the snapshot
     /// as part of the current update. These entry ids may be re-used
     /// if the same inode is discovered at a new path, or if the given
@@ -232,13 +241,6 @@ pub struct LocalRepositoryEntry {
     pub(crate) git_dir_path: Arc<Path>,
 }
 
-impl LocalRepositoryEntry {
-    // Note that this path should be relative to the worktree root.
-    pub(crate) fn in_dot_git(&self, path: &Path) -> bool {
-        path.starts_with(self.git_dir_path.as_ref())
-    }
-}
-
 impl Deref for LocalSnapshot {
     type Target = Snapshot;
 
@@ -330,7 +332,8 @@ impl Worktree {
                 );
             }
 
-            let (path_changes_tx, path_changes_rx) = channel::unbounded();
+            let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
+            let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
             let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
 
             cx.spawn_weak(|this, mut cx| async move {
@@ -370,7 +373,8 @@ impl Worktree {
                         fs,
                         scan_states_tx,
                         background,
-                        path_changes_rx,
+                        scan_requests_rx,
+                        path_prefixes_to_scan_rx,
                     )
                     .run(events)
                     .await;
@@ -381,7 +385,8 @@ impl Worktree {
                 snapshot,
                 is_scanning: watch::channel_with(true),
                 share: None,
-                path_changes_tx,
+                scan_requests_tx,
+                path_prefixes_to_scan_tx,
                 _background_scanner_task: background_scanner_task,
                 diagnostics: Default::default(),
                 diagnostic_summaries: Default::default(),
@@ -867,27 +872,27 @@ impl LocalWorktree {
         path: &Path,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<(File, String, Option<String>)>> {
-        let handle = cx.handle();
         let path = Arc::from(path);
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
-        let snapshot = self.snapshot();
+        let entry = self.refresh_entry(path.clone(), None, cx);
 
-        let mut index_task = None;
-
-        if let Some(repo) = snapshot.repository_for_path(&path) {
-            let repo_path = repo.work_directory.relativize(self, &path).unwrap();
-            if let Some(repo) = self.git_repositories.get(&*repo.work_directory) {
-                let repo = repo.repo_ptr.to_owned();
-                index_task = Some(
-                    cx.background()
-                        .spawn(async move { repo.lock().load_index_text(&repo_path) }),
-                );
-            }
-        }
-
-        cx.spawn(|this, mut cx| async move {
+        cx.spawn(|this, cx| async move {
             let text = fs.load(&abs_path).await?;
+            let entry = entry.await?;
+
+            let mut index_task = None;
+            let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot());
+            if let Some(repo) = snapshot.repository_for_path(&path) {
+                let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap();
+                if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) {
+                    let repo = repo.repo_ptr.clone();
+                    index_task = Some(
+                        cx.background()
+                            .spawn(async move { repo.lock().load_index_text(&repo_path) }),
+                    );
+                }
+            }
 
             let diff_base = if let Some(index_task) = index_task {
                 index_task.await
@@ -895,17 +900,10 @@ impl LocalWorktree {
                 None
             };
 
-            // Eagerly populate the snapshot with an updated entry for the loaded file
-            let entry = this
-                .update(&mut cx, |this, cx| {
-                    this.as_local().unwrap().refresh_entry(path, None, cx)
-                })
-                .await?;
-
             Ok((
                 File {
                     entry_id: entry.id,
-                    worktree: handle,
+                    worktree: this,
                     path: entry.path,
                     mtime: entry.mtime,
                     is_local: true,
@@ -983,6 +981,19 @@ impl LocalWorktree {
         })
     }
 
+    /// Find the lowest path in the worktree's datastructures that is an ancestor
+    fn lowest_ancestor(&self, path: &Path) -> PathBuf {
+        let mut lowest_ancestor = None;
+        for path in path.ancestors() {
+            if self.entry_for_path(path).is_some() {
+                lowest_ancestor = Some(path.to_path_buf());
+                break;
+            }
+        }
+
+        lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
+    }
+
     pub fn create_entry(
         &self,
         path: impl Into<Arc<Path>>,
@@ -990,6 +1001,7 @@ impl LocalWorktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
         let path = path.into();
+        let lowest_ancestor = self.lowest_ancestor(&path);
         let abs_path = self.absolutize(&path);
         let fs = self.fs.clone();
         let write = cx.background().spawn(async move {
@@ -1003,10 +1015,31 @@ impl LocalWorktree {
 
         cx.spawn(|this, mut cx| async move {
             write.await?;
-            this.update(&mut cx, |this, cx| {
-                this.as_local_mut().unwrap().refresh_entry(path, None, cx)
-            })
-            .await
+            let (result, refreshes) = this.update(&mut cx, |this, cx| {
+                let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+                let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
+                for refresh_path in refresh_paths.ancestors() {
+                    if refresh_path == Path::new("") {
+                        continue;
+                    }
+                    let refresh_full_path = lowest_ancestor.join(refresh_path);
+
+                    refreshes.push(this.as_local_mut().unwrap().refresh_entry(
+                        refresh_full_path.into(),
+                        None,
+                        cx,
+                    ));
+                }
+                (
+                    this.as_local_mut().unwrap().refresh_entry(path, None, cx),
+                    refreshes,
+                )
+            });
+            for refresh in refreshes {
+                refresh.await.log_err();
+            }
+
+            result.await
         })
     }
 
@@ -1039,14 +1072,10 @@ impl LocalWorktree {
         cx: &mut ModelContext<Worktree>,
     ) -> Option<Task<Result<()>>> {
         let entry = self.entry_for_id(entry_id)?.clone();
-        let abs_path = self.abs_path.clone();
+        let abs_path = self.absolutize(&entry.path);
         let fs = self.fs.clone();
 
         let delete = cx.background().spawn(async move {
-            let mut abs_path = fs.canonicalize(&abs_path).await?;
-            if entry.path.file_name().is_some() {
-                abs_path = abs_path.join(&entry.path);
-            }
             if entry.is_file() {
                 fs.remove_file(&abs_path, Default::default()).await?;
             } else {
@@ -1059,19 +1088,18 @@ impl LocalWorktree {
                 )
                 .await?;
             }
-            anyhow::Ok(abs_path)
+            anyhow::Ok(entry.path)
         });
 
         Some(cx.spawn(|this, mut cx| async move {
-            let abs_path = delete.await?;
-            let (tx, mut rx) = barrier::channel();
+            let path = delete.await?;
             this.update(&mut cx, |this, _| {
                 this.as_local_mut()
                     .unwrap()
-                    .path_changes_tx
-                    .try_send((vec![abs_path], tx))
-            })?;
-            rx.recv().await;
+                    .refresh_entries_for_paths(vec![path])
+            })
+            .recv()
+            .await;
             Ok(())
         }))
     }
@@ -1135,34 +1163,48 @@ impl LocalWorktree {
         }))
     }
 
+    pub fn expand_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<Task<Result<()>>> {
+        let path = self.entry_for_id(entry_id)?.path.clone();
+        let mut refresh = self.refresh_entries_for_paths(vec![path]);
+        Some(cx.background().spawn(async move {
+            refresh.next().await;
+            Ok(())
+        }))
+    }
+
+    pub fn refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+        let (tx, rx) = barrier::channel();
+        self.scan_requests_tx
+            .try_send(ScanRequest {
+                relative_paths: paths,
+                done: tx,
+            })
+            .ok();
+        rx
+    }
+
+    pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) {
+        self.path_prefixes_to_scan_tx.try_send(path_prefix).ok();
+    }
+
     fn refresh_entry(
         &self,
         path: Arc<Path>,
         old_path: Option<Arc<Path>>,
         cx: &mut ModelContext<Worktree>,
     ) -> Task<Result<Entry>> {
-        let fs = self.fs.clone();
-        let abs_root_path = self.abs_path.clone();
-        let path_changes_tx = self.path_changes_tx.clone();
+        let paths = if let Some(old_path) = old_path.as_ref() {
+            vec![old_path.clone(), path.clone()]
+        } else {
+            vec![path.clone()]
+        };
+        let mut refresh = self.refresh_entries_for_paths(paths);
         cx.spawn_weak(move |this, mut cx| async move {
-            let abs_path = fs.canonicalize(&abs_root_path).await?;
-            let mut paths = Vec::with_capacity(2);
-            paths.push(if path.file_name().is_some() {
-                abs_path.join(&path)
-            } else {
-                abs_path.clone()
-            });
-            if let Some(old_path) = old_path {
-                paths.push(if old_path.file_name().is_some() {
-                    abs_path.join(&old_path)
-                } else {
-                    abs_path.clone()
-                });
-            }
-
-            let (tx, mut rx) = barrier::channel();
-            path_changes_tx.try_send((paths, tx))?;
-            rx.recv().await;
+            refresh.recv().await;
             this.upgrade(&cx)
                 .ok_or_else(|| anyhow!("worktree was dropped"))?
                 .update(&mut cx, |this, _| {
@@ -1331,7 +1373,7 @@ impl RemoteWorktree {
         self.completed_scan_id >= scan_id
     }
 
-    fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = Result<()>> {
+    pub(crate) fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = Result<()>> {
         let (tx, rx) = oneshot::channel();
         if self.observed_snapshot(scan_id) {
             let _ = tx.send(());
@@ -1470,7 +1512,7 @@ impl Snapshot {
                     break;
                 }
             }
-            new_entries_by_path.push_tree(cursor.suffix(&()), &());
+            new_entries_by_path.append(cursor.suffix(&()), &());
             new_entries_by_path
         };
 
@@ -1568,7 +1610,7 @@ impl Snapshot {
     }
 
     pub fn visible_file_count(&self) -> usize {
-        self.entries_by_path.summary().visible_file_count
+        self.entries_by_path.summary().non_ignored_file_count
     }
 
     fn traverse_from_offset(
@@ -1837,15 +1879,6 @@ impl LocalSnapshot {
         Some((path, self.git_repositories.get(&repo.work_directory_id())?))
     }
 
-    pub(crate) fn repo_for_metadata(
-        &self,
-        path: &Path,
-    ) -> Option<(&ProjectEntryId, &LocalRepositoryEntry)> {
-        self.git_repositories
-            .iter()
-            .find(|(_, repo)| repo.in_dot_git(path))
-    }
-
     fn build_update(
         &self,
         project_id: u64,
@@ -1981,57 +2014,6 @@ impl LocalSnapshot {
         entry
     }
 
-    #[must_use = "Changed paths must be used for diffing later"]
-    fn build_repo(&mut self, parent_path: Arc<Path>, fs: &dyn Fs) -> Option<Vec<Arc<Path>>> {
-        let abs_path = self.abs_path.join(&parent_path);
-        let work_dir: Arc<Path> = parent_path.parent().unwrap().into();
-
-        // Guard against repositories inside the repository metadata
-        if work_dir
-            .components()
-            .find(|component| component.as_os_str() == *DOT_GIT)
-            .is_some()
-        {
-            return None;
-        };
-
-        let work_dir_id = self
-            .entry_for_path(work_dir.clone())
-            .map(|entry| entry.id)?;
-
-        if self.git_repositories.get(&work_dir_id).is_some() {
-            return None;
-        }
-
-        let repo = fs.open_repo(abs_path.as_path())?;
-        let work_directory = RepositoryWorkDirectory(work_dir.clone());
-
-        let repo_lock = repo.lock();
-
-        self.repository_entries.insert(
-            work_directory.clone(),
-            RepositoryEntry {
-                work_directory: work_dir_id.into(),
-                branch: repo_lock.branch_name().map(Into::into),
-            },
-        );
-
-        let changed_paths = self.scan_statuses(repo_lock.deref(), &work_directory);
-
-        drop(repo_lock);
-
-        self.git_repositories.insert(
-            work_dir_id,
-            LocalRepositoryEntry {
-                git_dir_scan_id: 0,
-                repo_ptr: repo,
-                git_dir_path: parent_path.clone(),
-            },
-        );
-
-        Some(changed_paths)
-    }
-
     #[must_use = "Changed paths must be used for diffing later"]
     fn scan_statuses(
         &mut self,
@@ -2098,11 +2080,18 @@ impl LocalSnapshot {
 
         ignore_stack
     }
-}
 
-impl LocalSnapshot {
     #[cfg(test)]
-    pub fn check_invariants(&self) {
+    pub(crate) fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
+        self.entries_by_path
+            .cursor::<()>()
+            .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
+    }
+
+    #[cfg(test)]
+    pub fn check_invariants(&self, git_state: bool) {
+        use pretty_assertions::assert_eq;
+
         assert_eq!(
             self.entries_by_path
                 .cursor::<()>()
@@ -2122,7 +2111,7 @@ impl LocalSnapshot {
         for entry in self.entries_by_path.cursor::<()>() {
             if entry.is_file() {
                 assert_eq!(files.next().unwrap().inode, entry.inode);
-                if !entry.is_ignored {
+                if !entry.is_ignored && !entry.is_external {
                     assert_eq!(visible_files.next().unwrap().inode, entry.inode);
                 }
             }
@@ -2132,7 +2121,11 @@ impl LocalSnapshot {
         assert!(visible_files.next().is_none());
 
         let mut bfs_paths = Vec::new();
-        let mut stack = vec![Path::new("")];
+        let mut stack = self
+            .root_entry()
+            .map(|e| e.path.as_ref())
+            .into_iter()
+            .collect::<Vec<_>>();
         while let Some(path) = stack.pop() {
             bfs_paths.push(path);
             let ix = stack.len();
@@ -2154,12 +2147,15 @@ impl LocalSnapshot {
             .collect::<Vec<_>>();
         assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
 
-        for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
-            let ignore_parent_path = ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
-            assert!(self.entry_for_path(&ignore_parent_path).is_some());
-            assert!(self
-                .entry_for_path(ignore_parent_path.join(&*GITIGNORE))
-                .is_some());
+        if git_state {
+            for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
+                let ignore_parent_path =
+                    ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
+                assert!(self.entry_for_path(&ignore_parent_path).is_some());
+                assert!(self
+                    .entry_for_path(ignore_parent_path.join(&*GITIGNORE))
+                    .is_some());
+            }
         }
     }
 
@@ -2177,6 +2173,20 @@ impl LocalSnapshot {
 }
 
 impl BackgroundScannerState {
+    fn should_scan_directory(&self, entry: &Entry) -> bool {
+        (!entry.is_external && !entry.is_ignored)
+            || entry.path.file_name() == Some(&*DOT_GIT)
+            || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
+            || self
+                .paths_to_scan
+                .iter()
+                .any(|p| p.starts_with(&entry.path))
+            || self
+                .path_prefixes_to_scan
+                .iter()
+                .any(|p| entry.path.starts_with(p))
+    }
+
     fn reuse_entry_id(&mut self, entry: &mut Entry) {
         if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
             entry.id = removed_entry_id;
@@ -2187,17 +2197,24 @@ impl BackgroundScannerState {
 
     fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
         self.reuse_entry_id(&mut entry);
-        self.snapshot.insert_entry(entry, fs)
+        let entry = self.snapshot.insert_entry(entry, fs);
+        if entry.path.file_name() == Some(&DOT_GIT) {
+            self.build_repository(entry.path.clone(), fs);
+        }
+
+        #[cfg(test)]
+        self.snapshot.check_invariants(false);
+
+        entry
     }
 
-    #[must_use = "Changed paths must be used for diffing later"]
     fn populate_dir(
         &mut self,
-        parent_path: Arc<Path>,
+        parent_path: &Arc<Path>,
         entries: impl IntoIterator<Item = Entry>,
         ignore: Option<Arc<Gitignore>>,
         fs: &dyn Fs,
-    ) -> Option<Vec<Arc<Path>>> {
+    ) {
         let mut parent_entry = if let Some(parent_entry) = self
             .snapshot
             .entries_by_path
@@ -2209,15 +2226,13 @@ impl BackgroundScannerState {
                 "populating a directory {:?} that has been removed",
                 parent_path
             );
-            return None;
+            return;
         };
 
         match parent_entry.kind {
-            EntryKind::PendingDir => {
-                parent_entry.kind = EntryKind::Dir;
-            }
+            EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir,
             EntryKind::Dir => {}
-            _ => return None,
+            _ => return,
         }
 
         if let Some(ignore) = ignore {
@@ -2227,11 +2242,16 @@ impl BackgroundScannerState {
                 .insert(abs_parent_path, (ignore, false));
         }
 
+        self.scanned_dirs.insert(parent_entry.id);
         let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
         let mut entries_by_id_edits = Vec::new();
+        let mut dotgit_path = None;
+
+        for entry in entries {
+            if entry.path.file_name() == Some(&DOT_GIT) {
+                dotgit_path = Some(entry.path.clone());
+            }
 
-        for mut entry in entries {
-            self.reuse_entry_id(&mut entry);
             entries_by_id_edits.push(Edit::Insert(PathEntry {
                 id: entry.id,
                 path: entry.path.clone(),
@@ -2246,10 +2266,15 @@ impl BackgroundScannerState {
             .edit(entries_by_path_edits, &());
         self.snapshot.entries_by_id.edit(entries_by_id_edits, &());
 
-        if parent_path.file_name() == Some(&DOT_GIT) {
-            return self.snapshot.build_repo(parent_path, fs);
+        if let Some(dotgit_path) = dotgit_path {
+            self.build_repository(dotgit_path, fs);
         }
-        None
+        if let Err(ix) = self.changed_paths.binary_search(parent_path) {
+            self.changed_paths.insert(ix, parent_path.clone());
+        }
+
+        #[cfg(test)]
+        self.snapshot.check_invariants(false);
     }
 
     fn remove_path(&mut self, path: &Path) {
@@ -2259,7 +2284,7 @@ impl BackgroundScannerState {
             let mut cursor = self.snapshot.entries_by_path.cursor::<TraversalProgress>();
             new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &());
             removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &());
-            new_entries.push_tree(cursor.suffix(&()), &());
+            new_entries.append(cursor.suffix(&()), &());
         }
         self.snapshot.entries_by_path = new_entries;
 
@@ -2284,6 +2309,140 @@ impl BackgroundScannerState {
                 *needs_update = true;
             }
         }
+
+        #[cfg(test)]
+        self.snapshot.check_invariants(false);
+    }
+
+    fn reload_repositories(&mut self, changed_paths: &[Arc<Path>], fs: &dyn Fs) {
+        let scan_id = self.snapshot.scan_id;
+
+        // Find each of the .git directories that contain any of the given paths.
+        let mut prev_dot_git_dir = None;
+        for changed_path in changed_paths {
+            let Some(dot_git_dir) = changed_path
+                .ancestors()
+                .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else {
+                    continue;
+                };
+
+            // Avoid processing the same repository multiple times, if multiple paths
+            // within it have changed.
+            if prev_dot_git_dir == Some(dot_git_dir) {
+                continue;
+            }
+            prev_dot_git_dir = Some(dot_git_dir);
+
+            // If there is already a repository for this .git directory, reload
+            // the status for all of its files.
+            let repository = self
+                .snapshot
+                .git_repositories
+                .iter()
+                .find_map(|(entry_id, repo)| {
+                    (repo.git_dir_path.as_ref() == dot_git_dir).then(|| (*entry_id, repo.clone()))
+                });
+            match repository {
+                None => {
+                    self.build_repository(dot_git_dir.into(), fs);
+                }
+                Some((entry_id, repository)) => {
+                    if repository.git_dir_scan_id == scan_id {
+                        continue;
+                    }
+                    let Some(work_dir) = self
+                        .snapshot
+                        .entry_for_id(entry_id)
+                        .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
+
+                    log::info!("reload git repository {:?}", dot_git_dir);
+                    let repository = repository.repo_ptr.lock();
+                    let branch = repository.branch_name();
+                    repository.reload_index();
+
+                    self.snapshot
+                        .git_repositories
+                        .update(&entry_id, |entry| entry.git_dir_scan_id = scan_id);
+                    self.snapshot
+                        .snapshot
+                        .repository_entries
+                        .update(&work_dir, |entry| entry.branch = branch.map(Into::into));
+
+                    let changed_paths = self.snapshot.scan_statuses(&*repository, &work_dir);
+                    util::extend_sorted(
+                        &mut self.changed_paths,
+                        changed_paths,
+                        usize::MAX,
+                        Ord::cmp,
+                    )
+                }
+            }
+        }
+
+        // Remove any git repositories whose .git entry no longer exists.
+        let mut snapshot = &mut self.snapshot;
+        let mut repositories = mem::take(&mut snapshot.git_repositories);
+        let mut repository_entries = mem::take(&mut snapshot.repository_entries);
+        repositories.retain(|work_directory_id, _| {
+            snapshot
+                .entry_for_id(*work_directory_id)
+                .map_or(false, |entry| {
+                    snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
+                })
+        });
+        repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
+        snapshot.git_repositories = repositories;
+        snapshot.repository_entries = repository_entries;
+    }
+
+    fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
+        log::info!("build git repository {:?}", dot_git_path);
+
+        let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
+
+        // Guard against repositories inside the repository metadata
+        if work_dir_path.iter().any(|component| component == *DOT_GIT) {
+            return None;
+        };
+
+        let work_dir_id = self
+            .snapshot
+            .entry_for_path(work_dir_path.clone())
+            .map(|entry| entry.id)?;
+
+        if self.snapshot.git_repositories.get(&work_dir_id).is_some() {
+            return None;
+        }
+
+        let abs_path = self.snapshot.abs_path.join(&dot_git_path);
+        let repository = fs.open_repo(abs_path.as_path())?;
+        let work_directory = RepositoryWorkDirectory(work_dir_path.clone());
+
+        let repo_lock = repository.lock();
+        self.snapshot.repository_entries.insert(
+            work_directory.clone(),
+            RepositoryEntry {
+                work_directory: work_dir_id.into(),
+                branch: repo_lock.branch_name().map(Into::into),
+            },
+        );
+
+        let changed_paths = self
+            .snapshot
+            .scan_statuses(repo_lock.deref(), &work_directory);
+        drop(repo_lock);
+
+        self.snapshot.git_repositories.insert(
+            work_dir_id,
+            LocalRepositoryEntry {
+                git_dir_scan_id: 0,
+                repo_ptr: repository,
+                git_dir_path: dot_git_path.clone(),
+            },
+        );
+
+        util::extend_sorted(&mut self.changed_paths, changed_paths, usize::MAX, Ord::cmp);
+        Some(())
     }
 }
 
@@ -2570,12 +2729,27 @@ pub struct Entry {
     pub inode: u64,
     pub mtime: SystemTime,
     pub is_symlink: bool,
+
+    /// Whether this entry is ignored by Git.
+    ///
+    /// We only scan ignored entries once the directory is expanded and
+    /// exclude them from searches.
     pub is_ignored: bool,
+
+    /// Whether this entry's canonical path is outside of the worktree.
+    /// This means the entry is only accessible from the worktree root via a
+    /// symlink.
+    ///
+    /// We only scan entries outside of the worktree once the symlinked
+    /// directory is expanded. External entries are treated like gitignored
+    /// entries in that they are not included in searches.
+    pub is_external: bool,
     pub git_status: Option<GitFileStatus>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum EntryKind {
+    UnloadedDir,
     PendingDir,
     Dir,
     File(CharBag),
@@ -2624,16 +2798,17 @@ impl Entry {
             mtime: metadata.mtime,
             is_symlink: metadata.is_symlink,
             is_ignored: false,
+            is_external: false,
             git_status: None,
         }
     }
 
     pub fn is_dir(&self) -> bool {
-        matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir)
+        self.kind.is_dir()
     }
 
     pub fn is_file(&self) -> bool {
-        matches!(self.kind, EntryKind::File(_))
+        self.kind.is_file()
     }
 
     pub fn git_status(&self) -> Option<GitFileStatus> {
@@ -2641,19 +2816,40 @@ impl Entry {
     }
 }
 
+impl EntryKind {
+    pub fn is_dir(&self) -> bool {
+        matches!(
+            self,
+            EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir
+        )
+    }
+
+    pub fn is_unloaded(&self) -> bool {
+        matches!(self, EntryKind::UnloadedDir)
+    }
+
+    pub fn is_file(&self) -> bool {
+        matches!(self, EntryKind::File(_))
+    }
+}
+
 impl sum_tree::Item for Entry {
     type Summary = EntrySummary;
 
     fn summary(&self) -> Self::Summary {
-        let visible_count = if self.is_ignored { 0 } else { 1 };
+        let non_ignored_count = if self.is_ignored || self.is_external {
+            0
+        } else {
+            1
+        };
         let file_count;
-        let visible_file_count;
+        let non_ignored_file_count;
         if self.is_file() {
             file_count = 1;
-            visible_file_count = visible_count;
+            non_ignored_file_count = non_ignored_count;
         } else {
             file_count = 0;
-            visible_file_count = 0;
+            non_ignored_file_count = 0;
         }
 
         let mut statuses = GitStatuses::default();
@@ -2669,9 +2865,9 @@ impl sum_tree::Item for Entry {
         EntrySummary {
             max_path: self.path.clone(),
             count: 1,
-            visible_count,
+            non_ignored_count,
             file_count,
-            visible_file_count,
+            non_ignored_file_count,
             statuses,
         }
     }
@@ -2689,9 +2885,9 @@ impl sum_tree::KeyedItem for Entry {
 pub struct EntrySummary {
     max_path: Arc<Path>,
     count: usize,
-    visible_count: usize,
+    non_ignored_count: usize,
     file_count: usize,
-    visible_file_count: usize,
+    non_ignored_file_count: usize,
     statuses: GitStatuses,
 }
 
@@ -2700,9 +2896,9 @@ impl Default for EntrySummary {
         Self {
             max_path: Arc::from(Path::new("")),
             count: 0,
-            visible_count: 0,
+            non_ignored_count: 0,
             file_count: 0,
-            visible_file_count: 0,
+            non_ignored_file_count: 0,
             statuses: Default::default(),
         }
     }
@@ -2714,9 +2910,9 @@ impl sum_tree::Summary for EntrySummary {
     fn add_summary(&mut self, rhs: &Self, _: &()) {
         self.max_path = rhs.max_path.clone();
         self.count += rhs.count;
-        self.visible_count += rhs.visible_count;
+        self.non_ignored_count += rhs.non_ignored_count;
         self.file_count += rhs.file_count;
-        self.visible_file_count += rhs.visible_file_count;
+        self.non_ignored_file_count += rhs.non_ignored_file_count;
         self.statuses += rhs.statuses;
     }
 }
@@ -2784,7 +2980,8 @@ struct BackgroundScanner {
     fs: Arc<dyn Fs>,
     status_updates_tx: UnboundedSender<ScanState>,
     executor: Arc<executor::Background>,
-    refresh_requests_rx: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
+    scan_requests_rx: channel::Receiver<ScanRequest>,
+    path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
     next_entry_id: Arc<AtomicUsize>,
     phase: BackgroundScannerPhase,
 }
@@ -2803,17 +3000,22 @@ impl BackgroundScanner {
         fs: Arc<dyn Fs>,
         status_updates_tx: UnboundedSender<ScanState>,
         executor: Arc<executor::Background>,
-        refresh_requests_rx: channel::Receiver<(Vec<PathBuf>, barrier::Sender)>,
+        scan_requests_rx: channel::Receiver<ScanRequest>,
+        path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
     ) -> Self {
         Self {
             fs,
             status_updates_tx,
             executor,
-            refresh_requests_rx,
+            scan_requests_rx,
+            path_prefixes_to_scan_rx,
             next_entry_id,
             state: Mutex::new(BackgroundScannerState {
                 prev_snapshot: snapshot.snapshot.clone(),
                 snapshot,
+                scanned_dirs: Default::default(),
+                path_prefixes_to_scan: Default::default(),
+                paths_to_scan: Default::default(),
                 removed_entry_ids: Default::default(),
                 changed_paths: Default::default(),
             }),
@@ -2823,7 +3025,7 @@ impl BackgroundScanner {
 
     async fn run(
         &mut self,
-        mut events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>,
+        mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>,
     ) {
         use futures::FutureExt as _;
 
@@ -2868,6 +3070,7 @@ impl BackgroundScanner {
             path: Arc::from(Path::new("")),
             ignore_stack,
             ancestor_inodes: TreeSet::from_ordered_entries(root_inode),
+            is_external: false,
             scan_queue: scan_job_tx.clone(),
         }))
         .unwrap();
@@ -2884,9 +3087,9 @@ impl BackgroundScanner {
         // For these events, update events cannot be as precise, because we didn't
         // have the previous state loaded yet.
         self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
-        if let Poll::Ready(Some(events)) = futures::poll!(events_rx.next()) {
+        if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) {
             let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>();
-            while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) {
+            while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
                 paths.extend(more_events.into_iter().map(|e| e.path));
             }
             self.process_events(paths).await;
@@ -2898,17 +3101,36 @@ impl BackgroundScanner {
             select_biased! {
                 // Process any path refresh requests from the worktree. Prioritize
                 // these before handling changes reported by the filesystem.
-                request = self.refresh_requests_rx.recv().fuse() => {
-                    let Ok((paths, barrier)) = request else { break };
-                    if !self.process_refresh_request(paths.clone(), barrier).await {
+                request = self.scan_requests_rx.recv().fuse() => {
+                    let Ok(request) = request else { break };
+                    if !self.process_scan_request(request, false).await {
                         return;
                     }
                 }
 
-                events = events_rx.next().fuse() => {
+                path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => {
+                    let Ok(path_prefix) = path_prefix else { break };
+                    log::trace!("adding path prefix {:?}", path_prefix);
+
+                    let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await;
+                    if did_scan {
+                        let abs_path =
+                        {
+                            let mut state = self.state.lock();
+                            state.path_prefixes_to_scan.insert(path_prefix.clone());
+                            state.snapshot.abs_path.join(&path_prefix)
+                        };
+
+                        if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
+                            self.process_events(vec![abs_path]).await;
+                        }
+                    }
+                }
+
+                events = fs_events_rx.next().fuse() => {
                     let Some(events) = events else { break };
                     let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>();
-                    while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) {
+                    while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
                         paths.extend(more_events.into_iter().map(|e| e.path));
                     }
                     self.process_events(paths.clone()).await;
@@ -2917,54 +3139,155 @@ impl BackgroundScanner {
         }
     }
 
-    async fn process_refresh_request(&self, paths: Vec<PathBuf>, barrier: barrier::Sender) -> bool {
-        self.reload_entries_for_paths(paths, None).await;
-        self.send_status_update(false, Some(barrier))
+    async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
+        log::debug!("rescanning paths {:?}", request.relative_paths);
+
+        request.relative_paths.sort_unstable();
+        self.forcibly_load_paths(&request.relative_paths).await;
+
+        let root_path = self.state.lock().snapshot.abs_path.clone();
+        let root_canonical_path = match self.fs.canonicalize(&root_path).await {
+            Ok(path) => path,
+            Err(err) => {
+                log::error!("failed to canonicalize root path: {}", err);
+                return false;
+            }
+        };
+        let abs_paths = request
+            .relative_paths
+            .iter()
+            .map(|path| {
+                if path.file_name().is_some() {
+                    root_canonical_path.join(path)
+                } else {
+                    root_canonical_path.clone()
+                }
+            })
+            .collect::<Vec<_>>();
+
+        self.reload_entries_for_paths(
+            root_path,
+            root_canonical_path,
+            &request.relative_paths,
+            abs_paths,
+            None,
+        )
+        .await;
+        self.send_status_update(scanning, Some(request.done))
     }
 
-    async fn process_events(&mut self, paths: Vec<PathBuf>) {
+    async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
+        let root_path = self.state.lock().snapshot.abs_path.clone();
+        let root_canonical_path = match self.fs.canonicalize(&root_path).await {
+            Ok(path) => path,
+            Err(err) => {
+                log::error!("failed to canonicalize root path: {}", err);
+                return;
+            }
+        };
+
+        let mut relative_paths = Vec::with_capacity(abs_paths.len());
+        abs_paths.sort_unstable();
+        abs_paths.dedup_by(|a, b| a.starts_with(&b));
+        abs_paths.retain(|abs_path| {
+            let snapshot = &self.state.lock().snapshot;
+            {
+                let relative_path: Arc<Path> =
+                    if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
+                        path.into()
+                    } else {
+                        log::error!(
+                        "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
+                    );
+                        return false;
+                    };
+
+                let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
+                    snapshot
+                        .entry_for_path(parent)
+                        .map_or(false, |entry| entry.kind == EntryKind::Dir)
+                });
+                if !parent_dir_is_loaded {
+                    log::debug!("ignoring event {relative_path:?} within unloaded directory");
+                    return false;
+                }
+
+                relative_paths.push(relative_path);
+                true
+            }
+        });
+
+        if relative_paths.is_empty() {
+            return;
+        }
+
+        log::debug!("received fs events {:?}", relative_paths);
+
         let (scan_job_tx, scan_job_rx) = channel::unbounded();
-        let paths = self
-            .reload_entries_for_paths(paths, Some(scan_job_tx.clone()))
-            .await;
+        self.reload_entries_for_paths(
+            root_path,
+            root_canonical_path,
+            &relative_paths,
+            abs_paths,
+            Some(scan_job_tx.clone()),
+        )
+        .await;
         drop(scan_job_tx);
         self.scan_dirs(false, scan_job_rx).await;
 
-        self.update_ignore_statuses().await;
+        let (scan_job_tx, scan_job_rx) = channel::unbounded();
+        self.update_ignore_statuses(scan_job_tx).await;
+        self.scan_dirs(false, scan_job_rx).await;
 
         {
             let mut state = self.state.lock();
-
-            if let Some(paths) = paths {
-                for path in paths {
-                    self.reload_git_repo(&path, &mut *state, self.fs.as_ref());
-                }
+            state.reload_repositories(&relative_paths, self.fs.as_ref());
+            state.snapshot.completed_scan_id = state.snapshot.scan_id;
+            for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
+                state.scanned_dirs.remove(&entry_id);
             }
+        }
 
-            let mut snapshot = &mut state.snapshot;
-
-            let mut git_repositories = mem::take(&mut snapshot.git_repositories);
-            git_repositories.retain(|work_directory_id, _| {
-                snapshot
-                    .entry_for_id(*work_directory_id)
-                    .map_or(false, |entry| {
-                        snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
-                    })
-            });
-            snapshot.git_repositories = git_repositories;
+        self.send_status_update(false, None);
+    }
 
-            let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries);
-            git_repository_entries.retain(|_, entry| {
-                snapshot
-                    .git_repositories
-                    .get(&entry.work_directory.0)
-                    .is_some()
-            });
-            snapshot.snapshot.repository_entries = git_repository_entries;
-            snapshot.completed_scan_id = snapshot.scan_id;
+    async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
+        let (scan_job_tx, mut scan_job_rx) = channel::unbounded();
+        {
+            let mut state = self.state.lock();
+            let root_path = state.snapshot.abs_path.clone();
+            for path in paths {
+                for ancestor in path.ancestors() {
+                    if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
+                        if entry.kind == EntryKind::UnloadedDir {
+                            let abs_path = root_path.join(ancestor);
+                            let ignore_stack =
+                                state.snapshot.ignore_stack_for_abs_path(&abs_path, true);
+                            let ancestor_inodes =
+                                state.snapshot.ancestor_inodes_for_path(&ancestor);
+                            scan_job_tx
+                                .try_send(ScanJob {
+                                    abs_path: abs_path.into(),
+                                    path: ancestor.into(),
+                                    ignore_stack,
+                                    scan_queue: scan_job_tx.clone(),
+                                    ancestor_inodes,
+                                    is_external: entry.is_external,
+                                })
+                                .unwrap();
+                            state.paths_to_scan.insert(path.clone());
+                            break;
+                        }
+                    }
+                }
+            }
+            drop(scan_job_tx);
+        }
+        while let Some(job) = scan_job_rx.next().await {
+            self.scan_dir(&job).await.log_err();
         }
 
-        self.send_status_update(false, None);
+        mem::take(&mut self.state.lock().paths_to_scan).len() > 0
     }
 
     async fn scan_dirs(

crates/project/src/worktree_tests.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
     worktree::{Event, Snapshot, WorktreeHandle},
-    EntryKind, PathChange, Worktree,
+    Entry, EntryKind, PathChange, Worktree,
 };
 use anyhow::Result;
 use client::Client;
@@ -8,12 +8,14 @@ use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
 use git::GITIGNORE;
 use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext};
 use parking_lot::Mutex;
+use postage::stream::Stream;
 use pretty_assertions::assert_eq;
 use rand::prelude::*;
 use serde_json::json;
 use std::{
     env,
     fmt::Write,
+    mem,
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -34,11 +36,8 @@ async fn test_traversal(cx: &mut TestAppContext) {
     )
     .await;
 
-    let http_client = FakeHttpClient::with_404_response();
-    let client = cx.read(|cx| Client::new(http_client, cx));
-
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         Path::new("/root"),
         true,
         fs,
@@ -107,11 +106,8 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
     )
     .await;
 
-    let http_client = FakeHttpClient::with_404_response();
-    let client = cx.read(|cx| Client::new(http_client, cx));
-
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         Path::new("/root"),
         true,
         fs,
@@ -154,7 +150,18 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
                 .collect::<Vec<_>>(),
             vec![Path::new("g"), Path::new("g/h"),]
         );
+    });
 
+    // Expand gitignored directory.
+    tree.read_with(cx, |tree, _| {
+        tree.as_local()
+            .unwrap()
+            .refresh_entries_for_paths(vec![Path::new("i/j").into()])
+    })
+    .recv()
+    .await;
+
+    tree.read_with(cx, |tree, _| {
         assert_eq!(
             tree.descendent_entries(false, false, Path::new("i"))
                 .map(|entry| entry.path.as_ref())
@@ -196,9 +203,8 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
     fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
     fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
 
-    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         Path::new("/root"),
         true,
         fs.clone(),
@@ -257,40 +263,506 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
 }
 
 #[gpui::test]
-async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
-    // .gitignores are handled explicitly by Zed and do not use the git
-    // machinery that the git_tests module checks
-    let parent_dir = temp_tree(json!({
-        ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
-        "tree": {
-            ".git": {},
-            ".gitignore": "ignored-dir\n",
-            "tracked-dir": {
-                "tracked-file1": "",
-                "ancestor-ignored-file1": "",
+async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/root",
+        json!({
+            "dir1": {
+                "deps": {
+                    // symlinks here
+                },
+                "src": {
+                    "a.rs": "",
+                    "b.rs": "",
+                },
+            },
+            "dir2": {
+                "src": {
+                    "c.rs": "",
+                    "d.rs": "",
+                }
             },
-            "ignored-dir": {
-                "ignored-file1": ""
+            "dir3": {
+                "deps": {},
+                "src": {
+                    "e.rs": "",
+                    "f.rs": "",
+                },
             }
-        }
-    }));
-    let dir = parent_dir.path().join("tree");
+        }),
+    )
+    .await;
 
-    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+    // These symlinks point to directories outside of the worktree's root, dir1.
+    fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
+        .await;
+    fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
+        .await;
 
     let tree = Worktree::local(
-        client,
-        dir.as_path(),
+        build_client(cx),
+        Path::new("/root/dir1"),
         true,
-        Arc::new(RealFs),
+        fs.clone(),
         Default::default(),
         &mut cx.to_async(),
     )
     .await
     .unwrap();
+
     cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
         .await;
-    tree.flush_fs_events(cx).await;
+
+    let tree_updates = Arc::new(Mutex::new(Vec::new()));
+    tree.update(cx, |_, cx| {
+        let tree_updates = tree_updates.clone();
+        cx.subscribe(&tree, move |_, _, event, _| {
+            if let Event::UpdatedEntries(update) = event {
+                tree_updates.lock().extend(
+                    update
+                        .iter()
+                        .map(|(path, _, change)| (path.clone(), *change)),
+                );
+            }
+        })
+        .detach();
+    });
+
+    // The symlinked directories are not scanned by default.
+    tree.read_with(cx, |tree, _| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_external))
+                .collect::<Vec<_>>(),
+            vec![
+                (Path::new(""), false),
+                (Path::new("deps"), false),
+                (Path::new("deps/dep-dir2"), true),
+                (Path::new("deps/dep-dir3"), true),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+            ]
+        );
+
+        assert_eq!(
+            tree.entry_for_path("deps/dep-dir2").unwrap().kind,
+            EntryKind::UnloadedDir
+        );
+    });
+
+    // Expand one of the symlinked directories.
+    tree.read_with(cx, |tree, _| {
+        tree.as_local()
+            .unwrap()
+            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
+    })
+    .recv()
+    .await;
+
+    // The expanded directory's contents are loaded. Subdirectories are
+    // not scanned yet.
+    tree.read_with(cx, |tree, _| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_external))
+                .collect::<Vec<_>>(),
+            vec![
+                (Path::new(""), false),
+                (Path::new("deps"), false),
+                (Path::new("deps/dep-dir2"), true),
+                (Path::new("deps/dep-dir3"), true),
+                (Path::new("deps/dep-dir3/deps"), true),
+                (Path::new("deps/dep-dir3/src"), true),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+            ]
+        );
+    });
+    assert_eq!(
+        mem::take(&mut *tree_updates.lock()),
+        &[
+            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
+            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
+            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
+        ]
+    );
+
+    // Expand a subdirectory of one of the symlinked directories.
+    tree.read_with(cx, |tree, _| {
+        tree.as_local()
+            .unwrap()
+            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
+    })
+    .recv()
+    .await;
+
+    // The expanded subdirectory's contents are loaded.
+    tree.read_with(cx, |tree, _| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_external))
+                .collect::<Vec<_>>(),
+            vec![
+                (Path::new(""), false),
+                (Path::new("deps"), false),
+                (Path::new("deps/dep-dir2"), true),
+                (Path::new("deps/dep-dir3"), true),
+                (Path::new("deps/dep-dir3/deps"), true),
+                (Path::new("deps/dep-dir3/src"), true),
+                (Path::new("deps/dep-dir3/src/e.rs"), true),
+                (Path::new("deps/dep-dir3/src/f.rs"), true),
+                (Path::new("src"), false),
+                (Path::new("src/a.rs"), false),
+                (Path::new("src/b.rs"), false),
+            ]
+        );
+    });
+
+    assert_eq!(
+        mem::take(&mut *tree_updates.lock()),
+        &[
+            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
+            (
+                Path::new("deps/dep-dir3/src/e.rs").into(),
+                PathChange::Loaded
+            ),
+            (
+                Path::new("deps/dep-dir3/src/f.rs").into(),
+                PathChange::Loaded
+            )
+        ]
+    );
+}
+
+#[gpui::test]
+async fn test_open_gitignored_files(cx: &mut TestAppContext) {
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/root",
+        json!({
+            ".gitignore": "node_modules\n",
+            "one": {
+                "node_modules": {
+                    "a": {
+                        "a1.js": "a1",
+                        "a2.js": "a2",
+                    },
+                    "b": {
+                        "b1.js": "b1",
+                        "b2.js": "b2",
+                    },
+                    "c": {
+                        "c1.js": "c1",
+                        "c2.js": "c2",
+                    }
+                },
+            },
+            "two": {
+                "x.js": "",
+                "y.js": "",
+            },
+        }),
+    )
+    .await;
+
+    let tree = Worktree::local(
+        build_client(cx),
+        Path::new("/root"),
+        true,
+        fs.clone(),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+
+    tree.read_with(cx, |tree, _| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            vec![
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("one"), false),
+                (Path::new("one/node_modules"), true),
+                (Path::new("two"), false),
+                (Path::new("two/x.js"), false),
+                (Path::new("two/y.js"), false),
+            ]
+        );
+    });
+
+    // Open a file that is nested inside of a gitignored directory that
+    // has not yet been expanded.
+    let prev_read_dir_count = fs.read_dir_call_count();
+    let buffer = tree
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx)
+        })
+        .await
+        .unwrap();
+
+    tree.read_with(cx, |tree, cx| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            vec![
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("one"), false),
+                (Path::new("one/node_modules"), true),
+                (Path::new("one/node_modules/a"), true),
+                (Path::new("one/node_modules/b"), true),
+                (Path::new("one/node_modules/b/b1.js"), true),
+                (Path::new("one/node_modules/b/b2.js"), true),
+                (Path::new("one/node_modules/c"), true),
+                (Path::new("two"), false),
+                (Path::new("two/x.js"), false),
+                (Path::new("two/y.js"), false),
+            ]
+        );
+
+        assert_eq!(
+            buffer.read(cx).file().unwrap().path().as_ref(),
+            Path::new("one/node_modules/b/b1.js")
+        );
+
+        // Only the newly-expanded directories are scanned.
+        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
+    });
+
+    // Open another file in a different subdirectory of the same
+    // gitignored directory.
+    let prev_read_dir_count = fs.read_dir_call_count();
+    let buffer = tree
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx)
+        })
+        .await
+        .unwrap();
+
+    tree.read_with(cx, |tree, cx| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
+                .collect::<Vec<_>>(),
+            vec![
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("one"), false),
+                (Path::new("one/node_modules"), true),
+                (Path::new("one/node_modules/a"), true),
+                (Path::new("one/node_modules/a/a1.js"), true),
+                (Path::new("one/node_modules/a/a2.js"), true),
+                (Path::new("one/node_modules/b"), true),
+                (Path::new("one/node_modules/b/b1.js"), true),
+                (Path::new("one/node_modules/b/b2.js"), true),
+                (Path::new("one/node_modules/c"), true),
+                (Path::new("two"), false),
+                (Path::new("two/x.js"), false),
+                (Path::new("two/y.js"), false),
+            ]
+        );
+
+        assert_eq!(
+            buffer.read(cx).file().unwrap().path().as_ref(),
+            Path::new("one/node_modules/a/a2.js")
+        );
+
+        // Only the newly-expanded directory is scanned.
+        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
+    });
+
+    // No work happens when files and directories change within an unloaded directory.
+    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
+    fs.create_dir("/root/one/node_modules/c/lib".as_ref())
+        .await
+        .unwrap();
+    cx.foreground().run_until_parked();
+    assert_eq!(
+        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
+        0
+    );
+}
+
+#[gpui::test]
+async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/root",
+        json!({
+            ".gitignore": "node_modules\n",
+            "a": {
+                "a.js": "",
+            },
+            "b": {
+                "b.js": "",
+            },
+            "node_modules": {
+                "c": {
+                    "c.js": "",
+                },
+                "d": {
+                    "d.js": "",
+                    "e": {
+                        "e1.js": "",
+                        "e2.js": "",
+                    },
+                    "f": {
+                        "f1.js": "",
+                        "f2.js": "",
+                    }
+                },
+            },
+        }),
+    )
+    .await;
+
+    let tree = Worktree::local(
+        build_client(cx),
+        Path::new("/root"),
+        true,
+        fs.clone(),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+
+    // Open a file within the gitignored directory, forcing some of its
+    // subdirectories to be read, but not all.
+    let read_dir_count_1 = fs.read_dir_call_count();
+    tree.read_with(cx, |tree, _| {
+        tree.as_local()
+            .unwrap()
+            .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
+    })
+    .recv()
+    .await;
+
+    // Those subdirectories are now loaded.
+    tree.read_with(cx, |tree, _| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|e| (e.path.as_ref(), e.is_ignored))
+                .collect::<Vec<_>>(),
+            &[
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("a"), false),
+                (Path::new("a/a.js"), false),
+                (Path::new("b"), false),
+                (Path::new("b/b.js"), false),
+                (Path::new("node_modules"), true),
+                (Path::new("node_modules/c"), true),
+                (Path::new("node_modules/d"), true),
+                (Path::new("node_modules/d/d.js"), true),
+                (Path::new("node_modules/d/e"), true),
+                (Path::new("node_modules/d/f"), true),
+            ]
+        );
+    });
+    let read_dir_count_2 = fs.read_dir_call_count();
+    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
+
+    // Update the gitignore so that node_modules is no longer ignored,
+    // but a subdirectory is ignored
+    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
+        .await
+        .unwrap();
+    cx.foreground().run_until_parked();
+
+    // All of the directories that are no longer ignored are now loaded.
+    tree.read_with(cx, |tree, _| {
+        assert_eq!(
+            tree.entries(true)
+                .map(|e| (e.path.as_ref(), e.is_ignored))
+                .collect::<Vec<_>>(),
+            &[
+                (Path::new(""), false),
+                (Path::new(".gitignore"), false),
+                (Path::new("a"), false),
+                (Path::new("a/a.js"), false),
+                (Path::new("b"), false),
+                (Path::new("b/b.js"), false),
+                // This directory is no longer ignored
+                (Path::new("node_modules"), false),
+                (Path::new("node_modules/c"), false),
+                (Path::new("node_modules/c/c.js"), false),
+                (Path::new("node_modules/d"), false),
+                (Path::new("node_modules/d/d.js"), false),
+                // This subdirectory is now ignored
+                (Path::new("node_modules/d/e"), true),
+                (Path::new("node_modules/d/f"), false),
+                (Path::new("node_modules/d/f/f1.js"), false),
+                (Path::new("node_modules/d/f/f2.js"), false),
+            ]
+        );
+    });
+
+    // Each of the newly-loaded directories is scanned only once.
+    let read_dir_count_3 = fs.read_dir_call_count();
+    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/root",
+        json!({
+            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
+            "tree": {
+                ".git": {},
+                ".gitignore": "ignored-dir\n",
+                "tracked-dir": {
+                    "tracked-file1": "",
+                    "ancestor-ignored-file1": "",
+                },
+                "ignored-dir": {
+                    "ignored-file1": ""
+                }
+            }
+        }),
+    )
+    .await;
+
+    let tree = Worktree::local(
+        build_client(cx),
+        "/root/tree".as_ref(),
+        true,
+        fs.clone(),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+
+    tree.read_with(cx, |tree, _| {
+        tree.as_local()
+            .unwrap()
+            .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
+    })
+    .recv()
+    .await;
+
     cx.read(|cx| {
         let tree = tree.read(cx);
         assert!(
@@ -311,10 +783,26 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
         );
     });
 
-    std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
-    std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
-    std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
-    tree.flush_fs_events(cx).await;
+    fs.create_file(
+        "/root/tree/tracked-dir/tracked-file2".as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    fs.create_file(
+        "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    fs.create_file(
+        "/root/tree/ignored-dir/ignored-file2".as_ref(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+
+    cx.foreground().run_until_parked();
     cx.read(|cx| {
         let tree = tree.read(cx);
         assert!(
@@ -346,10 +834,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
         "ignored-dir": {}
     }));
 
-    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
-
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         dir.path(),
         true,
         Arc::new(RealFs),
@@ -393,8 +879,6 @@ async fn test_write_file(cx: &mut TestAppContext) {
 
 #[gpui::test(iterations = 30)]
 async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
-    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
-
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/root",
@@ -407,7 +891,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
     .await;
 
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         "/root".as_ref(),
         true,
         fs,
@@ -452,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+    let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+    let fs_fake = FakeFs::new(cx.background());
+    fs_fake
+        .insert_tree(
+            "/root",
+            json!({
+                "a": {},
+            }),
+        )
+        .await;
+
+    let tree_fake = Worktree::local(
+        client_fake,
+        "/root".as_ref(),
+        true,
+        fs_fake,
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let entry = tree_fake
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_fake.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+    });
+
+    let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+    let fs_real = Arc::new(RealFs);
+    let temp_root = temp_tree(json!({
+        "a": {}
+    }));
+
+    let tree_real = Worktree::local(
+        client_real,
+        temp_root.path(),
+        true,
+        fs_real,
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let entry = tree_real
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_real.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+    });
+
+    // Test smallest change
+    let entry = tree_real
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("a/b/c/e.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_real.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+    });
+
+    // Test largest change
+    let entry = tree_real
+        .update(cx, |tree, cx| {
+            tree.as_local_mut()
+                .unwrap()
+                .create_entry("d/e/f/g.txt".as_ref(), false, cx)
+        })
+        .await
+        .unwrap();
+    assert!(entry.is_file());
+
+    cx.foreground().run_until_parked();
+    tree_real.read_with(cx, |tree, _| {
+        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
+        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
+        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
+        assert!(tree.entry_for_path("d/").unwrap().is_dir());
+    });
+}
+
 #[gpui::test(iterations = 100)]
 async fn test_random_worktree_operations_during_initial_scan(
     cx: &mut TestAppContext,
@@ -472,9 +1069,8 @@ async fn test_random_worktree_operations_during_initial_scan(
     }
     log::info!("generated initial tree");
 
-    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
     let worktree = Worktree::local(
-        client.clone(),
+        build_client(cx),
         root_dir,
         true,
         fs.clone(),
@@ -506,7 +1102,7 @@ async fn test_random_worktree_operations_during_initial_scan(
             .await
             .log_err();
         worktree.read_with(cx, |tree, _| {
-            tree.as_local().unwrap().snapshot().check_invariants()
+            tree.as_local().unwrap().snapshot().check_invariants(true)
         });
 
         if rng.gen_bool(0.6) {
@@ -523,7 +1119,7 @@ async fn test_random_worktree_operations_during_initial_scan(
     let final_snapshot = worktree.read_with(cx, |tree, _| {
         let tree = tree.as_local().unwrap();
         let snapshot = tree.snapshot();
-        snapshot.check_invariants();
+        snapshot.check_invariants(true);
         snapshot
     });
 
@@ -562,9 +1158,8 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
     }
     log::info!("generated initial tree");
 
-    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
     let worktree = Worktree::local(
-        client.clone(),
+        build_client(cx),
         root_dir,
         true,
         fs.clone(),
@@ -627,12 +1222,17 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
     log::info!("quiescing");
     fs.as_fake().flush_events(usize::MAX);
     cx.foreground().run_until_parked();
+
     let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
-    snapshot.check_invariants();
+    snapshot.check_invariants(true);
+    let expanded_paths = snapshot
+        .expanded_entries()
+        .map(|e| e.path.clone())
+        .collect::<Vec<_>>();
 
     {
         let new_worktree = Worktree::local(
-            client.clone(),
+            build_client(cx),
             root_dir,
             true,
             fs.clone(),
@@ -644,6 +1244,14 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
         new_worktree
             .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
             .await;
+        new_worktree
+            .update(cx, |tree, _| {
+                tree.as_local_mut()
+                    .unwrap()
+                    .refresh_entries_for_paths(expanded_paths)
+            })
+            .recv()
+            .await;
         let new_snapshot =
             new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
         assert_eq!(
@@ -660,11 +1268,25 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
         }
 
         assert_eq!(
-            prev_snapshot.entries(true).collect::<Vec<_>>(),
-            snapshot.entries(true).collect::<Vec<_>>(),
+            prev_snapshot
+                .entries(true)
+                .map(ignore_pending_dir)
+                .collect::<Vec<_>>(),
+            snapshot
+                .entries(true)
+                .map(ignore_pending_dir)
+                .collect::<Vec<_>>(),
             "wrong updates after snapshot {i}: {updates:#?}",
         );
     }
+
+    fn ignore_pending_dir(entry: &Entry) -> Entry {
+        let mut entry = entry.clone();
+        if entry.kind.is_dir() {
+            entry.kind = EntryKind::Dir
+        }
+        entry
+    }
 }
 
 // The worktree's `UpdatedEntries` event can be used to follow along with
@@ -679,7 +1301,6 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Workt
                     Ok(ix) | Err(ix) => ix,
                 };
                 match change_type {
-                    PathChange::Loaded => entries.insert(ix, entry.unwrap()),
                     PathChange::Added => entries.insert(ix, entry.unwrap()),
                     PathChange::Removed => drop(entries.remove(ix)),
                     PathChange::Updated => {
@@ -688,7 +1309,7 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Workt
                         assert_eq!(existing_entry.path, entry.path);
                         *existing_entry = entry;
                     }
-                    PathChange::AddedOrUpdated => {
+                    PathChange::AddedOrUpdated | PathChange::Loaded => {
                         let entry = entry.unwrap();
                         if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
                             *entries.get_mut(ix).unwrap() = entry;
@@ -947,10 +1568,8 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
     }));
     let root_path = root.path();
 
-    let http_client = FakeHttpClient::with_404_response();
-    let client = cx.read(|cx| Client::new(http_client, cx));
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         root_path,
         true,
         Arc::new(RealFs),
@@ -1026,10 +1645,8 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
         },
     }));
 
-    let http_client = FakeHttpClient::with_404_response();
-    let client = cx.read(|cx| Client::new(http_client, cx));
     let tree = Worktree::local(
-        client,
+        build_client(cx),
         root.path(),
         true,
         Arc::new(RealFs),
@@ -1150,39 +1767,37 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
 
     }));
 
-    let http_client = FakeHttpClient::with_404_response();
-    let client = cx.read(|cx| Client::new(http_client, cx));
-    let tree = Worktree::local(
-        client,
-        root.path(),
-        true,
-        Arc::new(RealFs),
-        Default::default(),
-        &mut cx.to_async(),
-    )
-    .await
-    .unwrap();
-
-    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
-        .await;
-
     const A_TXT: &'static str = "a.txt";
     const B_TXT: &'static str = "b.txt";
     const E_TXT: &'static str = "c/d/e.txt";
     const F_TXT: &'static str = "f.txt";
     const DOTGITIGNORE: &'static str = ".gitignore";
     const BUILD_FILE: &'static str = "target/build_file";
-    let project_path: &Path = &Path::new("project");
+    let project_path = Path::new("project");
 
+    // Set up git repository before creating the worktree.
     let work_dir = root.path().join("project");
     let mut repo = git_init(work_dir.as_path());
     repo.add_ignore_rule(IGNORE_RULE).unwrap();
-    git_add(Path::new(A_TXT), &repo);
-    git_add(Path::new(E_TXT), &repo);
-    git_add(Path::new(DOTGITIGNORE), &repo);
+    git_add(A_TXT, &repo);
+    git_add(E_TXT, &repo);
+    git_add(DOTGITIGNORE, &repo);
     git_commit("Initial commit", &repo);
 
+    let tree = Worktree::local(
+        build_client(cx),
+        root.path(),
+        true,
+        Arc::new(RealFs),
+        Default::default(),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
     tree.flush_fs_events(cx).await;
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
     deterministic.run_until_parked();
 
     // Check that the right git state is observed on startup
@@ -1202,39 +1817,39 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
         );
     });
 
+    // Modify a file in the working copy.
     std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
-
     tree.flush_fs_events(cx).await;
     deterministic.run_until_parked();
 
+    // The worktree detects that the file's git status has changed.
     tree.read_with(cx, |tree, _cx| {
         let snapshot = tree.snapshot();
-
         assert_eq!(
             snapshot.status_for_file(project_path.join(A_TXT)),
             Some(GitFileStatus::Modified)
         );
     });
 
-    git_add(Path::new(A_TXT), &repo);
-    git_add(Path::new(B_TXT), &repo);
+    // Create a commit in the git repository.
+    git_add(A_TXT, &repo);
+    git_add(B_TXT, &repo);
     git_commit("Committing modified and added", &repo);
     tree.flush_fs_events(cx).await;
     deterministic.run_until_parked();
 
-    // Check that repo only changes are tracked
+    // The worktree detects that the files' git status have changed.
     tree.read_with(cx, |tree, _cx| {
         let snapshot = tree.snapshot();
-
         assert_eq!(
             snapshot.status_for_file(project_path.join(F_TXT)),
             Some(GitFileStatus::Added)
         );
-
         assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
         assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
     });
 
+    // Modify files in the working copy and perform git operations on other files.
     git_reset(0, &repo);
     git_remove_index(Path::new(B_TXT), &repo);
     git_stash(&mut repo);

crates/project_panel/Cargo.toml πŸ”—

@@ -27,6 +27,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 anyhow.workspace = true
 schemars.workspace = true
+pretty_assertions.workspace = true
 unicase = "2.6"
 
 [dev-dependencies]

crates/project_panel/src/project_panel.rs πŸ”—

@@ -64,7 +64,7 @@ pub struct ProjectPanel {
     pending_serialization: Task<Option<()>>,
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
 struct Selection {
     worktree_id: WorktreeId,
     entry_id: ProjectEntryId,
@@ -153,6 +153,7 @@ pub fn init(cx: &mut AppContext) {
     );
 }
 
+#[derive(Debug)]
 pub enum Event {
     OpenedEntry {
         entry_id: ProjectEntryId,
@@ -410,17 +411,23 @@ impl ProjectPanel {
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
         if let Some((worktree, entry)) = self.selected_entry(cx) {
             if entry.is_dir() {
+                let worktree_id = worktree.id();
+                let entry_id = entry.id;
                 let expanded_dir_ids =
-                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
+                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
                         expanded_dir_ids
                     } else {
                         return;
                     };
 
-                match expanded_dir_ids.binary_search(&entry.id) {
+                match expanded_dir_ids.binary_search(&entry_id) {
                     Ok(_) => self.select_next(&SelectNext, cx),
                     Err(ix) => {
-                        expanded_dir_ids.insert(ix, entry.id);
+                        self.project.update(cx, |project, cx| {
+                            project.expand_entry(worktree_id, entry_id, cx);
+                        });
+
+                        expanded_dir_ids.insert(ix, entry_id);
                         self.update_visible_entries(None, cx);
                         cx.notify();
                     }
@@ -431,18 +438,20 @@ impl ProjectPanel {
 
     fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
         if let Some((worktree, mut entry)) = self.selected_entry(cx) {
+            let worktree_id = worktree.id();
             let expanded_dir_ids =
-                if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
+                if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
                     expanded_dir_ids
                 } else {
                     return;
                 };
 
             loop {
-                match expanded_dir_ids.binary_search(&entry.id) {
+                let entry_id = entry.id;
+                match expanded_dir_ids.binary_search(&entry_id) {
                     Ok(ix) => {
                         expanded_dir_ids.remove(ix);
-                        self.update_visible_entries(Some((worktree.id(), entry.id)), cx);
+                        self.update_visible_entries(Some((worktree_id, entry_id)), cx);
                         cx.notify();
                         break;
                     }
@@ -463,14 +472,17 @@ impl ProjectPanel {
     fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
         if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
             if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
-                match expanded_dir_ids.binary_search(&entry_id) {
-                    Ok(ix) => {
-                        expanded_dir_ids.remove(ix);
-                    }
-                    Err(ix) => {
-                        expanded_dir_ids.insert(ix, entry_id);
+                self.project.update(cx, |project, cx| {
+                    match expanded_dir_ids.binary_search(&entry_id) {
+                        Ok(ix) => {
+                            expanded_dir_ids.remove(ix);
+                        }
+                        Err(ix) => {
+                            project.expand_entry(worktree_id, entry_id, cx);
+                            expanded_dir_ids.insert(ix, entry_id);
+                        }
                     }
-                }
+                });
                 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
                 cx.focus_self();
                 cx.notify();
@@ -535,7 +547,7 @@ impl ProjectPanel {
                 worktree_id,
                 entry_id: NEW_ENTRY_ID,
             });
-            let new_path = entry.path.join(&filename);
+            let new_path = entry.path.join(&filename.trim_start_matches("/"));
             if path_already_exists(new_path.as_path()) {
                 return None;
             }
@@ -576,6 +588,7 @@ impl ProjectPanel {
                     if selection.entry_id == edited_entry_id {
                         selection.worktree_id = worktree_id;
                         selection.entry_id = new_entry.id;
+                        this.expand_to_selection(cx);
                     }
                 }
                 this.update_visible_entries(None, cx);
@@ -938,10 +951,37 @@ impl ProjectPanel {
     }
 
     fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
+        let (worktree, entry) = self.selected_entry_handle(cx)?;
+        Some((worktree.read(cx), entry))
+    }
+
+    fn selected_entry_handle<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> Option<(ModelHandle<Worktree>, &'a project::Entry)> {
         let selection = self.selection?;
         let project = self.project.read(cx);
-        let worktree = project.worktree_for_id(selection.worktree_id, cx)?.read(cx);
-        Some((worktree, worktree.entry_for_id(selection.entry_id)?))
+        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
+        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
+        Some((worktree, entry))
+    }
+
+    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let (worktree, entry) = self.selected_entry(cx)?;
+        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
+
+        for path in entry.path.ancestors() {
+            let Some(entry) = worktree.entry_for_path(path) else {
+                continue;
+            };
+            if entry.is_dir() {
+                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
+                    expanded_dir_ids.insert(idx, entry.id);
+                }
+            }
+        }
+
+        Some(())
     }
 
     fn update_visible_entries(
@@ -1002,6 +1042,7 @@ impl ProjectPanel {
                         mtime: entry.mtime,
                         is_symlink: false,
                         is_ignored: false,
+                        is_external: false,
                         git_status: entry.git_status,
                     });
                 }
@@ -1058,29 +1099,31 @@ impl ProjectPanel {
         entry_id: ProjectEntryId,
         cx: &mut ViewContext<Self>,
     ) {
-        let project = self.project.read(cx);
-        if let Some((worktree, expanded_dir_ids)) = project
-            .worktree_for_id(worktree_id, cx)
-            .zip(self.expanded_dir_ids.get_mut(&worktree_id))
-        {
-            let worktree = worktree.read(cx);
+        self.project.update(cx, |project, cx| {
+            if let Some((worktree, expanded_dir_ids)) = project
+                .worktree_for_id(worktree_id, cx)
+                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
+            {
+                project.expand_entry(worktree_id, entry_id, cx);
+                let worktree = worktree.read(cx);
 
-            if let Some(mut entry) = worktree.entry_for_id(entry_id) {
-                loop {
-                    if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
-                        expanded_dir_ids.insert(ix, entry.id);
-                    }
+                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
+                    loop {
+                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
+                            expanded_dir_ids.insert(ix, entry.id);
+                        }
 
-                    if let Some(parent_entry) =
-                        entry.path.parent().and_then(|p| worktree.entry_for_path(p))
-                    {
-                        entry = parent_entry;
-                    } else {
-                        break;
+                        if let Some(parent_entry) =
+                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
+                        {
+                            entry = parent_entry;
+                        } else {
+                            break;
+                        }
                     }
                 }
             }
-        }
+        });
     }
 
     fn for_each_visible_entry(
@@ -1190,7 +1233,7 @@ impl ProjectPanel {
 
         Flex::row()
             .with_child(
-                if kind == EntryKind::Dir {
+                if kind.is_dir() {
                     if details.is_expanded {
                         Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
                     } else {
@@ -1253,7 +1296,10 @@ impl ProjectPanel {
         let show_editor = details.is_editing && !details.is_processing;
 
         MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
-            let mut style = entry_style.style_for(state, details.is_selected).clone();
+            let mut style = entry_style
+                .in_state(details.is_selected)
+                .style_for(state)
+                .clone();
 
             if cx
                 .global::<DragAndDrop<Workspace>>()
@@ -1264,7 +1310,7 @@ impl ProjectPanel {
                     .filter(|destination| details.path.starts_with(destination))
                     .is_some()
             {
-                style = entry_style.active.clone().unwrap();
+                style = entry_style.active_state().default.clone();
             }
 
             let row_container_style = if show_editor {
@@ -1284,7 +1330,7 @@ impl ProjectPanel {
         })
         .on_click(MouseButton::Left, move |event, this, cx| {
             if !show_editor {
-                if kind == EntryKind::Dir {
+                if kind.is_dir() {
                     this.toggle_expanded(entry_id, cx);
                 } else {
                     this.open_entry(entry_id, event.click_count > 1, cx);
@@ -1405,9 +1451,11 @@ impl View for ProjectPanel {
                         let button_style = theme.open_project_button.clone();
                         let context_menu_item_style = theme::current(cx).context_menu.item.clone();
                         move |state, cx| {
-                            let button_style = button_style.style_for(state, false).clone();
-                            let context_menu_item =
-                                context_menu_item_style.style_for(state, true).clone();
+                            let button_style = button_style.style_for(state).clone();
+                            let context_menu_item = context_menu_item_style
+                                .active_state()
+                                .style_for(state)
+                                .clone();
 
                             theme::ui::keystroke_label(
                                 "Open a project",
@@ -1563,6 +1611,7 @@ impl ClipboardEntry {
 mod tests {
     use super::*;
     use gpui::{TestAppContext, ViewHandle};
+    use pretty_assertions::assert_eq;
     use project::FakeFs;
     use serde_json::json;
     use settings::SettingsStore;
@@ -1973,6 +2022,133 @@ mod tests {
         );
     }
 
+    #[gpui::test(iterations = 30)]
+    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                ".dockerignore": "",
+                ".git": {
+                    "HEAD": "",
+                },
+                "a": {
+                    "0": { "q": "", "r": "", "s": "" },
+                    "1": { "t": "", "u": "" },
+                    "2": { "v": "", "w": "", "x": "", "y": "" },
+                },
+                "b": {
+                    "3": { "Q": "" },
+                    "4": { "R": "", "S": "", "T": "", "U": "" },
+                },
+                "C": {
+                    "5": {},
+                    "6": { "V": "", "W": "" },
+                    "7": { "X": "" },
+                    "8": { "Y": {}, "Z": "" }
+                }
+            }),
+        )
+        .await;
+        fs.insert_tree(
+            "/root2",
+            json!({
+                "d": {
+                    "9": ""
+                },
+                "e": {}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1  <== selected",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        // Add a file with the root folder selected. The filename editor is placed
+        // before the first file in the root folder.
+        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+        cx.read_window(window_id, |cx| {
+            let panel = panel.read(cx);
+            assert!(panel.filename_editor.is_focused(cx));
+        });
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [EDITOR: '']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        let confirm = panel.update(cx, |panel, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                editor.set_text("/bdir1/dir2/the-new-filename", cx)
+            });
+            panel.confirm(&Confirm, cx).unwrap()
+        });
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    > C",
+                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..13, cx),
+            &[
+                "v root1",
+                "    > .git",
+                "    > a",
+                "    > b",
+                "    v bdir1",
+                "        v dir2",
+                "              the-new-filename  <== selected",
+                "    > C",
+                "      .dockerignore",
+                "v root2",
+                "    > d",
+                "    > e",
+            ]
+        );
+    }
+
     #[gpui::test]
     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
         init_test(cx);
@@ -2343,7 +2519,7 @@ mod tests {
                 }
 
                 let indent = "    ".repeat(details.depth);
-                let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) {
+                let icon = if details.kind.is_dir() {
                     if details.is_expanded {
                         "v "
                     } else {

crates/project_symbols/src/project_symbols.rs πŸ”—

@@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
         let style = &theme.picker.item;
-        let current_style = style.style_for(mouse_state, selected);
+        let current_style = style.in_state(selected).style_for(mouse_state);
 
         let string_match = &self.matches[ix];
         let symbol = &self.symbols[string_match.candidate_id];
@@ -229,7 +229,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
             .with_child(
                 // Avoid styling the path differently when it is selected, since
                 // the symbol's syntax highlighting doesn't change when selected.
-                Label::new(path.to_string(), style.default.label.clone()),
+                Label::new(
+                    path.to_string(),
+                    style.inactive_state().default.label.clone(),
+                ),
             )
             .contained()
             .with_style(current_style.container)

crates/recent_projects/Cargo.toml πŸ”—

@@ -21,6 +21,7 @@ util = { path = "../util"}
 theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 
+futures.workspace = true
 ordered-float.workspace = true
 postage.workspace = true
 smol.workspace = true

crates/recent_projects/src/recent_projects.rs πŸ”—

@@ -48,7 +48,7 @@ fn toggle(
                     let workspace = cx.weak_handle();
                     cx.add_view(|cx| {
                         RecentProjects::new(
-                            RecentProjectsDelegate::new(workspace, workspace_locations),
+                            RecentProjectsDelegate::new(workspace, workspace_locations, true),
                             cx,
                         )
                         .with_max_size(800., 1200.)
@@ -64,25 +64,40 @@ fn toggle(
     }))
 }
 
-type RecentProjects = Picker<RecentProjectsDelegate>;
+pub fn build_recent_projects(
+    workspace: WeakViewHandle<Workspace>,
+    workspaces: Vec<WorkspaceLocation>,
+    cx: &mut ViewContext<RecentProjects>,
+) -> RecentProjects {
+    Picker::new(
+        RecentProjectsDelegate::new(workspace, workspaces, false),
+        cx,
+    )
+    .with_theme(|theme| theme.picker.clone())
+}
+
+pub type RecentProjects = Picker<RecentProjectsDelegate>;
 
-struct RecentProjectsDelegate {
+pub struct RecentProjectsDelegate {
     workspace: WeakViewHandle<Workspace>,
     workspace_locations: Vec<WorkspaceLocation>,
     selected_match_index: usize,
     matches: Vec<StringMatch>,
+    render_paths: bool,
 }
 
 impl RecentProjectsDelegate {
     fn new(
         workspace: WeakViewHandle<Workspace>,
         workspace_locations: Vec<WorkspaceLocation>,
+        render_paths: bool,
     ) -> Self {
         Self {
             workspace,
             workspace_locations,
             selected_match_index: 0,
             matches: Default::default(),
+            render_paths,
         }
     }
 }
@@ -173,7 +188,7 @@ impl PickerDelegate for RecentProjectsDelegate {
         cx: &gpui::AppContext,
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
         let string_match = &self.matches[ix];
 
@@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 highlighted_location
                     .paths
                     .into_iter()
+                    .filter(|_| self.render_paths)
                     .map(|highlighted_path| highlighted_path.render(style.label.clone())),
             )
             .flex(1., false)

crates/rope/src/rope.rs πŸ”—

@@ -53,7 +53,7 @@ impl Rope {
             }
         }
 
-        self.chunks.push_tree(chunks.suffix(&()), &());
+        self.chunks.append(chunks.suffix(&()), &());
         self.check_invariants();
     }
 
@@ -384,6 +384,12 @@ impl<'a> From<&'a str> for Rope {
     }
 }
 
+impl From<String> for Rope {
+    fn from(text: String) -> Self {
+        Rope::from(text.as_str())
+    }
+}
+
 impl fmt::Display for Rope {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         for chunk in self.chunks() {

crates/rpc/proto/zed.proto πŸ”—

@@ -63,6 +63,8 @@ message Envelope {
         CopyProjectEntry copy_project_entry = 47;
         DeleteProjectEntry delete_project_entry = 48;
         ProjectEntryResponse project_entry_response = 49;
+        ExpandProjectEntry expand_project_entry = 114;
+        ExpandProjectEntryResponse expand_project_entry_response = 115;
 
         UpdateDiagnosticSummary update_diagnostic_summary = 50;
         StartLanguageServer start_language_server = 51;
@@ -134,6 +136,10 @@ message Envelope {
         OnTypeFormattingResponse on_type_formatting_response = 112;
 
         UpdateWorktreeSettings update_worktree_settings = 113;
+
+        InlayHints inlay_hints = 116;
+        InlayHintsResponse inlay_hints_response = 117;
+        RefreshInlayHints refresh_inlay_hints = 118;
     }
 }
 
@@ -372,6 +378,15 @@ message DeleteProjectEntry {
     uint64 entry_id = 2;
 }
 
+message ExpandProjectEntry {
+    uint64 project_id = 1;
+    uint64 entry_id = 2;
+}
+
+message ExpandProjectEntryResponse {
+    uint64 worktree_scan_id = 1;
+}
+
 message ProjectEntryResponse {
     Entry entry = 1;
     uint64 worktree_scan_id = 2;
@@ -694,6 +709,68 @@ message OnTypeFormattingResponse {
     Transaction transaction = 1;
 }
 
+message InlayHints {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor start = 3;
+    Anchor end = 4;
+    repeated VectorClockEntry version = 5;
+}
+
+message InlayHintsResponse {
+    repeated InlayHint hints = 1;
+    repeated VectorClockEntry version = 2;
+}
+
+message InlayHint {
+    Anchor position = 1;
+    InlayHintLabel label = 2;
+    optional string kind = 3;
+    bool padding_left = 4;
+    bool padding_right = 5;
+    InlayHintTooltip tooltip = 6;
+}
+
+message InlayHintLabel {
+    oneof label {
+        string value = 1;
+        InlayHintLabelParts label_parts = 2;
+    }
+}
+
+message InlayHintLabelParts {
+    repeated InlayHintLabelPart parts = 1;
+}
+
+message InlayHintLabelPart {
+    string value = 1;
+    InlayHintLabelPartTooltip tooltip = 2;
+    Location location = 3;
+}
+
+message InlayHintTooltip {
+    oneof content {
+        string value = 1;
+        MarkupContent markup_content = 2;
+    }
+}
+
+message InlayHintLabelPartTooltip {
+    oneof content {
+        string value = 1;
+        MarkupContent markup_content = 2;
+    }
+}
+
+message RefreshInlayHints {
+    uint64 project_id = 1;
+}
+
+message MarkupContent {
+    string kind = 1;
+    string value = 2;
+}
+
 message PerformRenameResponse {
     ProjectTransaction transaction = 2;
 }
@@ -1005,7 +1082,8 @@ message Entry {
     Timestamp mtime = 5;
     bool is_symlink = 6;
     bool is_ignored = 7;
-    optional GitStatus git_status = 8;
+    bool is_external = 8;
+    optional GitStatus git_status = 9;
 }
 
 message RepositoryEntry {

crates/rpc/src/proto.rs πŸ”—

@@ -150,6 +150,7 @@ messages!(
     (DeclineCall, Foreground),
     (DeleteProjectEntry, Foreground),
     (Error, Foreground),
+    (ExpandProjectEntry, Foreground),
     (Follow, Foreground),
     (FollowResponse, Foreground),
     (FormatBuffers, Foreground),
@@ -197,9 +198,13 @@ messages!(
     (PerformRenameResponse, Background),
     (OnTypeFormatting, Background),
     (OnTypeFormattingResponse, Background),
+    (InlayHints, Background),
+    (InlayHintsResponse, Background),
+    (RefreshInlayHints, Foreground),
     (Ping, Foreground),
     (PrepareRename, Background),
     (PrepareRenameResponse, Background),
+    (ExpandProjectEntryResponse, Foreground),
     (ProjectEntryResponse, Foreground),
     (RejoinRoom, Foreground),
     (RejoinRoomResponse, Foreground),
@@ -255,6 +260,7 @@ request_messages!(
     (CreateRoom, CreateRoomResponse),
     (DeclineCall, Ack),
     (DeleteProjectEntry, ProjectEntryResponse),
+    (ExpandProjectEntry, ExpandProjectEntryResponse),
     (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
@@ -283,6 +289,8 @@ request_messages!(
     (PerformRename, PerformRenameResponse),
     (PrepareRename, PrepareRenameResponse),
     (OnTypeFormatting, OnTypeFormattingResponse),
+    (InlayHints, InlayHintsResponse),
+    (RefreshInlayHints, Ack),
     (ReloadBuffers, ReloadBuffersResponse),
     (RequestContact, Ack),
     (RemoveContact, Ack),
@@ -311,6 +319,7 @@ entity_messages!(
     CreateBufferForPeer,
     CreateProjectEntry,
     DeleteProjectEntry,
+    ExpandProjectEntry,
     Follow,
     FormatBuffers,
     GetCodeActions,
@@ -328,6 +337,8 @@ entity_messages!(
     OpenBufferForSymbol,
     PerformRename,
     OnTypeFormatting,
+    InlayHints,
+    RefreshInlayHints,
     PrepareRename,
     ReloadBuffers,
     RemoveProjectCollaborator,

crates/rpc/src/rpc.rs πŸ”—

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 58;
+pub const PROTOCOL_VERSION: u32 = 59;

crates/search/src/buffer_search.rs πŸ”—

@@ -259,7 +259,11 @@ impl BufferSearchBar {
         }
     }
 
-    fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
+    pub fn is_dismissed(&self) -> bool {
+        self.dismissed
+    }
+
+    pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
         self.dismissed = true;
         for searchable_item in self.seachable_items_with_matches.keys() {
             if let Some(searchable_item) =
@@ -275,7 +279,7 @@ impl BufferSearchBar {
         cx.notify();
     }
 
-    fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
+    pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
         let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
             SearchableItemHandle::boxed_clone(searchable_item.as_ref())
         } else {
@@ -328,7 +332,11 @@ impl BufferSearchBar {
         Some(
             MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
                 let theme = theme::current(cx);
-                let style = theme.search.option_button.style_for(state, is_active);
+                let style = theme
+                    .search
+                    .option_button
+                    .in_state(is_active)
+                    .style_for(state);
                 Label::new(icon, style.text.clone())
                     .contained()
                     .with_style(style.container)
@@ -371,7 +379,7 @@ impl BufferSearchBar {
         enum NavButton {}
         MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.option_button.style_for(state, false);
+            let style = theme.search.option_button.inactive_state().style_for(state);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -403,7 +411,7 @@ impl BufferSearchBar {
 
         enum CloseButton {}
         MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
-            let style = theme.dismiss_button.style_for(state, false);
+            let style = theme.dismiss_button.style_for(state);
             Svg::new("icons/x_mark_8.svg")
                 .with_color(style.color)
                 .constrained()
@@ -480,7 +488,7 @@ impl BufferSearchBar {
         self.select_match(Direction::Prev, cx);
     }
 
-    fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+    pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
             if let Some(searchable_item) = self.active_searchable_item.as_ref() {
                 if let Some(matches) = self

crates/search/src/project_search.rs πŸ”—

@@ -896,7 +896,7 @@ impl ProjectSearchBar {
         enum NavButton {}
         MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.option_button.style_for(state, false);
+            let style = theme.search.option_button.inactive_state().style_for(state);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -927,7 +927,11 @@ impl ProjectSearchBar {
         let is_active = self.is_option_enabled(option, cx);
         MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
             let theme = theme::current(cx);
-            let style = theme.search.option_button.style_for(state, is_active);
+            let style = theme
+                .search
+                .option_button
+                .in_state(is_active)
+                .style_for(state);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)

crates/settings/Cargo.toml πŸ”—

@@ -21,7 +21,7 @@ util = { path = "../util" }
 
 anyhow.workspace = true
 futures.workspace = true
-json_comments = "0.2"
+serde_json_lenient = {version = "0.1", features = ["preserve_order", "raw_value"]}
 lazy_static.workspace = true
 postage.workspace = true
 rust-embed.workspace = true
@@ -37,6 +37,6 @@ tree-sitter-json = "*"
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 fs = { path = "../fs", features = ["test-support"] }
-
-pretty_assertions = "1.3.0"
+indoc.workspace = true
+pretty_assertions.workspace = true
 unindent.workspace = true

crates/settings/src/keymap_file.rs πŸ”—

@@ -1,30 +1,30 @@
 use crate::{settings_store::parse_json_with_comments, SettingsAssets};
-use anyhow::{Context, Result};
+use anyhow::{anyhow, Context, Result};
 use collections::BTreeMap;
-use gpui::{keymap_matcher::Binding, AppContext};
+use gpui::{keymap_matcher::Binding, AppContext, NoAction};
 use schemars::{
     gen::{SchemaGenerator, SchemaSettings},
     schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
     JsonSchema,
 };
 use serde::Deserialize;
-use serde_json::{value::RawValue, Value};
+use serde_json::Value;
 use util::{asset_str, ResultExt};
 
-#[derive(Deserialize, Default, Clone, JsonSchema)]
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 #[serde(transparent)]
 pub struct KeymapFile(Vec<KeymapBlock>);
 
-#[derive(Deserialize, Default, Clone, JsonSchema)]
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 pub struct KeymapBlock {
     #[serde(default)]
     context: Option<String>,
     bindings: BTreeMap<String, KeymapAction>,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Debug, Deserialize, Default, Clone)]
 #[serde(transparent)]
-pub struct KeymapAction(Box<RawValue>);
+pub struct KeymapAction(Value);
 
 impl JsonSchema for KeymapAction {
     fn schema_name() -> String {
@@ -37,11 +37,12 @@ impl JsonSchema for KeymapAction {
 }
 
 #[derive(Deserialize)]
-struct ActionWithData(Box<str>, Box<RawValue>);
+struct ActionWithData(Box<str>, Value);
 
 impl KeymapFile {
     pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
         let content = asset_str::<SettingsAssets>(asset_path);
+
         Self::parse(content.as_ref())?.add_to_cx(cx)
     }
 
@@ -54,18 +55,28 @@ impl KeymapFile {
             let bindings = bindings
                 .into_iter()
                 .filter_map(|(keystroke, action)| {
-                    let action = action.0.get();
+                    let action = action.0;
 
                     // This is a workaround for a limitation in serde: serde-rs/json#497
                     // We want to deserialize the action data as a `RawValue` so that we can
                     // deserialize the action itself dynamically directly from the JSON
                     // string. But `RawValue` currently does not work inside of an untagged enum.
-                    if action.starts_with('[') {
-                        let ActionWithData(name, data) = serde_json::from_str(action).log_err()?;
-                        cx.deserialize_action(&name, Some(data.get()))
-                    } else {
-                        let name = serde_json::from_str(action).log_err()?;
-                        cx.deserialize_action(name, None)
+                    match action {
+                        Value::Array(items) => {
+                            let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
+                                return Some(Err(anyhow!("Expected array of length 2")));
+                            };
+                            let serde_json::Value::String(name) = name else {
+                                return Some(Err(anyhow!("Expected first item in array to be a string.")))
+                            };
+                            cx.deserialize_action(
+                                &name,
+                                Some(data),
+                            )
+                        },
+                        Value::String(name) => cx.deserialize_action(&name, None),
+                        Value::Null => Ok(no_action()),
+                        _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))),
                     }
                     .with_context(|| {
                         format!(
@@ -105,6 +116,10 @@ impl KeymapFile {
                         instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
                         ..Default::default()
                     }),
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
+                        ..Default::default()
+                    }),
                 ]),
                 ..Default::default()
             })),
@@ -118,3 +133,28 @@ impl KeymapFile {
         serde_json::to_value(root_schema).unwrap()
     }
 }
+
+fn no_action() -> Box<dyn gpui::Action> {
+    Box::new(NoAction {})
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::KeymapFile;
+
+    #[test]
+    fn can_deserialize_keymap_with_trailing_comma() {
+        let json = indoc::indoc! {"[
+              // Standard macOS bindings
+              {
+                \"bindings\": {
+                  \"up\": \"menu::SelectPrev\",
+                },
+              },
+            ]
+                  "
+
+        };
+        KeymapFile::parse(json).unwrap();
+    }
+}

crates/settings/src/settings_store.rs πŸ”—

@@ -834,11 +834,8 @@ fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len:
 }
 
 pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
-    Ok(serde_json::from_reader(
-        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
-    )?)
+    Ok(serde_json_lenient::from_str(content)?)
 }
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/sum_tree/src/cursor.rs πŸ”—

@@ -97,6 +97,42 @@ where
         }
     }
 
+    pub fn next_item(&self) -> Option<&'a T> {
+        self.assert_did_seek();
+        if let Some(entry) = self.stack.last() {
+            if entry.index == entry.tree.0.items().len() - 1 {
+                if let Some(next_leaf) = self.next_leaf() {
+                    Some(next_leaf.0.items().first().unwrap())
+                } else {
+                    None
+                }
+            } else {
+                match *entry.tree.0 {
+                    Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]),
+                    _ => unreachable!(),
+                }
+            }
+        } else if self.at_end {
+            None
+        } else {
+            self.tree.first()
+        }
+    }
+
+    fn next_leaf(&self) -> Option<&'a SumTree<T>> {
+        for entry in self.stack.iter().rev().skip(1) {
+            if entry.index < entry.tree.0.child_trees().len() - 1 {
+                match *entry.tree.0 {
+                    Node::Internal {
+                        ref child_trees, ..
+                    } => return Some(child_trees[entry.index + 1].leftmost_leaf()),
+                    Node::Leaf { .. } => unreachable!(),
+                };
+            }
+        }
+        None
+    }
+
     pub fn prev_item(&self) -> Option<&'a T> {
         self.assert_did_seek();
         if let Some(entry) = self.stack.last() {
@@ -669,7 +705,7 @@ impl<'a, T: Item> SeekAggregate<'a, T> for () {
 impl<'a, T: Item> SeekAggregate<'a, T> for SliceSeekAggregate<T> {
     fn begin_leaf(&mut self) {}
     fn end_leaf(&mut self, cx: &<T::Summary as Summary>::Context) {
-        self.tree.push_tree(
+        self.tree.append(
             SumTree(Arc::new(Node::Leaf {
                 summary: mem::take(&mut self.leaf_summary),
                 items: mem::take(&mut self.leaf_items),
@@ -689,7 +725,7 @@ impl<'a, T: Item> SeekAggregate<'a, T> for SliceSeekAggregate<T> {
         _: &T::Summary,
         cx: &<T::Summary as Summary>::Context,
     ) {
-        self.tree.push_tree(tree.clone(), cx);
+        self.tree.append(tree.clone(), cx);
     }
 }
 

crates/sum_tree/src/sum_tree.rs πŸ”—

@@ -95,31 +95,18 @@ impl<D> fmt::Debug for End<D> {
     }
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)]
 pub enum Bias {
+    #[default]
     Left,
     Right,
 }
 
-impl Default for Bias {
-    fn default() -> Self {
-        Bias::Left
-    }
-}
-
-impl PartialOrd for Bias {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for Bias {
-    fn cmp(&self, other: &Self) -> Ordering {
-        match (self, other) {
-            (Self::Left, Self::Left) => Ordering::Equal,
-            (Self::Left, Self::Right) => Ordering::Less,
-            (Self::Right, Self::Right) => Ordering::Equal,
-            (Self::Right, Self::Left) => Ordering::Greater,
+impl Bias {
+    pub fn invert(self) -> Self {
+        match self {
+            Self::Left => Self::Right,
+            Self::Right => Self::Left,
         }
     }
 }
@@ -268,7 +255,7 @@ impl<T: Item> SumTree<T> {
 
         for item in iter {
             if leaf.is_some() && leaf.as_ref().unwrap().items().len() == 2 * TREE_BASE {
-                self.push_tree(SumTree(Arc::new(leaf.take().unwrap())), cx);
+                self.append(SumTree(Arc::new(leaf.take().unwrap())), cx);
             }
 
             if leaf.is_none() {
@@ -295,13 +282,13 @@ impl<T: Item> SumTree<T> {
         }
 
         if leaf.is_some() {
-            self.push_tree(SumTree(Arc::new(leaf.take().unwrap())), cx);
+            self.append(SumTree(Arc::new(leaf.take().unwrap())), cx);
         }
     }
 
     pub fn push(&mut self, item: T, cx: &<T::Summary as Summary>::Context) {
         let summary = item.summary();
-        self.push_tree(
+        self.append(
             SumTree(Arc::new(Node::Leaf {
                 summary: summary.clone(),
                 items: ArrayVec::from_iter(Some(item)),
@@ -311,11 +298,11 @@ impl<T: Item> SumTree<T> {
         );
     }
 
-    pub fn push_tree(&mut self, other: Self, cx: &<T::Summary as Summary>::Context) {
+    pub fn append(&mut self, other: Self, cx: &<T::Summary as Summary>::Context) {
         if !other.0.is_leaf() || !other.0.items().is_empty() {
             if self.0.height() < other.0.height() {
                 for tree in other.0.child_trees() {
-                    self.push_tree(tree.clone(), cx);
+                    self.append(tree.clone(), cx);
                 }
             } else if let Some(split_tree) = self.push_tree_recursive(other, cx) {
                 *self = Self::from_child_trees(self.clone(), split_tree, cx);
@@ -512,7 +499,7 @@ impl<T: KeyedItem> SumTree<T> {
                 }
             }
             new_tree.push(item, cx);
-            new_tree.push_tree(cursor.suffix(cx), cx);
+            new_tree.append(cursor.suffix(cx), cx);
             new_tree
         };
         replaced
@@ -529,7 +516,7 @@ impl<T: KeyedItem> SumTree<T> {
                     cursor.next(cx);
                 }
             }
-            new_tree.push_tree(cursor.suffix(cx), cx);
+            new_tree.append(cursor.suffix(cx), cx);
             new_tree
         };
         removed
@@ -563,7 +550,7 @@ impl<T: KeyedItem> SumTree<T> {
                 {
                     new_tree.extend(buffered_items.drain(..), cx);
                     let slice = cursor.slice(&new_key, Bias::Left, cx);
-                    new_tree.push_tree(slice, cx);
+                    new_tree.append(slice, cx);
                     old_item = cursor.item();
                 }
 
@@ -583,7 +570,7 @@ impl<T: KeyedItem> SumTree<T> {
             }
 
             new_tree.extend(buffered_items, cx);
-            new_tree.push_tree(cursor.suffix(cx), cx);
+            new_tree.append(cursor.suffix(cx), cx);
             new_tree
         };
 
@@ -719,7 +706,7 @@ mod tests {
         let mut tree2 = SumTree::new();
         tree2.extend(50..100, &());
 
-        tree1.push_tree(tree2, &());
+        tree1.append(tree2, &());
         assert_eq!(
             tree1.items(&()),
             (0..20).chain(50..100).collect::<Vec<u8>>()
@@ -766,7 +753,7 @@ mod tests {
                     let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right, &());
                     new_tree.extend(new_items, &());
                     cursor.seek(&Count(splice_end), Bias::Right, &());
-                    new_tree.push_tree(cursor.slice(&tree_end, Bias::Right, &()), &());
+                    new_tree.append(cursor.slice(&tree_end, Bias::Right, &()), &());
                     new_tree
                 };
 
@@ -838,6 +825,14 @@ mod tests {
                         assert_eq!(cursor.item(), None);
                     }
 
+                    if before_start {
+                        assert_eq!(cursor.next_item(), reference_items.get(0));
+                    } else if pos + 1 < reference_items.len() {
+                        assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]);
+                    } else {
+                        assert_eq!(cursor.next_item(), None);
+                    }
+
                     if i < 5 {
                         cursor.next(&());
                         if pos < reference_items.len() {
@@ -883,14 +878,17 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
         cursor.prev(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
         cursor.next(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
 
         // Single-element tree
@@ -903,22 +901,26 @@ mod tests {
         );
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
 
         cursor.next(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 1);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
 
         let mut cursor = tree.cursor::<IntegersSummary>();
         assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]);
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 1);
 
         cursor.seek(&Count(0), Bias::Right, &());
@@ -930,6 +932,7 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 1);
 
         // Multiple-element tree
@@ -940,67 +943,80 @@ mod tests {
         assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]);
         assert_eq!(cursor.item(), Some(&3));
         assert_eq!(cursor.prev_item(), Some(&2));
+        assert_eq!(cursor.next_item(), Some(&4));
         assert_eq!(cursor.start().sum, 3);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&4));
         assert_eq!(cursor.prev_item(), Some(&3));
+        assert_eq!(cursor.next_item(), Some(&5));
         assert_eq!(cursor.start().sum, 6);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&5));
         assert_eq!(cursor.prev_item(), Some(&4));
+        assert_eq!(cursor.next_item(), Some(&6));
         assert_eq!(cursor.start().sum, 10);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&6));
         assert_eq!(cursor.prev_item(), Some(&5));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 15);
 
         cursor.next(&());
         cursor.next(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&6));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 21);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&6));
         assert_eq!(cursor.prev_item(), Some(&5));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 15);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&5));
         assert_eq!(cursor.prev_item(), Some(&4));
+        assert_eq!(cursor.next_item(), Some(&6));
         assert_eq!(cursor.start().sum, 10);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&4));
         assert_eq!(cursor.prev_item(), Some(&3));
+        assert_eq!(cursor.next_item(), Some(&5));
         assert_eq!(cursor.start().sum, 6);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&3));
         assert_eq!(cursor.prev_item(), Some(&2));
+        assert_eq!(cursor.next_item(), Some(&4));
         assert_eq!(cursor.start().sum, 3);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&2));
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), Some(&3));
         assert_eq!(cursor.start().sum, 1);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), Some(&2));
         assert_eq!(cursor.start().sum, 0);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), Some(&1));
         assert_eq!(cursor.start().sum, 0);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), Some(&2));
         assert_eq!(cursor.start().sum, 0);
 
         let mut cursor = tree.cursor::<IntegersSummary>();
@@ -1012,6 +1028,7 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&6));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 21);
 
         cursor.seek(&Count(3), Bias::Right, &());
@@ -1023,6 +1040,7 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&6));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 21);
 
         // Seeking can bias left or right

crates/sum_tree/src/tree_map.rs πŸ”—

@@ -67,7 +67,7 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
             removed = Some(cursor.item().unwrap().value.clone());
             cursor.next(&());
         }
-        new_tree.push_tree(cursor.suffix(&()), &());
+        new_tree.append(cursor.suffix(&()), &());
         drop(cursor);
         self.0 = new_tree;
         removed
@@ -79,7 +79,7 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
         let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
         let mut new_tree = cursor.slice(&start, Bias::Left, &());
         cursor.seek(&end, Bias::Left, &());
-        new_tree.push_tree(cursor.suffix(&()), &());
+        new_tree.append(cursor.suffix(&()), &());
         drop(cursor);
         self.0 = new_tree;
     }
@@ -117,7 +117,7 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
             new_tree.push(updated, &());
             cursor.next(&());
         }
-        new_tree.push_tree(cursor.suffix(&()), &());
+        new_tree.append(cursor.suffix(&()), &());
         drop(cursor);
         self.0 = new_tree;
         result

crates/terminal_view/src/terminal_element.rs πŸ”—

@@ -395,16 +395,17 @@ impl TerminalElement {
         // Terminal Emulator controlled behavior:
         region = region
             // Start selections
-            .on_down(
-                MouseButton::Left,
-                TerminalElement::generic_button_handler(
-                    connection,
-                    origin,
-                    move |terminal, origin, e, _cx| {
-                        terminal.mouse_down(&e, origin);
-                    },
-                ),
-            )
+            .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
+                cx.focus_parent();
+                v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
+                if let Some(conn_handle) = connection.upgrade(cx) {
+                    conn_handle.update(cx, |terminal, cx| {
+                        terminal.mouse_down(&event, origin);
+
+                        cx.notify();
+                    })
+                }
+            })
             // Update drag selections
             .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
                 if cx.is_self_focused() {

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -25,6 +25,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(TerminalPanel::new_terminal);
 }
 
+#[derive(Debug)]
 pub enum Event {
     Close,
     DockPositionChanged,
@@ -86,6 +87,7 @@ impl TerminalPanel {
                                 }
                             })
                         },
+                        |_, _| {},
                         None,
                     ))
                     .with_child(Pane::render_tab_bar_button(
@@ -99,6 +101,7 @@ impl TerminalPanel {
                         Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
                         cx,
                         move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+                        |_, _| {},
                         None,
                     ))
                     .into_any()

crates/text/src/text.rs πŸ”—

@@ -600,7 +600,7 @@ impl Buffer {
         let mut old_fragments = self.fragments.cursor::<FragmentTextSummary>();
         let mut new_fragments =
             old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right, &None);
-        new_ropes.push_tree(new_fragments.summary().text);
+        new_ropes.append(new_fragments.summary().text);
 
         let mut fragment_start = old_fragments.start().visible;
         for (range, new_text) in edits {
@@ -625,8 +625,8 @@ impl Buffer {
                 }
 
                 let slice = old_fragments.slice(&range.start, Bias::Right, &None);
-                new_ropes.push_tree(slice.summary().text);
-                new_fragments.push_tree(slice, &None);
+                new_ropes.append(slice.summary().text);
+                new_fragments.append(slice, &None);
                 fragment_start = old_fragments.start().visible;
             }
 
@@ -728,8 +728,8 @@ impl Buffer {
         }
 
         let suffix = old_fragments.suffix(&None);
-        new_ropes.push_tree(suffix.summary().text);
-        new_fragments.push_tree(suffix, &None);
+        new_ropes.append(suffix.summary().text);
+        new_fragments.append(suffix, &None);
         let (visible_text, deleted_text) = new_ropes.finish();
         drop(old_fragments);
 
@@ -828,7 +828,7 @@ impl Buffer {
             Bias::Left,
             &cx,
         );
-        new_ropes.push_tree(new_fragments.summary().text);
+        new_ropes.append(new_fragments.summary().text);
 
         let mut fragment_start = old_fragments.start().0.full_offset();
         for (range, new_text) in edits {
@@ -854,8 +854,8 @@ impl Buffer {
 
                 let slice =
                     old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left, &cx);
-                new_ropes.push_tree(slice.summary().text);
-                new_fragments.push_tree(slice, &None);
+                new_ropes.append(slice.summary().text);
+                new_fragments.append(slice, &None);
                 fragment_start = old_fragments.start().0.full_offset();
             }
 
@@ -986,8 +986,8 @@ impl Buffer {
         }
 
         let suffix = old_fragments.suffix(&cx);
-        new_ropes.push_tree(suffix.summary().text);
-        new_fragments.push_tree(suffix, &None);
+        new_ropes.append(suffix.summary().text);
+        new_fragments.append(suffix, &None);
         let (visible_text, deleted_text) = new_ropes.finish();
         drop(old_fragments);
 
@@ -1056,8 +1056,8 @@ impl Buffer {
 
         for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) {
             let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None);
-            new_ropes.push_tree(preceding_fragments.summary().text);
-            new_fragments.push_tree(preceding_fragments, &None);
+            new_ropes.append(preceding_fragments.summary().text);
+            new_fragments.append(preceding_fragments, &None);
 
             if let Some(fragment) = old_fragments.item() {
                 let mut fragment = fragment.clone();
@@ -1087,8 +1087,8 @@ impl Buffer {
         }
 
         let suffix = old_fragments.suffix(&None);
-        new_ropes.push_tree(suffix.summary().text);
-        new_fragments.push_tree(suffix, &None);
+        new_ropes.append(suffix.summary().text);
+        new_fragments.append(suffix, &None);
 
         drop(old_fragments);
         let (visible_text, deleted_text) = new_ropes.finish();
@@ -2070,7 +2070,7 @@ impl<'a> RopeBuilder<'a> {
         }
     }
 
-    fn push_tree(&mut self, len: FragmentTextSummary) {
+    fn append(&mut self, len: FragmentTextSummary) {
         self.push(len.visible, true, true);
         self.push(len.deleted, false, false);
     }
@@ -2489,7 +2489,12 @@ impl ToOffset for Point {
 
 impl ToOffset for usize {
     fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
-        assert!(*self <= snapshot.len(), "offset {self} is out of range");
+        assert!(
+            *self <= snapshot.len(),
+            "offset {} is out of range, max allowed is {}",
+            self,
+            snapshot.len()
+        );
         *self
     }
 }

crates/theme/src/theme.rs πŸ”—

@@ -4,15 +4,16 @@ pub mod ui;
 
 use gpui::{
     color::Color,
-    elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
+    elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
     fonts::{HighlightStyle, TextStyle},
     platform, AppContext, AssetSource, Border, MouseState,
 };
+use schemars::JsonSchema;
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use settings::SettingsStore;
 use std::{collections::HashMap, sync::Arc};
-use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
+use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
 
 pub use theme_registry::*;
 pub use theme_settings::*;
@@ -36,7 +37,7 @@ pub fn init(source: impl AssetSource, cx: &mut AppContext) {
     .detach();
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct Theme {
     #[serde(default)]
     pub meta: ThemeMeta,
@@ -64,10 +65,10 @@ pub struct Theme {
     pub assistant: AssistantStyle,
     pub feedback: FeedbackStyle,
     pub welcome: WelcomeStyle,
-    pub color_scheme: ColorScheme,
+    pub titlebar: Titlebar,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct ThemeMeta {
     #[serde(skip_deserializing)]
     pub id: usize,
@@ -75,11 +76,10 @@ pub struct ThemeMeta {
     pub is_light: bool,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct Workspace {
     pub background: Color,
     pub blank_pane: BlankPaneStyle,
-    pub titlebar: Titlebar,
     pub tab_bar: TabBar,
     pub pane_divider: Border,
     pub leader_border_opacity: f32,
@@ -102,7 +102,7 @@ pub struct Workspace {
     pub drop_target_overlay_color: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct BlankPaneStyle {
     pub logo: SvgStyle,
     pub logo_shadow: SvgStyle,
@@ -112,13 +112,14 @@ pub struct BlankPaneStyle {
     pub keyboard_hint_width: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Titlebar {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub height: f32,
-    pub title: TextStyle,
-    pub highlight_color: Color,
+    pub project_menu_button: Toggleable<Interactive<ContainedText>>,
+    pub project_name_divider: ContainedText,
+    pub git_menu_button: Toggleable<Interactive<ContainedText>>,
     pub item_spacing: f32,
     pub face_pile_spacing: f32,
     pub avatar_ribbon: AvatarRibbon,
@@ -128,16 +129,34 @@ pub struct Titlebar {
     pub leader_avatar: AvatarStyle,
     pub follower_avatar: AvatarStyle,
     pub inactive_avatar_grayscale: bool,
-    pub sign_in_prompt: Interactive<ContainedText>,
+    pub sign_in_button: Toggleable<Interactive<ContainedText>>,
     pub outdated_warning: ContainedText,
-    pub share_button: Interactive<ContainedText>,
-    pub call_control: Interactive<IconButton>,
-    pub toggle_contacts_button: Interactive<IconButton>,
-    pub user_menu_button: Interactive<IconButton>,
+    pub share_button: Toggleable<Interactive<ContainedText>>,
+    pub muted: Color,
+    pub speaking: Color,
+    pub screen_share_button: Toggleable<Interactive<IconButton>>,
+    pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
+    pub toggle_microphone_button: Toggleable<Interactive<IconButton>>,
+    pub toggle_speakers_button: Toggleable<Interactive<IconButton>>,
+    pub leave_call_button: Interactive<IconButton>,
     pub toggle_contacts_badge: ContainerStyle,
+    pub user_menu: UserMenu,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct UserMenu {
+    pub user_menu_button_online: UserMenuButton,
+    pub user_menu_button_offline: UserMenuButton,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct UserMenuButton {
+    pub user_menu: Toggleable<Interactive<Icon>>,
+    pub avatar: AvatarStyle,
+    pub icon: Icon,
 }
 
-#[derive(Copy, Clone, Deserialize, Default)]
+#[derive(Copy, Clone, Deserialize, Default, JsonSchema)]
 pub struct AvatarStyle {
     #[serde(flatten)]
     pub image: ImageStyle,
@@ -145,14 +164,14 @@ pub struct AvatarStyle {
     pub outer_corner_radius: f32,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct Copilot {
     pub out_link_icon: Interactive<IconStyle>,
     pub modal: ModalStyle,
     pub auth: CopilotAuth,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct CopilotAuth {
     pub content_width: f32,
     pub prompting: CopilotAuthPrompting,
@@ -162,14 +181,14 @@ pub struct CopilotAuth {
     pub header: IconStyle,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct CopilotAuthPrompting {
     pub subheading: ContainedText,
     pub hint: ContainedText,
     pub device_code: DeviceCode,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct DeviceCode {
     pub text: TextStyle,
     pub cta: ButtonStyle,
@@ -179,19 +198,19 @@ pub struct DeviceCode {
     pub right_container: Interactive<ContainerStyle>,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct CopilotAuthNotAuthorized {
     pub subheading: ContainedText,
     pub warning: ContainedText,
 }
 
-#[derive(Deserialize, Default, Clone)]
+#[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct CopilotAuthAuthorized {
     pub subheading: ContainedText,
     pub hint: ContainedText,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ContactsPopover {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -199,17 +218,17 @@ pub struct ContactsPopover {
     pub width: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ContactList {
     pub user_query_editor: FieldEditor,
     pub user_query_editor_height: f32,
     pub add_contact_button: IconButton,
-    pub header_row: Interactive<ContainedText>,
+    pub header_row: Toggleable<Interactive<ContainedText>>,
     pub leave_call: Interactive<ContainedText>,
-    pub contact_row: Interactive<ContainerStyle>,
+    pub contact_row: Toggleable<Interactive<ContainerStyle>>,
     pub row_height: f32,
-    pub project_row: Interactive<ProjectRow>,
-    pub tree_branch: Interactive<TreeBranch>,
+    pub project_row: Toggleable<Interactive<ProjectRow>>,
+    pub tree_branch: Toggleable<Interactive<TreeBranch>>,
     pub contact_avatar: ImageStyle,
     pub contact_status_free: ContainerStyle,
     pub contact_status_busy: ContainerStyle,
@@ -221,7 +240,7 @@ pub struct ContactList {
     pub calling_indicator: ContainedText,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ProjectRow {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -229,13 +248,13 @@ pub struct ProjectRow {
     pub name: ContainedText,
 }
 
-#[derive(Deserialize, Default, Clone, Copy)]
+#[derive(Deserialize, Default, Clone, Copy, JsonSchema)]
 pub struct TreeBranch {
     pub width: f32,
     pub color: Color,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ContactFinder {
     pub picker: Picker,
     pub row_height: f32,
@@ -245,17 +264,17 @@ pub struct ContactFinder {
     pub disabled_contact_button: IconButton,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct DropdownMenu {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub header: Interactive<DropdownMenuItem>,
     pub section_header: ContainedText,
-    pub item: Interactive<DropdownMenuItem>,
+    pub item: Toggleable<Interactive<DropdownMenuItem>>,
     pub row_height: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct DropdownMenuItem {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -266,11 +285,11 @@ pub struct DropdownMenuItem {
     pub secondary_text_spacing: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct TabBar {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub pane_button: Interactive<IconButton>,
+    pub pane_button: Toggleable<Interactive<IconButton>>,
     pub pane_button_container: ContainerStyle,
     pub active_pane: TabStyles,
     pub inactive_pane: TabStyles,
@@ -294,13 +313,13 @@ impl TabBar {
     }
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct TabStyles {
     pub active_tab: Tab,
     pub inactive_tab: Tab,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct AvatarRibbon {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -308,7 +327,7 @@ pub struct AvatarRibbon {
     pub height: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct OfflineIcon {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -316,7 +335,7 @@ pub struct OfflineIcon {
     pub color: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Tab {
     pub height: f32,
     #[serde(flatten)]
@@ -333,7 +352,7 @@ pub struct Tab {
     pub icon_conflict: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Toolbar {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -342,14 +361,14 @@ pub struct Toolbar {
     pub nav_button: Interactive<IconButton>,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Notifications {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub width: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Search {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -359,14 +378,14 @@ pub struct Search {
     pub include_exclude_editor: FindEditor,
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
-    pub option_button: Interactive<ContainedText>,
+    pub option_button: Toggleable<Interactive<ContainedText>>,
     pub match_background: Color,
     pub match_index: ContainedText,
     pub results_status: TextStyle,
     pub dismiss_button: Interactive<IconButton>,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct FindEditor {
     #[serde(flatten)]
     pub input: FieldEditor,
@@ -374,7 +393,7 @@ pub struct FindEditor {
     pub max_width: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct StatusBar {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -390,15 +409,15 @@ pub struct StatusBar {
     pub diagnostic_message: Interactive<ContainedText>,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct StatusBarPanelButtons {
     pub group_left: ContainerStyle,
     pub group_bottom: ContainerStyle,
     pub group_right: ContainerStyle,
-    pub button: Interactive<PanelButton>,
+    pub button: Toggleable<Interactive<PanelButton>>,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct StatusBarDiagnosticSummary {
     pub container_ok: ContainerStyle,
     pub container_warning: ContainerStyle,
@@ -413,7 +432,7 @@ pub struct StatusBarDiagnosticSummary {
     pub summary_spacing: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct StatusBarLspStatus {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -424,14 +443,14 @@ pub struct StatusBarLspStatus {
     pub message: TextStyle,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct Dock {
     pub left: ContainerStyle,
     pub bottom: ContainerStyle,
     pub right: ContainerStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct PanelButton {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -440,20 +459,20 @@ pub struct PanelButton {
     pub label: ContainedText,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ProjectPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub entry: Interactive<ProjectPanelEntry>,
+    pub entry: Toggleable<Interactive<ProjectPanelEntry>>,
     pub dragged_entry: ProjectPanelEntry,
-    pub ignored_entry: Interactive<ProjectPanelEntry>,
-    pub cut_entry: Interactive<ProjectPanelEntry>,
+    pub ignored_entry: Toggleable<Interactive<ProjectPanelEntry>>,
+    pub cut_entry: Toggleable<Interactive<ProjectPanelEntry>>,
     pub filename_editor: FieldEditor,
     pub indent_width: f32,
     pub open_project_button: Interactive<ContainedText>,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct ProjectPanelEntry {
     pub height: f32,
     #[serde(flatten)]
@@ -465,28 +484,28 @@ pub struct ProjectPanelEntry {
     pub status: EntryStatus,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct EntryStatus {
     pub git: GitProjectStatus,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct GitProjectStatus {
     pub modified: Color,
     pub inserted: Color,
     pub conflict: Color,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct ContextMenu {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub item: Interactive<ContextMenuItem>,
+    pub item: Toggleable<Interactive<ContextMenuItem>>,
     pub keystroke_margin: f32,
     pub separator: ContainerStyle,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct ContextMenuItem {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -496,13 +515,13 @@ pub struct ContextMenuItem {
     pub icon_spacing: f32,
 }
 
-#[derive(Debug, Deserialize, Default)]
+#[derive(Debug, Deserialize, Default, JsonSchema)]
 pub struct CommandPalette {
-    pub key: Interactive<ContainedLabel>,
+    pub key: Toggleable<ContainedLabel>,
     pub keystroke_spacing: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct InviteLink {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -511,7 +530,7 @@ pub struct InviteLink {
     pub icon: Icon,
 }
 
-#[derive(Deserialize, Clone, Copy, Default)]
+#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
 pub struct Icon {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -519,7 +538,7 @@ pub struct Icon {
     pub width: f32,
 }
 
-#[derive(Deserialize, Clone, Copy, Default)]
+#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
 pub struct IconButton {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -528,7 +547,7 @@ pub struct IconButton {
     pub button_width: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ChatMessage {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -537,7 +556,7 @@ pub struct ChatMessage {
     pub timestamp: ContainedText,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ChannelSelect {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -549,7 +568,7 @@ pub struct ChannelSelect {
     pub menu: ContainerStyle,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ChannelName {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -557,7 +576,7 @@ pub struct ChannelName {
     pub name: TextStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Picker {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -565,10 +584,12 @@ pub struct Picker {
     pub input_editor: FieldEditor,
     pub empty_input_editor: FieldEditor,
     pub no_matches: ContainedLabel,
-    pub item: Interactive<ContainedLabel>,
+    pub item: Toggleable<Interactive<ContainedLabel>>,
+    pub header: ContainedLabel,
+    pub footer: ContainedLabel,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct ContainedText {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -576,7 +597,7 @@ pub struct ContainedText {
     pub text: TextStyle,
 }
 
-#[derive(Clone, Debug, Deserialize, Default)]
+#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 pub struct ContainedLabel {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -584,7 +605,7 @@ pub struct ContainedLabel {
     pub label: LabelStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct ProjectDiagnostics {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -594,7 +615,7 @@ pub struct ProjectDiagnostics {
     pub tab_summary_spacing: f32,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ContactNotification {
     pub header_avatar: ImageStyle,
     pub header_message: ContainedText,
@@ -604,21 +625,21 @@ pub struct ContactNotification {
     pub dismiss_button: Interactive<IconButton>,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct UpdateNotification {
     pub message: ContainedText,
     pub action_message: Interactive<ContainedText>,
     pub dismiss_button: Interactive<IconButton>,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct MessageNotification {
     pub message: ContainedText,
     pub action_message: Interactive<ContainedText>,
     pub dismiss_button: Interactive<IconButton>,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct ProjectSharedNotification {
     pub window_height: f32,
     pub window_width: f32,
@@ -635,7 +656,7 @@ pub struct ProjectSharedNotification {
     pub dismiss_button: ContainedText,
 }
 
-#[derive(Deserialize, Default)]
+#[derive(Deserialize, Default, JsonSchema)]
 pub struct IncomingCallNotification {
     pub window_height: f32,
     pub window_width: f32,
@@ -652,7 +673,7 @@ pub struct IncomingCallNotification {
     pub decline_button: ContainedText,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Editor {
     pub text_color: Color,
     #[serde(default)]
@@ -670,6 +691,7 @@ pub struct Editor {
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
     pub syntax: Arc<SyntaxTheme>,
+    pub hint: HighlightStyle,
     pub suggestion: HighlightStyle,
     pub diagnostic_path_header: DiagnosticPathHeader,
     pub diagnostic_header: DiagnosticHeader,
@@ -693,7 +715,7 @@ pub struct Editor {
     pub whitespace: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Scrollbar {
     pub track: ContainerStyle,
     pub thumb: ContainerStyle,
@@ -703,14 +725,14 @@ pub struct Scrollbar {
     pub selections: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct GitDiffColors {
     pub inserted: Color,
     pub modified: Color,
     pub deleted: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct DiagnosticPathHeader {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -719,7 +741,7 @@ pub struct DiagnosticPathHeader {
     pub text_scale_factor: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct DiagnosticHeader {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -730,7 +752,7 @@ pub struct DiagnosticHeader {
     pub icon_width_factor: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct DiagnosticStyle {
     pub message: LabelStyle,
     #[serde(default)]
@@ -738,7 +760,7 @@ pub struct DiagnosticStyle {
     pub text_scale_factor: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct AutocompleteStyle {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -748,13 +770,13 @@ pub struct AutocompleteStyle {
     pub match_highlight: HighlightStyle,
 }
 
-#[derive(Clone, Copy, Default, Deserialize)]
+#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
 pub struct SelectionStyle {
     pub cursor: Color,
     pub selection: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct FieldEditor {
     #[serde(flatten)]
     pub container: ContainerStyle,
@@ -764,21 +786,21 @@ pub struct FieldEditor {
     pub selection: SelectionStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct InteractiveColor {
     pub color: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct CodeActions {
     #[serde(default)]
-    pub indicator: Interactive<InteractiveColor>,
+    pub indicator: Toggleable<Interactive<InteractiveColor>>,
     pub vertical_scale: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Folds {
-    pub indicator: Interactive<InteractiveColor>,
+    pub indicator: Toggleable<Interactive<InteractiveColor>>,
     pub ellipses: FoldEllipses,
     pub fold_background: Color,
     pub icon_margin_scale: f32,
@@ -786,14 +808,14 @@ pub struct Folds {
     pub foldable_icon: String,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct FoldEllipses {
     pub text_color: Color,
     pub background: Interactive<InteractiveColor>,
     pub corner_radius_factor: f32,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct DiffStyle {
     pub inserted: Color,
     pub modified: Color,
@@ -803,41 +825,49 @@ pub struct DiffStyle {
     pub corner_radius: f32,
 }
 
-#[derive(Debug, Default, Clone, Copy)]
+#[derive(Debug, Default, Clone, Copy, JsonSchema)]
 pub struct Interactive<T> {
     pub default: T,
-    pub hover: Option<T>,
-    pub hover_and_active: Option<T>,
+    pub hovered: Option<T>,
     pub clicked: Option<T>,
-    pub click_and_active: Option<T>,
-    pub active: Option<T>,
     pub disabled: Option<T>,
 }
 
-impl<T> Interactive<T> {
-    pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
+#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
+pub struct Toggleable<T> {
+    active: T,
+    inactive: T,
+}
+
+impl<T> Toggleable<T> {
+    pub fn new(active: T, inactive: T) -> Self {
+        Self { active, inactive }
+    }
+    pub fn in_state(&self, active: bool) -> &T {
         if active {
-            if state.hovered() {
-                self.hover_and_active
-                    .as_ref()
-                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
-            } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some()
-            {
-                self.click_and_active
-                    .as_ref()
-                    .unwrap_or(self.active.as_ref().unwrap_or(&self.default))
-            } else {
-                self.active.as_ref().unwrap_or(&self.default)
-            }
-        } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
+            &self.active
+        } else {
+            &self.inactive
+        }
+    }
+    pub fn active_state(&self) -> &T {
+        self.in_state(true)
+    }
+    pub fn inactive_state(&self) -> &T {
+        self.in_state(false)
+    }
+}
+
+impl<T> Interactive<T> {
+    pub fn style_for(&self, state: &mut MouseState) -> &T {
+        if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
             self.clicked.as_ref().unwrap()
         } else if state.hovered() {
-            self.hover.as_ref().unwrap_or(&self.default)
+            self.hovered.as_ref().unwrap_or(&self.default)
         } else {
             &self.default
         }
     }
-
     pub fn disabled_style(&self) -> &T {
         self.disabled.as_ref().unwrap_or(&self.default)
     }
@@ -850,13 +880,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
     {
         #[derive(Deserialize)]
         struct Helper {
-            #[serde(flatten)]
             default: Value,
-            hover: Option<Value>,
-            hover_and_active: Option<Value>,
+            hovered: Option<Value>,
             clicked: Option<Value>,
-            click_and_active: Option<Value>,
-            active: Option<Value>,
             disabled: Option<Value>,
         }
 
@@ -881,21 +907,15 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
             }
         };
 
-        let hover = deserialize_state(json.hover)?;
-        let hover_and_active = deserialize_state(json.hover_and_active)?;
+        let hovered = deserialize_state(json.hovered)?;
         let clicked = deserialize_state(json.clicked)?;
-        let click_and_active = deserialize_state(json.click_and_active)?;
-        let active = deserialize_state(json.active)?;
         let disabled = deserialize_state(json.disabled)?;
         let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
 
         Ok(Interactive {
             default,
-            hover,
-            hover_and_active,
+            hovered,
             clicked,
-            click_and_active,
-            active,
             disabled,
         })
     }
@@ -912,7 +932,7 @@ impl Editor {
     }
 }
 
-#[derive(Default)]
+#[derive(Default, JsonSchema)]
 pub struct SyntaxTheme {
     pub highlights: Vec<(String, HighlightStyle)>,
 }
@@ -946,7 +966,7 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
     }
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct HoverPopover {
     pub container: ContainerStyle,
     pub info_container: ContainerStyle,
@@ -958,7 +978,7 @@ pub struct HoverPopover {
     pub highlight: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct TerminalStyle {
     pub black: Color,
     pub red: Color,
@@ -992,24 +1012,39 @@ pub struct TerminalStyle {
     pub dim_foreground: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct AssistantStyle {
     pub container: ContainerStyle,
-    pub header: ContainerStyle,
+    pub hamburger_button: Interactive<IconStyle>,
+    pub split_button: Interactive<IconStyle>,
+    pub assist_button: Interactive<IconStyle>,
+    pub quote_button: Interactive<IconStyle>,
+    pub zoom_in_button: Interactive<IconStyle>,
+    pub zoom_out_button: Interactive<IconStyle>,
+    pub plus_button: Interactive<IconStyle>,
+    pub title: ContainedText,
+    pub message_header: ContainerStyle,
     pub sent_at: ContainedText,
     pub user_sender: Interactive<ContainedText>,
     pub assistant_sender: Interactive<ContainedText>,
     pub system_sender: Interactive<ContainedText>,
-    pub model_info_container: ContainerStyle,
     pub model: Interactive<ContainedText>,
     pub remaining_tokens: ContainedText,
     pub no_remaining_tokens: ContainedText,
     pub error_icon: Icon,
     pub api_key_editor: FieldEditor,
     pub api_key_prompt: ContainedText,
+    pub saved_conversation: SavedConversation,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct SavedConversation {
+    pub container: Interactive<ContainerStyle>,
+    pub saved_at: ContainedText,
+    pub title: ContainedText,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct FeedbackStyle {
     pub submit_button: Interactive<ContainedText>,
     pub button_margin: f32,
@@ -1018,7 +1053,7 @@ pub struct FeedbackStyle {
     pub link_text_hover: ContainedText,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct WelcomeStyle {
     pub page_width: f32,
     pub logo: SvgStyle,
@@ -1032,7 +1067,7 @@ pub struct WelcomeStyle {
     pub checkbox_group: ContainerStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct ColorScheme {
     pub name: String,
     pub is_light: bool,
@@ -1047,13 +1082,13 @@ pub struct ColorScheme {
     pub players: Vec<Player>,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Player {
     pub cursor: Color,
     pub selection: Color,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct RampSet {
     pub neutral: Vec<Color>,
     pub red: Vec<Color>,
@@ -1066,7 +1101,7 @@ pub struct RampSet {
     pub magenta: Vec<Color>,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Layer {
     pub base: StyleSet,
     pub variant: StyleSet,
@@ -1077,7 +1112,7 @@ pub struct Layer {
     pub negative: StyleSet,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct StyleSet {
     pub default: Style,
     pub active: Style,
@@ -1087,7 +1122,7 @@ pub struct StyleSet {
     pub inverted: Style,
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct Style {
     pub background: Color,
     pub border: Color,

crates/theme/src/theme_settings.rs πŸ”—

@@ -14,12 +14,13 @@ use util::ResultExt as _;
 
 const MIN_FONT_SIZE: f32 = 6.0;
 
-#[derive(Clone)]
+#[derive(Clone, JsonSchema)]
 pub struct ThemeSettings {
     pub buffer_font_family_name: String,
     pub buffer_font_features: fonts::Features,
     pub buffer_font_family: FamilyId,
     pub(crate) buffer_font_size: f32,
+    #[serde(skip)]
     pub theme: Arc<Theme>,
 }
 

crates/theme/src/ui.rs πŸ”—

@@ -1,23 +1,23 @@
 use std::borrow::Cow;
 
 use gpui::{
-    color::Color,
     elements::{
-        ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
-        MouseEventHandler, ParentElement, Stack, Svg,
+        ConstrainedBox, Container, ContainerStyle, Dimensions, Empty, Flex, KeystrokeLabel, Label,
+        MouseEventHandler, ParentElement, Stack, Svg, SvgStyle,
     },
     fonts::TextStyle,
-    geometry::vector::{vec2f, Vector2F},
+    geometry::vector::Vector2F,
     platform,
     platform::MouseButton,
     scene::MouseClick,
     Action, Element, EventContext, MouseState, View, ViewContext,
 };
+use schemars::JsonSchema;
 use serde::Deserialize;
 
 use crate::{ContainedText, Interactive};
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct CheckboxStyle {
     pub icon: SvgStyle,
     pub label: ContainedText,
@@ -93,25 +93,6 @@ where
     .with_cursor_style(platform::CursorStyle::PointingHand)
 }
 
-#[derive(Clone, Deserialize, Default)]
-pub struct SvgStyle {
-    pub color: Color,
-    pub asset: String,
-    pub dimensions: Dimensions,
-}
-
-#[derive(Clone, Deserialize, Default)]
-pub struct Dimensions {
-    pub width: f32,
-    pub height: f32,
-}
-
-impl Dimensions {
-    pub fn to_vec(&self) -> Vector2F {
-        vec2f(self.width, self.height)
-    }
-}
-
 pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
     Svg::new(style.asset.clone())
         .with_color(style.color)
@@ -120,10 +101,10 @@ pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
         .with_height(style.dimensions.height)
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct IconStyle {
-    icon: SvgStyle,
-    container: ContainerStyle,
+    pub icon: SvgStyle,
+    pub container: ContainerStyle,
 }
 
 pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
@@ -170,7 +151,7 @@ where
     F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
 {
     MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
-        let style = style.style_for(state, false);
+        let style = style.style_for(state);
         Label::new(label, style.text.to_owned())
             .aligned()
             .contained()
@@ -182,7 +163,7 @@ where
     .with_cursor_style(platform::CursorStyle::PointingHand)
 }
 
-#[derive(Clone, Deserialize, Default)]
+#[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct ModalStyle {
     close_icon: Interactive<IconStyle>,
     container: ContainerStyle,
@@ -220,13 +201,13 @@ where
                     title,
                     style
                         .title_text
-                        .style_for(&mut MouseState::default(), false)
+                        .style_for(&mut MouseState::default())
                         .clone(),
                 ))
                 .with_child(
                     // FIXME: Get a better tag type
                     MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
-                        let style = style.close_icon.style_for(state, false);
+                        let style = style.close_icon.style_for(state);
                         icon(style)
                     })
                     .on_click(platform::MouseButton::Left, move |_, _, cx| {

crates/theme_selector/src/theme_selector.rs πŸ”—

@@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
         let theme = theme::current(cx);
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
         let theme_match = &self.matches[ix];
         Label::new(theme_match.string.clone(), style.label.clone())

crates/theme_testbench/Cargo.toml πŸ”—

@@ -1,19 +0,0 @@
-[package]
-name = "theme_testbench"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/theme_testbench.rs"
-doctest = false
-
-
-[dependencies]
-gpui = { path = "../gpui" }
-theme = { path = "../theme" }
-settings = { path = "../settings" }
-workspace = { path = "../workspace" }
-project = { path = "../project" }
-
-smallvec.workspace = true

crates/theme_testbench/src/theme_testbench.rs πŸ”—

@@ -1,300 +0,0 @@
-use gpui::{
-    actions,
-    color::Color,
-    elements::{
-        AnyElement, Canvas, Container, ContainerStyle, Flex, Label, Margin, MouseEventHandler,
-        Padding, ParentElement,
-    },
-    fonts::TextStyle,
-    AppContext, Border, Element, Entity, ModelHandle, Quad, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle,
-};
-use project::Project;
-use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings};
-use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
-
-actions!(theme, [DeployThemeTestbench]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ThemeTestbench::deploy);
-
-    register_deserializable_item::<ThemeTestbench>(cx)
-}
-
-pub struct ThemeTestbench {}
-
-impl ThemeTestbench {
-    pub fn deploy(
-        workspace: &mut Workspace,
-        _: &DeployThemeTestbench,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        let view = cx.add_view(|_| ThemeTestbench {});
-        workspace.add_item(Box::new(view), cx);
-    }
-
-    fn render_ramps(color_scheme: &ColorScheme) -> Flex<Self> {
-        fn display_ramp(ramp: &Vec<Color>) -> AnyElement<ThemeTestbench> {
-            Flex::row()
-                .with_children(ramp.iter().cloned().map(|color| {
-                    Canvas::new(move |scene, bounds, _, _, _| {
-                        scene.push_quad(Quad {
-                            bounds,
-                            background: Some(color),
-                            ..Default::default()
-                        });
-                    })
-                    .flex(1.0, false)
-                }))
-                .flex(1.0, false)
-                .into_any()
-        }
-
-        Flex::column()
-            .with_child(display_ramp(&color_scheme.ramps.neutral))
-            .with_child(display_ramp(&color_scheme.ramps.red))
-            .with_child(display_ramp(&color_scheme.ramps.orange))
-            .with_child(display_ramp(&color_scheme.ramps.yellow))
-            .with_child(display_ramp(&color_scheme.ramps.green))
-            .with_child(display_ramp(&color_scheme.ramps.cyan))
-            .with_child(display_ramp(&color_scheme.ramps.blue))
-            .with_child(display_ramp(&color_scheme.ramps.violet))
-            .with_child(display_ramp(&color_scheme.ramps.magenta))
-    }
-
-    fn render_layer(
-        layer_index: usize,
-        layer: &Layer,
-        cx: &mut ViewContext<Self>,
-    ) -> Container<Self> {
-        Flex::column()
-            .with_child(
-                Self::render_button_set(0, layer_index, "base", &layer.base, cx).flex(1., false),
-            )
-            .with_child(
-                Self::render_button_set(1, layer_index, "variant", &layer.variant, cx)
-                    .flex(1., false),
-            )
-            .with_child(
-                Self::render_button_set(2, layer_index, "on", &layer.on, cx).flex(1., false),
-            )
-            .with_child(
-                Self::render_button_set(3, layer_index, "accent", &layer.accent, cx)
-                    .flex(1., false),
-            )
-            .with_child(
-                Self::render_button_set(4, layer_index, "positive", &layer.positive, cx)
-                    .flex(1., false),
-            )
-            .with_child(
-                Self::render_button_set(5, layer_index, "warning", &layer.warning, cx)
-                    .flex(1., false),
-            )
-            .with_child(
-                Self::render_button_set(6, layer_index, "negative", &layer.negative, cx)
-                    .flex(1., false),
-            )
-            .contained()
-            .with_style(ContainerStyle {
-                margin: Margin {
-                    top: 10.,
-                    bottom: 10.,
-                    left: 10.,
-                    right: 10.,
-                },
-                background_color: Some(layer.base.default.background),
-                ..Default::default()
-            })
-    }
-
-    fn render_button_set(
-        set_index: usize,
-        layer_index: usize,
-        set_name: &'static str,
-        style_set: &StyleSet,
-        cx: &mut ViewContext<Self>,
-    ) -> Flex<Self> {
-        Flex::row()
-            .with_child(Self::render_button(
-                set_index * 6,
-                layer_index,
-                set_name,
-                &style_set,
-                None,
-                cx,
-            ))
-            .with_child(Self::render_button(
-                set_index * 6 + 1,
-                layer_index,
-                "hovered",
-                &style_set,
-                Some(|style_set| &style_set.hovered),
-                cx,
-            ))
-            .with_child(Self::render_button(
-                set_index * 6 + 2,
-                layer_index,
-                "pressed",
-                &style_set,
-                Some(|style_set| &style_set.pressed),
-                cx,
-            ))
-            .with_child(Self::render_button(
-                set_index * 6 + 3,
-                layer_index,
-                "active",
-                &style_set,
-                Some(|style_set| &style_set.active),
-                cx,
-            ))
-            .with_child(Self::render_button(
-                set_index * 6 + 4,
-                layer_index,
-                "disabled",
-                &style_set,
-                Some(|style_set| &style_set.disabled),
-                cx,
-            ))
-            .with_child(Self::render_button(
-                set_index * 6 + 5,
-                layer_index,
-                "inverted",
-                &style_set,
-                Some(|style_set| &style_set.inverted),
-                cx,
-            ))
-    }
-
-    fn render_button(
-        button_index: usize,
-        layer_index: usize,
-        text: &'static str,
-        style_set: &StyleSet,
-        style_override: Option<fn(&StyleSet) -> &Style>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum TestBenchButton {}
-        MouseEventHandler::<TestBenchButton, _>::new(layer_index + button_index, cx, |state, cx| {
-            let style = if let Some(style_override) = style_override {
-                style_override(&style_set)
-            } else if state.clicked().is_some() {
-                &style_set.pressed
-            } else if state.hovered() {
-                &style_set.hovered
-            } else {
-                &style_set.default
-            };
-
-            Self::render_label(text.to_string(), style, cx)
-                .contained()
-                .with_style(ContainerStyle {
-                    margin: Margin {
-                        top: 4.,
-                        bottom: 4.,
-                        left: 4.,
-                        right: 4.,
-                    },
-                    padding: Padding {
-                        top: 4.,
-                        bottom: 4.,
-                        left: 4.,
-                        right: 4.,
-                    },
-                    background_color: Some(style.background),
-                    border: Border {
-                        width: 1.,
-                        color: style.border,
-                        overlay: false,
-                        top: true,
-                        bottom: true,
-                        left: true,
-                        right: true,
-                    },
-                    corner_radius: 2.,
-                    ..Default::default()
-                })
-        })
-        .flex(1., true)
-        .into_any()
-    }
-
-    fn render_label(text: String, style: &Style, cx: &mut ViewContext<Self>) -> Label {
-        let settings = settings::get::<ThemeSettings>(cx);
-        let font_cache = cx.font_cache();
-        let family_id = settings.buffer_font_family;
-        let font_size = settings.buffer_font_size(cx);
-        let font_id = font_cache
-            .select_font(family_id, &Default::default())
-            .unwrap();
-
-        let text_style = TextStyle {
-            color: style.foreground,
-            font_family_id: family_id,
-            font_family_name: font_cache.family_name(family_id).unwrap(),
-            font_id,
-            font_size,
-            font_properties: Default::default(),
-            underline: Default::default(),
-        };
-
-        Label::new(text, text_style)
-    }
-}
-
-impl Entity for ThemeTestbench {
-    type Event = ();
-}
-
-impl View for ThemeTestbench {
-    fn ui_name() -> &'static str {
-        "ThemeTestbench"
-    }
-
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
-        let color_scheme = &theme::current(cx).clone().color_scheme;
-
-        Flex::row()
-            .with_child(
-                Self::render_ramps(color_scheme)
-                    .contained()
-                    .with_margin_right(10.)
-                    .flex(0.1, false),
-            )
-            .with_child(
-                Flex::column()
-                    .with_child(Self::render_layer(100, &color_scheme.lowest, cx).flex(1., true))
-                    .with_child(Self::render_layer(200, &color_scheme.middle, cx).flex(1., true))
-                    .with_child(Self::render_layer(300, &color_scheme.highest, cx).flex(1., true))
-                    .flex(1., false),
-            )
-            .into_any()
-    }
-}
-
-impl Item for ThemeTestbench {
-    fn tab_content<T: View>(
-        &self,
-        _: Option<usize>,
-        style: &theme::Tab,
-        _: &AppContext,
-    ) -> AnyElement<T> {
-        Label::new("Theme Testbench", style.label.clone())
-            .aligned()
-            .contained()
-            .into_any()
-    }
-
-    fn serialized_item_kind() -> Option<&'static str> {
-        Some("ThemeTestBench")
-    }
-
-    fn deserialize(
-        _project: ModelHandle<Project>,
-        _workspace: WeakViewHandle<Workspace>,
-        _workspace_id: workspace::WorkspaceId,
-        _item_id: workspace::ItemId,
-        cx: &mut ViewContext<Pane>,
-    ) -> Task<gpui::anyhow::Result<ViewHandle<Self>>> {
-        Task::ready(Ok(cx.add_view(|_| Self {})))
-    }
-}

crates/util/src/paths.rs πŸ”—

@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
 lazy_static::lazy_static! {
     pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
     pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
+    pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
     pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
     pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
     pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");

crates/util/src/util.rs πŸ”—

@@ -118,14 +118,15 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se
     }
 }
 
-pub trait ResultExt {
+pub trait ResultExt<E> {
     type Ok;
 
     fn log_err(self) -> Option<Self::Ok>;
     fn warn_on_err(self) -> Option<Self::Ok>;
+    fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
 }
 
-impl<T, E> ResultExt for Result<T, E>
+impl<T, E> ResultExt<E> for Result<T, E>
 where
     E: std::fmt::Debug,
 {
@@ -152,6 +153,15 @@ where
             }
         }
     }
+
+    /// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err
+    fn inspect_error(self, func: impl FnOnce(&E)) -> Self {
+        if let Err(err) = &self {
+            func(err);
+        }
+
+        self
+    }
 }
 
 pub trait TryFutureExt {

crates/vim/src/motion.rs πŸ”—

@@ -209,8 +209,9 @@ impl Motion {
         map: &DisplaySnapshot,
         point: DisplayPoint,
         goal: SelectionGoal,
-        times: usize,
+        maybe_times: Option<usize>,
     ) -> Option<(DisplayPoint, SelectionGoal)> {
+        let times = maybe_times.unwrap_or(1);
         use Motion::*;
         let infallible = self.infallible();
         let (new_point, goal) = match self {
@@ -236,7 +237,10 @@ impl Motion {
             EndOfLine => (end_of_line(map, point), SelectionGoal::None),
             CurrentLine => (end_of_line(map, point), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
-            EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
+            EndOfDocument => (
+                end_of_document(map, point, maybe_times),
+                SelectionGoal::None,
+            ),
             Matching => (matching(map, point), SelectionGoal::None),
             FindForward { before, text } => (
                 find_forward(map, point, *before, text.clone(), times),
@@ -257,7 +261,7 @@ impl Motion {
         &self,
         map: &DisplaySnapshot,
         selection: &mut Selection<DisplayPoint>,
-        times: usize,
+        times: Option<usize>,
         expand_to_surrounding_newline: bool,
     ) -> bool {
         if let Some((new_head, goal)) =
@@ -473,14 +477,19 @@ fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) ->
     map.clip_point(new_point, Bias::Left)
 }
 
-fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
-    let mut new_point = if line == 1 {
-        map.max_point()
+fn end_of_document(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    line: Option<usize>,
+) -> DisplayPoint {
+    let new_row = if let Some(line) = line {
+        (line - 1) as u32
     } else {
-        Point::new((line - 1) as u32, 0).to_display_point(map)
+        map.max_buffer_row()
     };
-    *new_point.column_mut() = point.column();
-    map.clip_point(new_point, Bias::Left)
+
+    let new_point = Point::new(new_row, point.column());
+    map.clip_point(new_point.to_display_point(map), Bias::Left)
 }
 
 fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {

crates/vim/src/normal.rs πŸ”—

@@ -1,8 +1,11 @@
+mod case;
 mod change;
 mod delete;
+mod scroll;
+mod substitute;
 mod yank;
 
-use std::{borrow::Cow, cmp::Ordering, sync::Arc};
+use std::{borrow::Cow, sync::Arc};
 
 use crate::{
     motion::Motion,
@@ -12,25 +15,22 @@ use crate::{
 };
 use collections::{HashMap, HashSet};
 use editor::{
-    display_map::ToDisplayPoint,
-    scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
-    Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
+    display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
+    DisplayPoint,
 };
-use gpui::{actions, impl_actions, AppContext, ViewContext, WindowContext};
+use gpui::{actions, AppContext, ViewContext, WindowContext};
 use language::{AutoindentMode, Point, SelectionGoal};
 use log::error;
-use serde::Deserialize;
 use workspace::Workspace;
 
 use self::{
+    case::change_case,
     change::{change_motion, change_object},
     delete::{delete_motion, delete_object},
+    substitute::substitute,
     yank::{yank_motion, yank_object},
 };
 
-#[derive(Clone, PartialEq, Deserialize)]
-struct Scroll(ScrollAmount);
-
 actions!(
     vim,
     [
@@ -45,17 +45,24 @@ actions!(
         DeleteToEndOfLine,
         Paste,
         Yank,
+        Substitute,
+        ChangeCase,
     ]
 );
 
-impl_actions!(vim, [Scroll]);
-
 pub fn init(cx: &mut AppContext) {
     cx.add_action(insert_after);
     cx.add_action(insert_first_non_whitespace);
     cx.add_action(insert_end_of_line);
     cx.add_action(insert_line_above);
     cx.add_action(insert_line_below);
+    cx.add_action(change_case);
+    cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
+        Vim::update(cx, |vim, cx| {
+            let times = vim.pop_number_operator(cx);
+            substitute(vim, times, cx);
+        })
+    });
     cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
         Vim::update(cx, |vim, cx| {
             let times = vim.pop_number_operator(cx);
@@ -81,19 +88,14 @@ pub fn init(cx: &mut AppContext) {
         })
     });
     cx.add_action(paste);
-    cx.add_action(|_: &mut Workspace, Scroll(amount): &Scroll, cx| {
-        Vim::update(cx, |vim, cx| {
-            vim.update_active_editor(cx, |editor, cx| {
-                scroll(editor, amount, cx);
-            })
-        })
-    });
+
+    scroll::init(cx);
 }
 
 pub fn normal_motion(
     motion: Motion,
     operator: Option<Operator>,
-    times: usize,
+    times: Option<usize>,
     cx: &mut WindowContext,
 ) {
     Vim::update(cx, |vim, cx| {
@@ -129,7 +131,7 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
     })
 }
 
-fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
+fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_cursors_with(|map, cursor, goal| {
@@ -147,7 +149,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::Right.move_point(map, cursor, goal, 1)
+                    Motion::Right.move_point(map, cursor, goal, None)
                 });
             });
         });
@@ -164,7 +166,7 @@ fn insert_first_non_whitespace(
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
+                    Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
                 });
             });
         });
@@ -177,7 +179,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.maybe_move_cursors_with(|map, cursor, goal| {
-                    Motion::EndOfLine.move_point(map, cursor, goal, 1)
+                    Motion::EndOfLine.move_point(map, cursor, goal, None)
                 });
             });
         });
@@ -237,7 +239,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
                 });
                 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.maybe_move_cursors_with(|map, cursor, goal| {
-                        Motion::EndOfLine.move_point(map, cursor, goal, 1)
+                        Motion::EndOfLine.move_point(map, cursor, goal, None)
                     });
                 });
                 editor.edit_with_autoindent(edits, cx);
@@ -384,46 +386,6 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
     });
 }
 
-fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
-    let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
-    editor.scroll_screen(amount, cx);
-    if should_move_cursor {
-        let selection_ordering = editor.newest_selection_on_screen(cx);
-        if selection_ordering.is_eq() {
-            return;
-        }
-
-        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
-            visible_rows as u32
-        } else {
-            return;
-        };
-
-        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
-        let top_anchor = editor.scroll_manager.anchor().anchor;
-
-        editor.change_selections(None, cx, |s| {
-            s.replace_cursors_with(|snapshot| {
-                let mut new_point = top_anchor.to_display_point(&snapshot);
-
-                match selection_ordering {
-                    Ordering::Less => {
-                        *new_point.row_mut() += scroll_margin_rows;
-                        new_point = snapshot.clip_point(new_point, Bias::Right);
-                    }
-                    Ordering::Greater => {
-                        *new_point.row_mut() += visible_rows - scroll_margin_rows as u32;
-                        new_point = snapshot.clip_point(new_point, Bias::Left);
-                    }
-                    Ordering::Equal => unreachable!(),
-                }
-
-                vec![new_point]
-            })
-        });
-    }
-}
-
 pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {

crates/vim/src/normal/case.rs πŸ”—

@@ -0,0 +1,64 @@
+use gpui::ViewContext;
+use language::Point;
+use workspace::Workspace;
+
+use crate::{motion::Motion, normal::ChangeCase, Vim};
+
+pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
+    Vim::update(cx, |vim, cx| {
+        let count = vim.pop_number_operator(cx);
+        vim.update_active_editor(cx, |editor, cx| {
+            editor.set_clip_at_line_ends(false, cx);
+            editor.transact(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| {
+                    s.move_with(|map, selection| {
+                        if selection.start == selection.end {
+                            Motion::Right.expand_selection(map, selection, count, true);
+                        }
+                    })
+                });
+                let selections = editor.selections.all::<Point>(cx);
+                for selection in selections.into_iter().rev() {
+                    let snapshot = editor.buffer().read(cx).snapshot(cx);
+                    editor.buffer().update(cx, |buffer, cx| {
+                        let range = selection.start..selection.end;
+                        let text = snapshot
+                            .text_for_range(selection.start..selection.end)
+                            .flat_map(|s| s.chars())
+                            .flat_map(|c| {
+                                if c.is_lowercase() {
+                                    c.to_uppercase().collect::<Vec<char>>()
+                                } else {
+                                    c.to_lowercase().collect::<Vec<char>>()
+                                }
+                            })
+                            .collect::<String>();
+
+                        buffer.edit([(range, text)], None, cx)
+                    })
+                }
+            });
+            editor.set_clip_at_line_ends(true, cx);
+        });
+    })
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{state::Mode, test::VimTestContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_change_case(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state(indoc! {"Λ‡abC\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["~"]);
+        cx.assert_editor_state("AˇbC\n");
+        cx.simulate_keystrokes(["2", "~"]);
+        cx.assert_editor_state("ABcˇ\n");
+
+        cx.set_state(indoc! {"aπŸ˜€CΒ«dΓ‰1*fΛ‡Β»\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["~"]);
+        cx.assert_editor_state("aπŸ˜€CDΓ©1*FΛ‡\n");
+    }
+}

crates/vim/src/normal/change.rs πŸ”—

@@ -6,7 +6,7 @@ use editor::{
 use gpui::WindowContext;
 use language::Selection;
 
-pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
+pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     // Some motions ignore failure when switching to normal mode
     let mut motion_succeeded = matches!(
         motion,
@@ -78,10 +78,10 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
 fn expand_changed_word_selection(
     map: &DisplaySnapshot,
     selection: &mut Selection<DisplayPoint>,
-    times: usize,
+    times: Option<usize>,
     ignore_punctuation: bool,
 ) -> bool {
-    if times == 1 {
+    if times.is_none() || times.unwrap() == 1 {
         let in_word = map
             .chars_at(selection.head())
             .next()
@@ -97,7 +97,8 @@ fn expand_changed_word_selection(
             });
             true
         } else {
-            Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
+            Motion::NextWordStart { ignore_punctuation }
+                .expand_selection(map, selection, None, false)
         }
     } else {
         Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)

crates/vim/src/normal/delete.rs πŸ”—

@@ -3,7 +3,7 @@ use collections::{HashMap, HashSet};
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
 use gpui::WindowContext;
 
-pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
+pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);

crates/vim/src/normal/scroll.rs πŸ”—

@@ -0,0 +1,120 @@
+use std::cmp::Ordering;
+
+use crate::Vim;
+use editor::{display_map::ToDisplayPoint, scroll::scroll_amount::ScrollAmount, Editor};
+use gpui::{actions, AppContext, ViewContext};
+use language::Bias;
+use workspace::Workspace;
+
+actions!(
+    vim,
+    [LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown,]
+);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(|_: &mut Workspace, _: &LineDown, cx| {
+        scroll(cx, |c| ScrollAmount::Line(c.unwrap_or(1.)))
+    });
+    cx.add_action(|_: &mut Workspace, _: &LineUp, cx| {
+        scroll(cx, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
+    });
+    cx.add_action(|_: &mut Workspace, _: &PageDown, cx| {
+        scroll(cx, |c| ScrollAmount::Page(c.unwrap_or(1.)))
+    });
+    cx.add_action(|_: &mut Workspace, _: &PageUp, cx| {
+        scroll(cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
+    });
+    cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| {
+        scroll(cx, |c| {
+            if let Some(c) = c {
+                ScrollAmount::Line(c)
+            } else {
+                ScrollAmount::Page(0.5)
+            }
+        })
+    });
+    cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| {
+        scroll(cx, |c| {
+            if let Some(c) = c {
+                ScrollAmount::Line(-c)
+            } else {
+                ScrollAmount::Page(-0.5)
+            }
+        })
+    });
+}
+
+fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
+    Vim::update(cx, |vim, cx| {
+        let amount = by(vim.pop_number_operator(cx).map(|c| c as f32));
+        vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx));
+    })
+}
+
+fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
+    let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
+    editor.scroll_screen(amount, cx);
+    if should_move_cursor {
+        let selection_ordering = editor.newest_selection_on_screen(cx);
+        if selection_ordering.is_eq() {
+            return;
+        }
+
+        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+            visible_rows as u32
+        } else {
+            return;
+        };
+
+        let top_anchor = editor.scroll_manager.anchor().anchor;
+
+        editor.change_selections(None, cx, |s| {
+            s.replace_cursors_with(|snapshot| {
+                let mut new_point = top_anchor.to_display_point(&snapshot);
+
+                match selection_ordering {
+                    Ordering::Less => {
+                        new_point = snapshot.clip_point(new_point, Bias::Right);
+                    }
+                    Ordering::Greater => {
+                        *new_point.row_mut() += visible_rows - 1;
+                        new_point = snapshot.clip_point(new_point, Bias::Left);
+                    }
+                    Ordering::Equal => unreachable!(),
+                }
+
+                vec![new_point]
+            })
+        });
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{state::Mode, test::VimTestContext};
+    use gpui::geometry::vector::vec2f;
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_scroll(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state(indoc! {"Λ‡a\nb\nc\nd\ne\n"}, Mode::Normal);
+
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
+        });
+        cx.simulate_keystrokes(["ctrl-e"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.))
+        });
+        cx.simulate_keystrokes(["2", "ctrl-e"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.))
+        });
+        cx.simulate_keystrokes(["ctrl-y"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
+        });
+    }
+}

crates/vim/src/normal/substitute.rs πŸ”—

@@ -0,0 +1,73 @@
+use gpui::WindowContext;
+use language::Point;
+
+use crate::{motion::Motion, Mode, Vim};
+
+pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.set_clip_at_line_ends(false, cx);
+        editor.transact(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| {
+                s.move_with(|map, selection| {
+                    if selection.start == selection.end {
+                        Motion::Right.expand_selection(map, selection, count, true);
+                    }
+                })
+            });
+            let selections = editor.selections.all::<Point>(cx);
+            for selection in selections.into_iter().rev() {
+                editor.buffer().update(cx, |buffer, cx| {
+                    buffer.edit([(selection.start..selection.end, "")], None, cx)
+                })
+            }
+        });
+        editor.set_clip_at_line_ends(true, cx);
+    });
+    vim.switch_mode(Mode::Insert, true, cx)
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{state::Mode, test::VimTestContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_substitute(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // supports a single cursor
+        cx.set_state(indoc! {"Λ‡abc\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["s", "x"]);
+        cx.assert_editor_state("xˇbc\n");
+
+        // supports a selection
+        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
+        cx.assert_editor_state("a«bcˇ»\n");
+        cx.simulate_keystrokes(["s", "x"]);
+        cx.assert_editor_state("axˇ\n");
+
+        // supports counts
+        cx.set_state(indoc! {"Λ‡abc\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["2", "s", "x"]);
+        cx.assert_editor_state("xˇc\n");
+
+        // supports multiple cursors
+        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["2", "s", "x"]);
+        cx.assert_editor_state("axˇdexˇg\n");
+
+        // does not read beyond end of line
+        cx.set_state(indoc! {"Λ‡abc\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["5", "s", "x"]);
+        cx.assert_editor_state("xˇ\n");
+
+        // it handles multibyte characters
+        cx.set_state(indoc! {"Λ‡cΓ fΓ©\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["4", "s"]);
+        cx.assert_editor_state("Λ‡\n");
+
+        // should transactionally undo selection changes
+        cx.simulate_keystrokes(["escape", "u"]);
+        cx.assert_editor_state("Λ‡cΓ fΓ©\n");
+    }
+}

crates/vim/src/normal/yank.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}
 use collections::HashMap;
 use gpui::WindowContext;
 
-pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
+pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.update_active_editor(cx, |editor, cx| {
         editor.transact(cx, |editor, cx| {
             editor.set_clip_at_line_ends(false, cx);

crates/vim/src/test.rs πŸ”—

@@ -98,3 +98,44 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
         assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
     })
 }
+
+#[gpui::test]
+async fn test_count_down(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
+    cx.simulate_keystrokes(["2", "down"]);
+    cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
+    cx.simulate_keystrokes(["9", "down"]);
+    cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
+}
+
+#[gpui::test]
+async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    // goes to end by default
+    cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal);
+    cx.simulate_keystrokes(["shift-g"]);
+    cx.assert_editor_state("aa\nbb\ncˇc");
+
+    // can go to line 1 (https://github.com/zed-industries/community/issues/710)
+    cx.simulate_keystrokes(["1", "shift-g"]);
+    cx.assert_editor_state("aˇa\nbb\ncc");
+}
+
+#[gpui::test]
+async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    // works in normal mode
+    cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal);
+    cx.simulate_keystrokes([">", ">"]);
+    cx.assert_editor_state("aa\n    bˇb\ncc");
+    cx.simulate_keystrokes(["<", "<"]);
+    cx.assert_editor_state("aa\nbˇb\ncc");
+
+    // works in visuial mode
+    cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
+    cx.assert_editor_state("aa\n    b«b\n    cˇ»c");
+}

crates/vim/src/vim.rs πŸ”—

@@ -238,13 +238,12 @@ impl Vim {
         popped_operator
     }
 
-    fn pop_number_operator(&mut self, cx: &mut WindowContext) -> usize {
-        let mut times = 1;
+    fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
         if let Some(Operator::Number(number)) = self.active_operator() {
-            times = number;
             self.pop_operator(cx);
+            return Some(number);
         }
-        times
+        None
     }
 
     fn clear_operator(&mut self, cx: &mut WindowContext) {

crates/vim/src/visual.rs πŸ”—

@@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(paste);
 }
 
-pub fn visual_motion(motion: Motion, times: usize, cx: &mut WindowContext) {
+pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         vim.update_active_editor(cx, |editor, cx| {
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {

crates/welcome/src/base_keymap_picker.rs πŸ”—

@@ -141,7 +141,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
     ) -> gpui::AnyElement<Picker<Self>> {
         let theme = &theme::current(cx);
         let keymap_match = &self.matches[ix];
-        let style = theme.picker.item.style_for(mouse_state, selected);
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
         Label::new(keymap_match.string.clone(), style.label.clone())
             .with_highlights(keymap_match.positions.clone())

crates/workspace/src/dock.rs πŸ”—

@@ -249,7 +249,7 @@ impl Dock {
         }
     }
 
-    pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+    pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
         let subscriptions = [
             cx.observe(&panel, |_, _, cx| cx.notify()),
             cx.subscribe(&panel, |this, panel, event, cx| {
@@ -498,7 +498,9 @@ impl View for PanelButtons {
                     Stack::new()
                         .with_child(
                             MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
-                                let style = button_style.style_for(state, is_active);
+                                let style = button_style.in_state(is_active);
+
+                                let style = style.style_for(state);
                                 Flex::row()
                                     .with_child(
                                         Svg::new(view.icon_path(cx))
@@ -598,11 +600,12 @@ impl StatusItemView for PanelButtons {
     }
 }
 
-#[cfg(test)]
-pub(crate) mod test {
+#[cfg(any(test, feature = "test-support"))]
+pub mod test {
     use super::*;
     use gpui::{ViewContext, WindowContext};
 
+    #[derive(Debug)]
     pub enum TestPanelEvent {
         PositionChanged,
         Activated,

crates/workspace/src/item.rs πŸ”—

@@ -710,8 +710,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
     }
 }
 
-#[cfg(test)]
-pub(crate) mod test {
+#[cfg(any(test, feature = "test-support"))]
+pub mod test {
     use super::{Item, ItemEvent};
     use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
     use gpui::{

crates/workspace/src/notifications.rs πŸ”—

@@ -291,7 +291,7 @@ pub mod simple_message_notification {
                         )
                         .with_child(
                             MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
-                                let style = theme.dismiss_button.style_for(state, false);
+                                let style = theme.dismiss_button.style_for(state);
                                 Svg::new("icons/x_mark_8.svg")
                                     .with_color(style.color)
                                     .constrained()
@@ -323,7 +323,7 @@ pub mod simple_message_notification {
                                 0,
                                 cx,
                                 |state, _| {
-                                    let style = theme.action_message.style_for(state, false);
+                                    let style = theme.action_message.style_for(state);
 
                                     Flex::row()
                                         .with_child(

crates/workspace/src/pane.rs πŸ”—

@@ -1,9 +1,10 @@
 mod dragged_item_receiver;
 
 use super::{ItemHandle, SplitDirection};
+pub use crate::toolbar::Toolbar;
 use crate::{
-    item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
-    NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
+    item::WeakItemHandle, notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile,
+    NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
@@ -250,7 +251,7 @@ impl Pane {
                 pane: handle.clone(),
                 next_timestamp,
             }))),
-            toolbar: cx.add_view(|_| Toolbar::new(handle)),
+            toolbar: cx.add_view(|_| Toolbar::new(Some(handle))),
             tab_bar_context_menu: TabBarContextMenu {
                 kind: TabBarContextMenuKind::New,
                 handle: context_menu,
@@ -272,6 +273,11 @@ impl Pane {
                         Some(("New...".into(), None)),
                         cx,
                         |pane, cx| pane.deploy_new_menu(cx),
+                        |pane, cx| {
+                            pane.tab_bar_context_menu
+                                .handle
+                                .update(cx, |menu, _| menu.delay_cancel())
+                        },
                         pane.tab_bar_context_menu
                             .handle_if_kind(TabBarContextMenuKind::New),
                     ))
@@ -282,22 +288,36 @@ impl Pane {
                         Some(("Split Pane".into(), None)),
                         cx,
                         |pane, cx| pane.deploy_split_menu(cx),
+                        |pane, cx| {
+                            pane.tab_bar_context_menu
+                                .handle
+                                .update(cx, |menu, _| menu.delay_cancel())
+                        },
                         pane.tab_bar_context_menu
                             .handle_if_kind(TabBarContextMenuKind::Split),
                     ))
-                    .with_child(Pane::render_tab_bar_button(
-                        2,
+                    .with_child({
+                        let icon_path;
+                        let tooltip_label;
                         if pane.is_zoomed() {
-                            "icons/minimize_8.svg"
+                            icon_path = "icons/minimize_8.svg";
+                            tooltip_label = "Zoom In".into();
                         } else {
-                            "icons/maximize_8.svg"
-                        },
-                        pane.is_zoomed(),
-                        Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
-                        cx,
-                        move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
-                        None,
-                    ))
+                            icon_path = "icons/maximize_8.svg";
+                            tooltip_label = "Zoom In".into();
+                        }
+
+                        Pane::render_tab_bar_button(
+                            2,
+                            icon_path,
+                            pane.is_zoomed(),
+                            Some((tooltip_label, Some(Box::new(ToggleZoom)))),
+                            cx,
+                            move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
+                            move |_, _| {},
+                            None,
+                        )
+                    })
                     .into_any()
             }),
         }
@@ -979,7 +999,7 @@ impl Pane {
 
     fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
@@ -997,7 +1017,7 @@ impl Pane {
 
     fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
-            menu.show(
+            menu.toggle(
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
@@ -1112,7 +1132,7 @@ impl Pane {
             .get(self.active_item_index)
             .map(|item| item.as_ref());
         self.toolbar.update(cx, |toolbar, cx| {
-            toolbar.set_active_pane_item(active_item, cx);
+            toolbar.set_active_item(active_item, cx);
         });
     }
 
@@ -1407,20 +1427,24 @@ impl Pane {
             .into_any()
     }
 
-    pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
+    pub fn render_tab_bar_button<
+        F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+        F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
+    >(
         index: usize,
         icon: &'static str,
-        active: bool,
+        is_active: bool,
         tooltip: Option<(String, Option<Box<dyn Action>>)>,
         cx: &mut ViewContext<Pane>,
-        on_click: F,
+        on_click: F1,
+        on_down: F2,
         context_menu: Option<ViewHandle<ContextMenu>>,
     ) -> AnyElement<Pane> {
         enum TabBarButton {}
 
         let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
             let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
-            let style = theme.pane_button.style_for(mouse_state, active);
+            let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
             Svg::new(icon)
                 .with_color(style.color)
                 .constrained()
@@ -1431,6 +1455,7 @@ impl Pane {
                 .with_height(style.button_width)
         })
         .with_cursor_style(CursorStyle::PointingHand)
+        .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
         .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
         .into_any();
         if let Some((tooltip, action)) = tooltip {
@@ -1602,7 +1627,7 @@ impl View for Pane {
         }
 
         self.toolbar.update(cx, |toolbar, cx| {
-            toolbar.pane_focus_update(true, cx);
+            toolbar.focus_changed(true, cx);
         });
 
         if let Some(active_item) = self.active_item() {
@@ -1631,7 +1656,7 @@ impl View for Pane {
     fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
         self.has_focus = false;
         self.toolbar.update(cx, |toolbar, cx| {
-            toolbar.pane_focus_update(false, cx);
+            toolbar.focus_changed(false, cx);
         });
         cx.notify();
     }

crates/workspace/src/persistence.rs πŸ”—

@@ -162,6 +162,12 @@ define_connection! {
         ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
         ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
         ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
+    ),
+    // Add panel zoom persistence
+    sql!(
+        ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
+        ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
+        ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
     )];
 }
 
@@ -196,10 +202,13 @@ impl WorkspaceDb {
                     display,
                     left_dock_visible,
                     left_dock_active_panel,
+                    left_dock_zoom,
                     right_dock_visible,
                     right_dock_active_panel,
+                    right_dock_zoom,
                     bottom_dock_visible,
-                    bottom_dock_active_panel
+                    bottom_dock_active_panel,
+                    bottom_dock_zoom
                 FROM workspaces
                 WHERE workspace_location = ?
             })
@@ -244,22 +253,28 @@ impl WorkspaceDb {
                         workspace_location,
                         left_dock_visible,
                         left_dock_active_panel,
+                        left_dock_zoom,
                         right_dock_visible,
                         right_dock_active_panel,
+                        right_dock_zoom,
                         bottom_dock_visible,
                         bottom_dock_active_panel,
+                        bottom_dock_zoom,
                         timestamp
                     )
-                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, CURRENT_TIMESTAMP)
+                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
                     ON CONFLICT DO
                     UPDATE SET
                         workspace_location = ?2,
                         left_dock_visible = ?3,
                         left_dock_active_panel = ?4,
-                        right_dock_visible = ?5,
-                        right_dock_active_panel = ?6,
-                        bottom_dock_visible = ?7,
-                        bottom_dock_active_panel = ?8,
+                        left_dock_zoom = ?5,
+                        right_dock_visible = ?6,
+                        right_dock_active_panel = ?7,
+                        right_dock_zoom = ?8,
+                        bottom_dock_visible = ?9,
+                        bottom_dock_active_panel = ?10,
+                        bottom_dock_zoom = ?11,
                         timestamp = CURRENT_TIMESTAMP
                 ))?((workspace.id, &workspace.location, workspace.docks))
                 .context("Updating workspace")?;

crates/workspace/src/persistence/model.rs πŸ”—

@@ -100,16 +100,19 @@ impl Bind for DockStructure {
 pub struct DockData {
     pub(crate) visible: bool,
     pub(crate) active_panel: Option<String>,
+    pub(crate) zoom: bool,
 }
 
 impl Column for DockData {
     fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
         let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
         let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
+        let (zoom, next_index) = Option::<bool>::column(statement, next_index)?;
         Ok((
             DockData {
                 visible: visible.unwrap_or(false),
                 active_panel,
+                zoom: zoom.unwrap_or(false),
             },
             next_index,
         ))
@@ -119,7 +122,8 @@ impl Column for DockData {
 impl Bind for DockData {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         let next_index = statement.bind(&self.visible, start_index)?;
-        statement.bind(&self.active_panel, next_index)
+        let next_index = statement.bind(&self.active_panel, next_index)?;
+        statement.bind(&self.zoom, next_index)
     }
 }
 

crates/workspace/src/toolbar.rs πŸ”—

@@ -38,7 +38,7 @@ trait ToolbarItemViewHandle {
         active_pane_item: Option<&dyn ItemHandle>,
         cx: &mut WindowContext,
     ) -> ToolbarItemLocation;
-    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
+    fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext);
     fn row_count(&self, cx: &WindowContext) -> usize;
 }
 
@@ -51,10 +51,10 @@ pub enum ToolbarItemLocation {
 }
 
 pub struct Toolbar {
-    active_pane_item: Option<Box<dyn ItemHandle>>,
+    active_item: Option<Box<dyn ItemHandle>>,
     hidden: bool,
     can_navigate: bool,
-    pane: WeakViewHandle<Pane>,
+    pane: Option<WeakViewHandle<Pane>>,
     items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
 }
 
@@ -121,7 +121,7 @@ impl View for Toolbar {
         let pane = self.pane.clone();
         let mut enable_go_backward = false;
         let mut enable_go_forward = false;
-        if let Some(pane) = pane.upgrade(cx) {
+        if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) {
             let pane = pane.read(cx);
             enable_go_backward = pane.can_navigate_backward();
             enable_go_forward = pane.can_navigate_forward();
@@ -143,19 +143,17 @@ impl View for Toolbar {
                 enable_go_backward,
                 spacing,
                 {
-                    let pane = pane.clone();
                     move |toolbar, cx| {
-                        if let Some(workspace) = toolbar
-                            .pane
-                            .upgrade(cx)
-                            .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
+                        if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
                         {
-                            let pane = pane.clone();
-                            cx.window_context().defer(move |cx| {
-                                workspace.update(cx, |workspace, cx| {
-                                    workspace.go_back(pane.clone(), cx).detach_and_log_err(cx);
-                                });
-                            })
+                            if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
+                                let pane = pane.downgrade();
+                                cx.window_context().defer(move |cx| {
+                                    workspace.update(cx, |workspace, cx| {
+                                        workspace.go_back(pane, cx).detach_and_log_err(cx);
+                                    });
+                                })
+                            }
                         }
                     }
                 },
@@ -171,21 +169,17 @@ impl View for Toolbar {
                 enable_go_forward,
                 spacing,
                 {
-                    let pane = pane.clone();
                     move |toolbar, cx| {
-                        if let Some(workspace) = toolbar
-                            .pane
-                            .upgrade(cx)
-                            .and_then(|pane| pane.read(cx).workspace().upgrade(cx))
+                        if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
                         {
-                            let pane = pane.clone();
-                            cx.window_context().defer(move |cx| {
-                                workspace.update(cx, |workspace, cx| {
-                                    workspace
-                                        .go_forward(pane.clone(), cx)
-                                        .detach_and_log_err(cx);
-                                });
-                            });
+                            if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
+                                let pane = pane.downgrade();
+                                cx.window_context().defer(move |cx| {
+                                    workspace.update(cx, |workspace, cx| {
+                                        workspace.go_forward(pane, cx).detach_and_log_err(cx);
+                                    });
+                                })
+                            }
                         }
                     }
                 },
@@ -231,7 +225,7 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
 ) -> AnyElement<Toolbar> {
     MouseEventHandler::<A, _>::new(0, cx, |state, _| {
         let style = if enabled {
-            style.style_for(state, false)
+            style.style_for(state)
         } else {
             style.disabled_style()
         };
@@ -269,9 +263,9 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
 }
 
 impl Toolbar {
-    pub fn new(pane: WeakViewHandle<Pane>) -> Self {
+    pub fn new(pane: Option<WeakViewHandle<Pane>>) -> Self {
         Self {
-            active_pane_item: None,
+            active_item: None,
             pane,
             items: Default::default(),
             hidden: false,
@@ -288,7 +282,7 @@ impl Toolbar {
     where
         T: 'static + ToolbarItemView,
     {
-        let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
+        let location = item.set_active_pane_item(self.active_item.as_deref(), cx);
         cx.subscribe(&item, |this, item, event, cx| {
             if let Some((_, current_location)) =
                 this.items.iter_mut().find(|(i, _)| i.id() == item.id())
@@ -307,20 +301,16 @@ impl Toolbar {
         cx.notify();
     }
 
-    pub fn set_active_pane_item(
-        &mut self,
-        pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.active_pane_item = pane_item.map(|item| item.boxed_clone());
+    pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        self.active_item = item.map(|item| item.boxed_clone());
         self.hidden = self
-            .active_pane_item
+            .active_item
             .as_ref()
             .map(|item| !item.show_toolbar(cx))
             .unwrap_or(false);
 
         for (toolbar_item, current_location) in self.items.iter_mut() {
-            let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
+            let new_location = toolbar_item.set_active_pane_item(item, cx);
             if new_location != *current_location {
                 *current_location = new_location;
                 cx.notify();
@@ -328,9 +318,9 @@ impl Toolbar {
         }
     }
 
-    pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
+    pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext<Self>) {
         for (toolbar_item, _) in self.items.iter_mut() {
-            toolbar_item.pane_focus_update(pane_focused, cx);
+            toolbar_item.focus_changed(focused, cx);
         }
     }
 
@@ -364,7 +354,7 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
         })
     }
 
-    fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
+    fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) {
         self.update(cx, |this, cx| {
             this.pane_focus_update(pane_focused, cx);
             cx.notify();

crates/workspace/src/workspace.rs πŸ”—

@@ -97,9 +97,25 @@ lazy_static! {
 }
 
 pub trait Modal: View {
+    fn has_focus(&self) -> bool;
     fn dismiss_on_event(event: &Self::Event) -> bool;
 }
 
+trait ModalHandle {
+    fn as_any(&self) -> &AnyViewHandle;
+    fn has_focus(&self, cx: &WindowContext) -> bool;
+}
+
+impl<T: Modal> ModalHandle for ViewHandle<T> {
+    fn as_any(&self) -> &AnyViewHandle {
+        self
+    }
+
+    fn has_focus(&self, cx: &WindowContext) -> bool {
+        self.read(cx).has_focus()
+    }
+}
+
 #[derive(Clone, PartialEq)]
 pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
@@ -140,9 +156,11 @@ pub struct OpenPaths {
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePane(pub usize);
 
+#[derive(Deserialize)]
 pub struct Toast {
     id: usize,
     msg: Cow<'static, str>,
+    #[serde(skip)]
     on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
 }
 
@@ -183,9 +201,9 @@ impl Clone for Toast {
     }
 }
 
-pub type WorkspaceId = i64;
+impl_actions!(workspace, [ActivatePane, Toast]);
 
-impl_actions!(workspace, [ActivatePane]);
+pub type WorkspaceId = i64;
 
 pub fn init_settings(cx: &mut AppContext) {
     settings::register::<WorkspaceSettings>(cx);
@@ -464,7 +482,7 @@ pub enum Event {
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
     remote_entity_subscription: Option<client::Subscription>,
-    modal: Option<AnyViewHandle>,
+    modal: Option<ActiveModal>,
     zoomed: Option<AnyWeakViewHandle>,
     zoomed_position: Option<DockPosition>,
     center: PaneGroup,
@@ -493,6 +511,11 @@ pub struct Workspace {
     pane_history_timestamp: Arc<AtomicUsize>,
 }
 
+struct ActiveModal {
+    view: Box<dyn ModalHandle>,
+    previously_focused_view_id: Option<usize>,
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub struct ViewId {
     pub creator: PeerId,
@@ -553,6 +576,10 @@ impl Workspace {
                     }
                 }
 
+                project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
+                    cx.add_view(|_| MessageNotification::new(message.clone()))
+                }),
+
                 _ => {}
             }
             cx.notify()
@@ -855,7 +882,10 @@ impl Workspace {
         &self.right_dock
     }
 
-    pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+    pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
+    where
+        T::Event: std::fmt::Debug,
+    {
         let dock = match panel.position(cx) {
             DockPosition::Left => &self.left_dock,
             DockPosition::Bottom => &self.bottom_dock,
@@ -898,10 +928,11 @@ impl Workspace {
                     });
                 } else if T::should_zoom_in_on_event(event) {
                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
-                    if panel.has_focus(cx) {
-                        this.zoomed = Some(panel.downgrade().into_any());
-                        this.zoomed_position = Some(panel.read(cx).position(cx));
+                    if !panel.has_focus(cx) {
+                        cx.focus(&panel);
                     }
+                    this.zoomed = Some(panel.downgrade().into_any());
+                    this.zoomed_position = Some(panel.read(cx).position(cx));
                 } else if T::should_zoom_out_on_event(event) {
                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
                     if this.zoomed_position == Some(prev_position) {
@@ -919,6 +950,7 @@ impl Workspace {
                         this.zoomed = None;
                         this.zoomed_position = None;
                     }
+                    this.update_active_view_for_followers(cx);
                     cx.notify();
                 }
             }
@@ -1471,8 +1503,10 @@ impl Workspace {
         cx.notify();
         // Whatever modal was visible is getting clobbered. If its the same type as V, then return
         // it. Otherwise, create a new modal and set it as active.
-        let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
-        if let Some(already_open_modal) = already_open_modal {
+        if let Some(already_open_modal) = self
+            .dismiss_modal(cx)
+            .and_then(|modal| modal.downcast::<V>())
+        {
             cx.focus_self();
             Some(already_open_modal)
         } else {
@@ -1483,8 +1517,12 @@ impl Workspace {
                 }
             })
             .detach();
+            let previously_focused_view_id = cx.focused_view_id();
             cx.focus(&modal);
-            self.modal = Some(modal.into_any());
+            self.modal = Some(ActiveModal {
+                view: Box::new(modal),
+                previously_focused_view_id,
+            });
             None
         }
     }
@@ -1492,13 +1530,20 @@ impl Workspace {
     pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
         self.modal
             .as_ref()
-            .and_then(|modal| modal.clone().downcast::<V>())
+            .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
     }
 
-    pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
-        if self.modal.take().is_some() {
-            cx.focus(&self.active_pane);
+    pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
+        if let Some(modal) = self.modal.take() {
+            if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
+                if modal.view.has_focus(cx) {
+                    cx.window_context().focus(Some(previously_focused_view_id));
+                }
+            }
             cx.notify();
+            Some(modal.view.as_any().clone())
+        } else {
+            None
         }
     }
 
@@ -1598,9 +1643,7 @@ impl Workspace {
                         focus_center = true;
                     }
                 } else {
-                    if active_panel.is_zoomed(cx) {
-                        cx.focus(active_panel.as_any());
-                    }
+                    cx.focus(active_panel.as_any());
                     reveal_dock = true;
                 }
             }
@@ -1697,6 +1740,11 @@ impl Workspace {
         cx.notify();
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
+        self.zoomed.and_then(|view| view.upgrade(cx))
+    }
+
     fn dismiss_zoomed_items_to_reveal(
         &mut self,
         dock_to_reveal: Option<DockPosition>,
@@ -1946,18 +1994,7 @@ impl Workspace {
             self.zoomed = None;
         }
         self.zoomed_position = None;
-
-        self.update_followers(
-            proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
-                id: self.active_item(cx).and_then(|item| {
-                    item.to_followable_item_handle(cx)?
-                        .remote_id(&self.app_state.client, cx)
-                        .map(|id| id.to_proto())
-                }),
-                leader_id: self.leader_for_pane(&pane),
-            }),
-            cx,
-        );
+        self.update_active_view_for_followers(cx);
 
         cx.notify();
     }
@@ -2293,11 +2330,11 @@ impl Workspace {
         // (https://github.com/zed-industries/zed/issues/1290)
         let is_fullscreen = cx.window_is_fullscreen();
         let container_theme = if is_fullscreen {
-            let mut container_theme = theme.workspace.titlebar.container;
+            let mut container_theme = theme.titlebar.container;
             container_theme.padding.left = container_theme.padding.right;
             container_theme
         } else {
-            theme.workspace.titlebar.container
+            theme.titlebar.container
         };
 
         enum TitleBar {}
@@ -2317,7 +2354,7 @@ impl Workspace {
             }
         })
         .constrained()
-        .with_height(theme.workspace.titlebar.height)
+        .with_height(theme.titlebar.height)
         .into_any_named("titlebar")
     }
 
@@ -2646,6 +2683,30 @@ impl Workspace {
         Ok(())
     }
 
+    fn update_active_view_for_followers(&self, cx: &AppContext) {
+        if self.active_pane.read(cx).has_focus() {
+            self.update_followers(
+                proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
+                    id: self.active_item(cx).and_then(|item| {
+                        item.to_followable_item_handle(cx)?
+                            .remote_id(&self.app_state.client, cx)
+                            .map(|id| id.to_proto())
+                    }),
+                    leader_id: self.leader_for_pane(&self.active_pane),
+                }),
+                cx,
+            );
+        } else {
+            self.update_followers(
+                proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
+                    id: None,
+                    leader_id: None,
+                }),
+                cx,
+            );
+        }
+    }
+
     fn update_followers(
         &self,
         update: proto::update_followers::Variant,
@@ -2693,12 +2754,10 @@ impl Workspace {
                             .and_then(|id| state.items_by_leader_view_id.get(&id))
                         {
                             items_to_activate.push((pane.clone(), item.boxed_clone()));
-                        } else {
-                            if let Some(shared_screen) =
-                                self.shared_screen_for_peer(leader_id, pane, cx)
-                            {
-                                items_to_activate.push((pane.clone(), Box::new(shared_screen)));
-                            }
+                        } else if let Some(shared_screen) =
+                            self.shared_screen_for_peer(leader_id, pane, cx)
+                        {
+                            items_to_activate.push((pane.clone(), Box::new(shared_screen)));
                         }
                     }
                 }
@@ -2740,7 +2799,7 @@ impl Workspace {
         let call = self.active_call()?;
         let room = call.read(cx).room()?.read(cx);
         let participant = room.remote_participant_for_peer_id(peer_id)?;
-        let track = participant.tracks.values().next()?.clone();
+        let track = participant.video_tracks.values().next()?.clone();
         let user = participant.user.clone();
 
         for item in pane.read(cx).items_of_type::<SharedScreen>() {
@@ -2838,7 +2897,7 @@ impl Workspace {
         cx.notify();
     }
 
-    fn serialize_workspace(&self, cx: &AppContext) {
+    fn serialize_workspace(&self, cx: &ViewContext<Self>) {
         fn serialize_pane_handle(
             pane_handle: &ViewHandle<Pane>,
             cx: &AppContext,
@@ -2881,7 +2940,7 @@ impl Workspace {
             }
         }
 
-        fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
+        fn build_serialized_docks(this: &Workspace, cx: &ViewContext<Workspace>) -> DockStructure {
             let left_dock = this.left_dock.read(cx);
             let left_visible = left_dock.is_open();
             let left_active_panel = left_dock.visible_panel().and_then(|panel| {
@@ -2890,6 +2949,10 @@ impl Workspace {
                         .to_string(),
                 )
             });
+            let left_dock_zoom = left_dock
+                .visible_panel()
+                .map(|panel| panel.is_zoomed(cx))
+                .unwrap_or(false);
 
             let right_dock = this.right_dock.read(cx);
             let right_visible = right_dock.is_open();
@@ -2899,6 +2962,10 @@ impl Workspace {
                         .to_string(),
                 )
             });
+            let right_dock_zoom = right_dock
+                .visible_panel()
+                .map(|panel| panel.is_zoomed(cx))
+                .unwrap_or(false);
 
             let bottom_dock = this.bottom_dock.read(cx);
             let bottom_visible = bottom_dock.is_open();
@@ -2908,19 +2975,26 @@ impl Workspace {
                         .to_string(),
                 )
             });
+            let bottom_dock_zoom = bottom_dock
+                .visible_panel()
+                .map(|panel| panel.is_zoomed(cx))
+                .unwrap_or(false);
 
             DockStructure {
                 left: DockData {
                     visible: left_visible,
                     active_panel: left_active_panel,
+                    zoom: left_dock_zoom,
                 },
                 right: DockData {
                     visible: right_visible,
                     active_panel: right_active_panel,
+                    zoom: right_dock_zoom,
                 },
                 bottom: DockData {
                     visible: bottom_visible,
                     active_panel: bottom_active_panel,
+                    zoom: bottom_dock_zoom,
                 },
             }
         }
@@ -3033,14 +3107,31 @@ impl Workspace {
                                 dock.activate_panel(ix, cx);
                             }
                         }
+                                dock.active_panel()
+                                    .map(|panel| {
+                                        panel.set_zoomed(docks.left.zoom, cx)
+                                    });
+                                if docks.left.visible && docks.left.zoom {
+                                    cx.focus_self()
+                                }
                     });
+                    // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
                     workspace.right_dock.update(cx, |dock, cx| {
                         dock.set_open(docks.right.visible, cx);
                         if let Some(active_panel) = docks.right.active_panel {
                             if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
                                 dock.activate_panel(ix, cx);
+
                             }
                         }
+                                dock.active_panel()
+                                    .map(|panel| {
+                                        panel.set_zoomed(docks.right.zoom, cx)
+                                    });
+
+                                if docks.right.visible && docks.right.zoom {
+                                    cx.focus_self()
+                                }
                     });
                     workspace.bottom_dock.update(cx, |dock, cx| {
                         dock.set_open(docks.bottom.visible, cx);
@@ -3049,8 +3140,18 @@ impl Workspace {
                                 dock.activate_panel(ix, cx);
                             }
                         }
+
+                        dock.active_panel()
+                            .map(|panel| {
+                                panel.set_zoomed(docks.bottom.zoom, cx)
+                            });
+
+                        if docks.bottom.visible && docks.bottom.zoom {
+                            cx.focus_self()
+                        }
                     });
 
+
                     cx.notify();
                 })?;
 
@@ -3429,7 +3530,7 @@ impl View for Workspace {
                                         )
                                     }))
                                     .with_children(self.modal.as_ref().map(|modal| {
-                                        ChildView::new(modal, cx)
+                                        ChildView::new(modal.view.as_any(), cx)
                                             .contained()
                                             .with_style(theme.workspace.modal)
                                             .aligned()
@@ -4413,7 +4514,7 @@ mod tests {
         workspace.read_with(cx, |workspace, cx| {
             assert!(workspace.right_dock().read(cx).is_open());
             assert!(!panel.is_zoomed(cx));
-            assert!(!panel.has_focus(cx));
+            assert!(panel.has_focus(cx));
         });
 
         // Focus and zoom panel
@@ -4488,7 +4589,7 @@ mod tests {
         workspace.read_with(cx, |workspace, cx| {
             let pane = pane.read(cx);
             assert!(!pane.is_zoomed());
-            assert!(pane.has_focus());
+            assert!(!pane.has_focus());
             assert!(workspace.right_dock().read(cx).is_open());
             assert!(workspace.zoomed.is_none());
         });

crates/xtask/Cargo.toml πŸ”—

@@ -0,0 +1,13 @@
+[package]
+name = "xtask"
+version = "0.1.0"
+edition = "2021"
+publish = false
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0"
+clap = {version = "4.0", features = ["derive"]}
+theme = {path = "../theme"}
+serde_json.workspace = true
+schemars.workspace = true

crates/xtask/src/cli.rs πŸ”—

@@ -0,0 +1,23 @@
+use clap::{Parser, Subcommand};
+use std::path::PathBuf;
+/// Common utilities for Zed developers.
+// For more information, see [matklad's repository README](https://github.com/matklad/cargo-xtask/)
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+#[command(propagate_version = true)]
+pub struct Cli {
+    #[command(subcommand)]
+    pub command: Commands,
+}
+
+/// Command to run.
+#[derive(Subcommand)]
+pub enum Commands {
+    /// Builds theme types for interop with Typescript.
+    BuildThemeTypes {
+        #[clap(short, long, default_value = "schemas")]
+        out_dir: PathBuf,
+        #[clap(short, long, default_value = "theme.json")]
+        file_name: PathBuf,
+    },
+}

crates/xtask/src/main.rs πŸ”—

@@ -0,0 +1,29 @@
+mod cli;
+
+use std::path::PathBuf;
+
+use anyhow::Result;
+use clap::Parser;
+use schemars::schema_for;
+use theme::Theme;
+
+fn build_themes(out_dir: PathBuf, file_name: PathBuf) -> Result<()> {
+    let theme = schema_for!(Theme);
+    let output = serde_json::to_string_pretty(&theme)?;
+
+    std::fs::create_dir(&out_dir)?;
+
+    let mut file_path = out_dir;
+    file_path.push(file_name);
+
+    std::fs::write(file_path, output)?;
+
+    Ok(())
+}
+
+fn main() -> Result<()> {
+    let args = cli::Cli::parse();
+    match args.command {
+        cli::Commands::BuildThemeTypes { out_dir, file_name } => build_themes(out_dir, file_name),
+    }
+}

crates/zed-actions/Cargo.toml πŸ”—

@@ -0,0 +1,10 @@
+[package]
+name = "zed-actions"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+gpui = { path = "../gpui" }

crates/zed-actions/src/lib.rs πŸ”—

@@ -0,0 +1,28 @@
+use gpui::actions;
+
+actions!(
+    zed,
+    [
+        About,
+        Hide,
+        HideOthers,
+        ShowAll,
+        Minimize,
+        Zoom,
+        ToggleFullScreen,
+        Quit,
+        DebugElements,
+        OpenLog,
+        OpenLicenses,
+        OpenTelemetryLog,
+        OpenKeymap,
+        OpenSettings,
+        OpenLocalSettings,
+        OpenDefaultSettings,
+        OpenDefaultKeymap,
+        IncreaseBufferFontSize,
+        DecreaseBufferFontSize,
+        ResetBufferFontSize,
+        ResetDatabase,
+    ]
+);

crates/zed/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.92.0"
+version = "0.95.0"
 publish = false
 
 [lib]
@@ -16,6 +16,7 @@ name = "Zed"
 path = "src/main.rs"
 
 [dependencies]
+audio = { path = "../audio" }
 activity_indicator = { path = "../activity_indicator" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
@@ -62,12 +63,11 @@ text = { path = "../text" }
 terminal_view = { path = "../terminal_view" }
 theme = { path = "../theme" }
 theme_selector = { path = "../theme_selector" }
-theme_testbench = { path = "../theme_testbench" }
 util = { path = "../util" }
 vim = { path = "../vim" }
 workspace = { path = "../workspace" }
 welcome = { path = "../welcome" }
-
+zed-actions = {path = "../zed-actions"}
 anyhow.workspace = true
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-tar = "0.4.2"

crates/zed/src/assets.rs πŸ”—

@@ -7,6 +7,7 @@ use rust_embed::RustEmbed;
 #[include = "fonts/**/*"]
 #[include = "icons/**/*"]
 #[include = "themes/**/*"]
+#[include = "sounds/**/*"]
 #[include = "*.md"]
 #[exclude = "*.DS_Store"]
 pub struct Assets;

crates/zed/src/languages/c.rs πŸ”—

@@ -2,14 +2,14 @@ use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 pub use language::*;
+use lsp::LanguageServerBinary;
 use smol::fs::{self, File};
 use std::{any::Any, path::PathBuf, sync::Arc};
-use util::fs::remove_matching;
-use util::github::latest_github_release;
-use util::http::HttpClient;
-use util::ResultExt;
-
-use util::github::GitHubLspBinaryVersion;
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 pub struct CLspAdapter;
 
@@ -21,9 +21,9 @@ impl super::LspAdapter for CLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("clangd/clangd", false, http).await?;
+        let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?;
         let asset_name = format!("clangd-mac-{}.zip", release.name);
         let asset = release
             .assets
@@ -40,8 +40,8 @@ impl super::LspAdapter for CLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
@@ -49,7 +49,8 @@ impl super::LspAdapter for CLspAdapter {
         let binary_path = version_dir.join("bin/clangd");
 
         if fs::metadata(&binary_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .context("error downloading release")?;
@@ -81,32 +82,24 @@ impl super::LspAdapter for CLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last_clangd_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_clangd_dir = Some(entry.path());
-                }
-            }
-            let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let clangd_bin = clangd_dir.join("bin/clangd");
-            if clangd_bin.exists() {
-                Ok(LanguageServerBinary {
-                    path: clangd_bin,
-                    arguments: vec![],
-                })
-            } else {
-                Err(anyhow!(
-                    "missing clangd binary in directory {:?}",
-                    clangd_dir
-                ))
-            }
-        })()
-        .await
-        .log_err()
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
     }
 
     async fn label_for_completion(
@@ -246,6 +239,34 @@ impl super::LspAdapter for CLspAdapter {
     }
 }
 
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_clangd_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_clangd_dir = Some(entry.path());
+            }
+        }
+        let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let clangd_bin = clangd_dir.join("bin/clangd");
+        if clangd_bin.exists() {
+            Ok(LanguageServerBinary {
+                path: clangd_bin,
+                arguments: vec![],
+            })
+        } else {
+            Err(anyhow!(
+                "missing clangd binary in directory {:?}",
+                clangd_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::TestAppContext;

crates/zed/src/languages/elixir.rs πŸ”—

@@ -1,16 +1,23 @@
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
+use gpui::{AsyncAppContext, Task};
 pub use language::*;
-use lsp::{CompletionItemKind, SymbolKind};
+use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
 use smol::fs::{self, File};
-use std::{any::Any, path::PathBuf, sync::Arc};
-use util::fs::remove_matching;
-use util::github::latest_github_release;
-use util::http::HttpClient;
-use util::ResultExt;
-
-use util::github::GitHubLspBinaryVersion;
+use std::{
+    any::Any,
+    path::PathBuf,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 pub struct ElixirLspAdapter;
 
@@ -20,19 +27,58 @@ impl LspAdapter for ElixirLspAdapter {
         LanguageServerName("elixir-ls".into())
     }
 
+    fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|mut cx| async move {
+            let elixir_output = smol::process::Command::new("elixir")
+                .args(["--version"])
+                .output()
+                .await;
+            if elixir_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })
+                }
+                return Err(anyhow!("cannot run elixir-ls"));
+            }
+
+            Ok(())
+        }))
+    }
+
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
+        let http = delegate.http_client();
         let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
-        let asset_name = "elixir-ls.zip";
+        let version_name = release
+            .name
+            .strip_prefix("Release ")
+            .context("Elixir-ls release name does not start with prefix")?
+            .to_owned();
+
+        let asset_name = format!("elixir-ls-{}.zip", &version_name);
         let asset = release
             .assets
             .iter()
             .find(|asset| asset.name == asset_name)
             .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+
         let version = GitHubLspBinaryVersion {
-            name: release.name,
+            name: version_name,
             url: asset.browser_download_url.clone(),
         };
         Ok(Box::new(version) as Box<_>)
@@ -41,8 +87,8 @@ impl LspAdapter for ElixirLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
@@ -50,7 +96,8 @@ impl LspAdapter for ElixirLspAdapter {
         let binary_path = version_dir.join("language_server.sh");
 
         if fs::metadata(&binary_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .context("error downloading release")?;
@@ -76,7 +123,7 @@ impl LspAdapter for ElixirLspAdapter {
                 .await?
                 .status;
             if !unzip_status.success() {
-                Err(anyhow!("failed to unzip clangd archive"))?;
+                Err(anyhow!("failed to unzip elixir-ls archive"))?;
             }
 
             remove_matching(&container_dir, |entry| entry != version_dir).await;
@@ -88,21 +135,19 @@ impl LspAdapter for ElixirLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                last = Some(entry?.path());
-            }
-            last.map(|path| LanguageServerBinary {
-                path,
-                arguments: vec![],
-            })
-            .ok_or_else(|| anyhow!("no cached binary"))
-        })()
-        .await
-        .log_err()
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
     }
 
     async fn label_for_completion(
@@ -188,3 +233,20 @@ impl LspAdapter for ElixirLspAdapter {
         })
     }
 }
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+        last.map(|path| LanguageServerBinary {
+            path,
+            arguments: vec![],
+        })
+        .ok_or_else(|| anyhow!("no cached binary"))
+    })()
+    .await
+    .log_err()
+}

crates/zed/src/languages/elixir/highlights.scm πŸ”—

@@ -36,8 +36,6 @@
 
 (char) @constant
 
-(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
-
 (escape_sequence) @string.escape
 
 [
@@ -146,3 +144,10 @@
   "<<"
   ">>"
 ] @punctuation.bracket
+
+(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
+
+((sigil
+  (sigil_name) @_sigil_name
+  (quoted_content) @embedded)
+ (#eq? @_sigil_name "H"))

crates/zed/src/languages/go.rs πŸ”—

@@ -1,16 +1,24 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
+use gpui::{AsyncAppContext, Task};
 pub use language::*;
 use lazy_static::lazy_static;
+use lsp::LanguageServerBinary;
 use regex::Regex;
 use smol::{fs, process};
-use std::ffi::{OsStr, OsString};
-use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc};
-use util::fs::remove_matching;
-use util::github::latest_github_release;
-use util::http::HttpClient;
-use util::ResultExt;
+use std::{
+    any::Any,
+    ffi::{OsStr, OsString},
+    ops::Range,
+    path::PathBuf,
+    str,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{fs::remove_matching, github::latest_github_release, ResultExt};
 
 fn server_binary_arguments() -> Vec<OsString> {
     vec!["-mode=stdio".into()]
@@ -31,9 +39,9 @@ impl super::LspAdapter for GoLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("golang/tools", false, http).await?;
+        let release = latest_github_release("golang/tools", false, delegate.http_client()).await?;
         let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
         if version.is_none() {
             log::warn!(
@@ -44,11 +52,39 @@ impl super::LspAdapter for GoLspAdapter {
         Ok(Box::new(version) as Box<_>)
     }
 
+    fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str =
+            "Could not install the Go language server `gopls`, because `go` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|mut cx| async move {
+            let install_output = process::Command::new("go").args(["version"]).output().await;
+            if install_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })
+                }
+                return Err(anyhow!("cannot install gopls"));
+            }
+            Ok(())
+        }))
+    }
+
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<Option<String>>().unwrap();
         let this = *self;
@@ -68,7 +104,10 @@ impl super::LspAdapter for GoLspAdapter {
                     });
                 }
             }
-        } else if let Some(path) = this.cached_server_binary(container_dir.clone()).await {
+        } else if let Some(path) = this
+            .cached_server_binary(container_dir.clone(), delegate)
+            .await
+        {
             return Ok(path);
         }
 
@@ -105,33 +144,24 @@ impl super::LspAdapter for GoLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last_binary_path = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_file()
-                    && entry
-                        .file_name()
-                        .to_str()
-                        .map_or(false, |name| name.starts_with("gopls_"))
-                {
-                    last_binary_path = Some(entry.path());
-                }
-            }
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
 
-            if let Some(path) = last_binary_path {
-                Ok(LanguageServerBinary {
-                    path,
-                    arguments: server_binary_arguments(),
-                })
-            } else {
-                Err(anyhow!("no cached binary"))
-            }
-        })()
-        .await
-        .log_err()
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
     }
 
     async fn label_for_completion(
@@ -294,6 +324,35 @@ impl super::LspAdapter for GoLspAdapter {
     }
 }
 
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name.starts_with("gopls_"))
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: server_binary_arguments(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })()
+    .await
+    .log_err()
+}
+
 fn adjust_runs(
     delta: usize,
     mut runs: Vec<(Range<usize>, HighlightId)>,

crates/zed/src/languages/heex/highlights.scm πŸ”—

@@ -1,17 +1,11 @@
 ; HEEx delimiters
 [
-  "%>"
   "--%>"
   "-->"
   "/>"
   "<!"
   "<!--"
   "<"
-  "<%!--"
-  "<%"
-  "<%#"
-  "<%%="
-  "<%="
   "</"
   "</:"
   "<:"
@@ -20,6 +14,15 @@
   "}"
 ] @punctuation.bracket
 
+[
+  "<%!--"
+  "<%"
+  "<%#"
+  "<%%="
+  "<%="
+  "%>"
+] @keyword
+
 ; HEEx operators are highlighted as such
 "=" @operator
 

crates/zed/src/languages/heex/injections.scm πŸ”—

@@ -1,13 +1,13 @@
-((directive (partial_expression_value) @content)
- (#set! language "elixir")
- (#set! include-children)
- (#set! combined))
+(
+  (directive
+    [
+      (partial_expression_value)
+      (expression_value)
+      (ending_expression_value)
+    ] @content)
+  (#set! language "elixir")
+  (#set! combined)
+)
 
-; Regular expression_values do not need to be combined
-((directive (expression_value) @content)
- (#set! language "elixir"))
-
-; expressions live within HTML tags, and do not need to be combined
-;     <link href={ Routes.static_path(..) } />
 ((expression (expression_value) @content)
  (#set! language "elixir"))

crates/zed/src/languages/html.rs πŸ”—

@@ -1,16 +1,22 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
 use smol::fs;
-use std::ffi::OsString;
-use std::path::Path;
-use std::{any::Any, path::PathBuf, sync::Arc};
-use util::http::HttpClient;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
 use util::ResultExt;
 
+const SERVER_PATH: &'static str =
+    "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
 }
@@ -20,9 +26,6 @@ pub struct HtmlLspAdapter {
 }
 
 impl HtmlLspAdapter {
-    const SERVER_PATH: &'static str =
-        "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
-
     pub fn new(node: Arc<NodeRuntime>) -> Self {
         HtmlLspAdapter { node }
     }
@@ -36,7 +39,7 @@ impl LspAdapter for HtmlLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node
@@ -48,11 +51,11 @@ impl LspAdapter for HtmlLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
-        let server_path = container_dir.join(Self::SERVER_PATH);
+        let server_path = container_dir.join(SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
@@ -69,32 +72,19 @@ impl LspAdapter for HtmlLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let server_path = last_version_dir.join(Self::SERVER_PATH);
-            if server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: server_binary_arguments(&server_path),
-                })
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
-            }
-        })()
-        .await
-        .log_err()
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -103,3 +93,34 @@ impl LspAdapter for HtmlLspAdapter {
         }))
     }
 }
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed/src/languages/json.rs πŸ”—

@@ -3,7 +3,8 @@ use async_trait::async_trait;
 use collections::HashMap;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
-use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
 use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -16,7 +17,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::{paths, ResultExt};
 
 const SERVER_PATH: &'static str =
@@ -45,7 +45,7 @@ impl LspAdapter for JsonLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(
             self.node
@@ -57,8 +57,8 @@ impl LspAdapter for JsonLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
         let server_path = container_dir.join(SERVER_PATH);
@@ -78,33 +78,19 @@ impl LspAdapter for JsonLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
 
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let server_path = last_version_dir.join(SERVER_PATH);
-            if server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: server_binary_arguments(&server_path),
-                })
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
-            }
-        })()
-        .await
-        .log_err()
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
     }
 
     async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -157,6 +143,38 @@ impl LspAdapter for JsonLspAdapter {
     }
 }
 
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
 fn schema_file_match(path: &Path) -> &Path {
     path.strip_prefix(path.parent().unwrap().parent().unwrap())
         .unwrap()

crates/zed/src/languages/language_plugin.rs πŸ”—

@@ -3,10 +3,10 @@ use async_trait::async_trait;
 use collections::HashMap;
 use futures::lock::Mutex;
 use gpui::executor::Background;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
 use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
 use std::{any::Any, path::PathBuf, sync::Arc};
-use util::http::HttpClient;
 use util::ResultExt;
 
 #[allow(dead_code)]
@@ -72,7 +72,7 @@ impl LspAdapter for PluginLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         let runtime = self.runtime.clone();
         let function = self.fetch_latest_server_version;
@@ -92,8 +92,8 @@ impl LspAdapter for PluginLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = *version.downcast::<String>().unwrap();
         let runtime = self.runtime.clone();
@@ -110,7 +110,11 @@ impl LspAdapter for PluginLspAdapter {
             .await
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         let runtime = self.runtime.clone();
         let function = self.cached_server_binary;
 
@@ -126,6 +130,14 @@ impl LspAdapter for PluginLspAdapter {
             .await
     }
 
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
     async fn initialization_options(&self) -> Option<serde_json::Value> {
         let string: String = self
             .runtime

crates/zed/src/languages/lua.rs πŸ”—

@@ -3,12 +3,15 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{io::BufReader, StreamExt};
-use language::{LanguageServerBinary, LanguageServerName};
+use language::{LanguageServerName, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
 use smol::fs;
-use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
-use util::{async_iife, github::latest_github_release, http::HttpClient, ResultExt};
-
-use util::github::GitHubLspBinaryVersion;
+use std::{any::Any, env::consts, ffi::OsString, path::PathBuf};
+use util::{
+    async_iife,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 #[derive(Copy, Clone)]
 pub struct LuaLspAdapter;
@@ -28,9 +31,11 @@ impl super::LspAdapter for LuaLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("LuaLS/lua-language-server", false, http).await?;
+        let release =
+            latest_github_release("LuaLS/lua-language-server", false, delegate.http_client())
+                .await?;
         let version = release.name.clone();
         let platform = match consts::ARCH {
             "x86_64" => "x64",
@@ -53,15 +58,16 @@ impl super::LspAdapter for LuaLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 
         let binary_path = container_dir.join("bin/lua-language-server");
 
         if fs::metadata(&binary_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .map_err(|err| anyhow!("error downloading release: {}", err))?;
@@ -81,32 +87,52 @@ impl super::LspAdapter for LuaLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        async_iife!({
-            let mut last_binary_path = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_file()
-                    && entry
-                        .file_name()
-                        .to_str()
-                        .map_or(false, |name| name == "lua-language-server")
-                {
-                    last_binary_path = Some(entry.path());
-                }
-            }
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
 
-            if let Some(path) = last_binary_path {
-                Ok(LanguageServerBinary {
-                    path,
-                    arguments: server_binary_arguments(),
-                })
-            } else {
-                Err(anyhow!("no cached binary"))
-            }
-        })
-        .await
-        .log_err()
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--version".into()];
+                binary
+            })
     }
 }
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_iife!({
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "lua-language-server")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: server_binary_arguments(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}

crates/zed/src/languages/python.rs πŸ”—

@@ -1,7 +1,8 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use smol::fs;
 use std::{
@@ -10,9 +11,10 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::ResultExt;
 
+const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
 }
@@ -22,8 +24,6 @@ pub struct PythonLspAdapter {
 }
 
 impl PythonLspAdapter {
-    const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
-
     pub fn new(node: Arc<NodeRuntime>) -> Self {
         PythonLspAdapter { node }
     }
@@ -37,7 +37,7 @@ impl LspAdapter for PythonLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
     }
@@ -45,11 +45,11 @@ impl LspAdapter for PythonLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
-        let server_path = container_dir.join(Self::SERVER_PATH);
+        let server_path = container_dir.join(SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
@@ -63,32 +63,19 @@ impl LspAdapter for PythonLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let server_path = last_version_dir.join(Self::SERVER_PATH);
-            if server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: server_binary_arguments(&server_path),
-                })
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
-            }
-        })()
-        .await
-        .log_err()
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
     }
 
     async fn process_completion(&self, item: &mut lsp::CompletionItem) {
@@ -167,6 +154,37 @@ impl LspAdapter for PythonLspAdapter {
     }
 }
 
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::{ModelContext, TestAppContext};

crates/zed/src/languages/ruby.rs πŸ”—

@@ -1,8 +1,8 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
 use std::{any::Any, path::PathBuf, sync::Arc};
-use util::http::HttpClient;
 
 pub struct RubyLanguageServer;
 
@@ -14,7 +14,7 @@ impl LspAdapter for RubyLanguageServer {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(()))
     }
@@ -22,19 +22,31 @@ impl LspAdapter for RubyLanguageServer {
     async fn fetch_server_binary(
         &self,
         _version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         Err(anyhow!("solargraph must be installed manually"))
     }
 
-    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         Some(LanguageServerBinary {
             path: "solargraph".into(),
             arguments: vec!["stdio".into()],
         })
     }
 
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
     async fn label_for_completion(
         &self,
         item: &lsp::CompletionItem,

crates/zed/src/languages/rust.rs πŸ”—

@@ -4,13 +4,15 @@ use async_trait::async_trait;
 use futures::{io::BufReader, StreamExt};
 pub use language::*;
 use lazy_static::lazy_static;
+use lsp::LanguageServerBinary;
 use regex::Regex;
 use smol::fs::{self, File};
 use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
-use util::fs::remove_matching;
-use util::github::{latest_github_release, GitHubLspBinaryVersion};
-use util::http::HttpClient;
-use util::ResultExt;
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 pub struct RustLspAdapter;
 
@@ -22,9 +24,11 @@ impl LspAdapter for RustLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("rust-analyzer/rust-analyzer", false, http).await?;
+        let release =
+            latest_github_release("rust-analyzer/rust-analyzer", false, delegate.http_client())
+                .await?;
         let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
         let asset = release
             .assets
@@ -40,14 +44,15 @@ impl LspAdapter for RustLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 
         if fs::metadata(&destination_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .map_err(|err| anyhow!("error downloading release: {}", err))?;
@@ -69,21 +74,24 @@ impl LspAdapter for RustLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                last = Some(entry?.path());
-            }
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir).await
+    }
 
-            anyhow::Ok(LanguageServerBinary {
-                path: last.ok_or_else(|| anyhow!("no cached binary"))?,
-                arguments: Default::default(),
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
             })
-        })()
-        .await
-        .log_err()
     }
 
     async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
@@ -250,6 +258,22 @@ impl LspAdapter for RustLspAdapter {
         })
     }
 }
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            last = Some(entry?.path());
+        }
+
+        anyhow::Ok(LanguageServerBinary {
+            path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+            arguments: Default::default(),
+        })
+    })()
+    .await
+    .log_err()
+}
 
 #[cfg(test)]
 mod tests {

crates/zed/src/languages/typescript.rs πŸ”—

@@ -4,8 +4,8 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt};
 use gpui::AppContext;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
-use lsp::CodeActionKind;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::{CodeActionKind, LanguageServerBinary};
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
 use smol::{fs, io::BufReader, stream::StreamExt};
@@ -16,7 +16,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{fs::remove_matching, github::latest_github_release, http::HttpClient};
+use util::{fs::remove_matching, github::latest_github_release};
 use util::{github::GitHubLspBinaryVersion, ResultExt};
 
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@@ -58,7 +58,7 @@ impl LspAdapter for TypeScriptLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(TypeScriptVersions {
             typescript_version: self.node.npm_package_latest_version("typescript").await?,
@@ -72,8 +72,8 @@ impl LspAdapter for TypeScriptLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<TypeScriptVersions>().unwrap();
         let server_path = container_dir.join(Self::NEW_SERVER_PATH);
@@ -99,29 +99,19 @@ impl LspAdapter for TypeScriptLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
-            let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
-            if new_server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: typescript_server_binary_arguments(&new_server_path),
-                })
-            } else if old_server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: typescript_server_binary_arguments(&old_server_path),
-                })
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    container_dir
-                ))
-            }
-        })()
-        .await
-        .log_err()
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &self.node).await
     }
 
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -169,6 +159,34 @@ impl LspAdapter for TypeScriptLspAdapter {
     }
 }
 
+async fn get_cached_ts_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
+        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
+        if new_server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: typescript_server_binary_arguments(&new_server_path),
+            })
+        } else if old_server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: typescript_server_binary_arguments(&old_server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                container_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}
+
 pub struct EsLintLspAdapter {
     node: Arc<NodeRuntime>,
 }
@@ -204,12 +222,13 @@ impl LspAdapter for EsLintLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         // At the time of writing the latest vscode-eslint release was released in 2020 and requires
         // special custom LSP protocol extensions be handled to fully initialize. Download the latest
         // prerelease instead to sidestep this issue
-        let release = latest_github_release("microsoft/vscode-eslint", true, http).await?;
+        let release =
+            latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?;
         Ok(Box::new(GitHubLspBinaryVersion {
             name: release.name,
             url: release.tarball_url,
@@ -219,8 +238,8 @@ impl LspAdapter for EsLintLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
@@ -229,7 +248,8 @@ impl LspAdapter for EsLintLspAdapter {
         if fs::metadata(&server_path).await.is_err() {
             remove_matching(&container_dir, |entry| entry != destination_path).await;
 
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .map_err(|err| anyhow!("error downloading release: {}", err))?;
@@ -243,11 +263,11 @@ impl LspAdapter for EsLintLspAdapter {
             fs::rename(first.path(), &repo_root).await?;
 
             self.node
-                .run_npm_subcommand(&repo_root, "install", &[])
+                .run_npm_subcommand(Some(&repo_root), "install", &[])
                 .await?;
 
             self.node
-                .run_npm_subcommand(&repo_root, "run-script", &["compile"])
+                .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
                 .await?;
         }
 
@@ -257,22 +277,19 @@ impl LspAdapter for EsLintLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            // This is unfortunate but we don't know what the version is to build a path directly
-            let mut dir = fs::read_dir(&container_dir).await?;
-            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
-            if !first.file_type().await?.is_dir() {
-                return Err(anyhow!("First entry is not a directory"));
-            }
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_eslint_server_binary(container_dir, &self.node).await
+    }
 
-            Ok(LanguageServerBinary {
-                path: first.path().join(Self::SERVER_PATH),
-                arguments: Default::default(),
-            })
-        })()
-        .await
-        .log_err()
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_eslint_server_binary(container_dir, &self.node).await
     }
 
     async fn label_for_completion(
@@ -288,6 +305,28 @@ impl LspAdapter for EsLintLspAdapter {
     }
 }
 
+async fn get_cached_eslint_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        // This is unfortunate but we don't know what the version is to build a path directly
+        let mut dir = fs::read_dir(&container_dir).await?;
+        let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+        if !first.file_type().await?.is_dir() {
+            return Err(anyhow!("First entry is not a directory"));
+        }
+        let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
+
+        Ok(LanguageServerBinary {
+            path: node.binary_path().await?,
+            arguments: eslint_server_binary_arguments(&server_path),
+        })
+    })()
+    .await
+    .log_err()
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::TestAppContext;

crates/zed/src/languages/yaml.rs πŸ”—

@@ -3,8 +3,9 @@ use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
 use language::{
-    language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
+    language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
 };
+use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::Value;
 use smol::fs;
@@ -15,9 +16,10 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::ResultExt;
 
+const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
 }
@@ -27,8 +29,6 @@ pub struct YamlLspAdapter {
 }
 
 impl YamlLspAdapter {
-    const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
-
     pub fn new(node: Arc<NodeRuntime>) -> Self {
         YamlLspAdapter { node }
     }
@@ -42,7 +42,7 @@ impl LspAdapter for YamlLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node
@@ -54,11 +54,11 @@ impl LspAdapter for YamlLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
-        let server_path = container_dir.join(Self::SERVER_PATH);
+        let server_path = container_dir.join(SERVER_PATH);
 
         if fs::metadata(&server_path).await.is_err() {
             self.node
@@ -72,34 +72,20 @@ impl LspAdapter for YamlLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
-        (|| async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let server_path = last_version_dir.join(Self::SERVER_PATH);
-            if server_path.exists() {
-                Ok(LanguageServerBinary {
-                    path: self.node.binary_path().await?,
-                    arguments: server_binary_arguments(&server_path),
-                })
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
-            }
-        })()
-        .await
-        .log_err()
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
     }
 
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
     fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
         let tab_size = all_language_settings(None, cx)
             .language(Some("YAML"))
@@ -117,3 +103,34 @@ impl LspAdapter for YamlLspAdapter {
         )
     }
 }
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| async move {
+        let mut last_version_dir = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_dir() {
+                last_version_dir = Some(entry.path());
+            }
+        }
+        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+        let server_path = last_version_dir.join(SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                arguments: server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                last_version_dir
+            ))
+        }
+    })()
+    .await
+    .log_err()
+}

crates/zed/src/main.rs πŸ”—

@@ -31,7 +31,6 @@ use std::{
     ffi::OsStr,
     fs::OpenOptions,
     io::Write as _,
-    ops::Not,
     os::unix::prelude::OsStrExt,
     panic,
     path::{Path, PathBuf},
@@ -49,6 +48,7 @@ use util::{
     http::{self, HttpClient},
     paths::PathLikeWithPosition,
 };
+use uuid::Uuid;
 use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
@@ -69,9 +69,8 @@ fn main() {
     log::info!("========== starting zed ==========");
     let mut app = gpui::App::new(Assets).unwrap();
 
-    init_panic_hook(&app);
-
-    app.background();
+    let installation_id = app.background().block(installation_id()).ok();
+    init_panic_hook(&app, installation_id.clone());
 
     load_embedded_fonts(&app);
 
@@ -132,7 +131,7 @@ fn main() {
         languages.set_executor(cx.background().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
-        let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
+        let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned());
 
         languages::init(languages.clone(), node_runtime.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
@@ -155,7 +154,6 @@ fn main() {
         search::init(cx);
         vim::init(cx);
         terminal_view::init(cx);
-        theme_testbench::init(cx);
         copilot::init(http.clone(), node_runtime, cx);
         ai::init(cx);
 
@@ -170,7 +168,7 @@ fn main() {
         })
         .detach();
 
-        client.telemetry().start();
+        client.telemetry().start(installation_id);
 
         let app_state = Arc::new(AppState {
             languages,
@@ -182,6 +180,8 @@ fn main() {
             background_actions,
         });
         cx.set_global(Arc::downgrade(&app_state));
+
+        audio::init(Assets, cx);
         auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
 
         workspace::init(app_state.clone(), cx);
@@ -270,6 +270,22 @@ fn main() {
     });
 }
 
+async fn installation_id() -> Result<String> {
+    let legacy_key_name = "device_id";
+
+    if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) {
+        Ok(installation_id)
+    } else {
+        let installation_id = Uuid::new_v4().to_string();
+
+        KEY_VALUE_STORE
+            .write_kvp(legacy_key_name.to_string(), installation_id.clone())
+            .await?;
+
+        Ok(installation_id)
+    }
+}
+
 fn open_urls(
     urls: Vec<String>,
     cli_connections_tx: &mpsc::UnboundedSender<(
@@ -373,7 +389,8 @@ struct Panic {
     os_version: Option<String>,
     architecture: String,
     panicked_on: u128,
-    identifying_backtrace: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    installation_id: Option<String>,
 }
 
 #[derive(Serialize)]
@@ -382,7 +399,7 @@ struct PanicRequest {
     token: String,
 }
 
-fn init_panic_hook(app: &App) {
+fn init_panic_hook(app: &App, installation_id: Option<String>) {
     let is_pty = stdout_is_a_pty();
     let platform = app.platform();
 
@@ -401,61 +418,18 @@ fn init_panic_hook(app: &App) {
             .unwrap_or_else(|| "Box<Any>".to_string());
 
         let backtrace = Backtrace::new();
-        let backtrace = backtrace
+        let mut backtrace = backtrace
             .frames()
             .iter()
-            .filter_map(|frame| {
-                let symbol = frame.symbols().first()?;
-                let path = symbol.filename()?;
-                Some((path, symbol.lineno(), format!("{:#}", symbol.name()?)))
-            })
+            .filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?)))
             .collect::<Vec<_>>();
 
-        let this_file_path = Path::new(file!());
-
-        // Find the first frame in the backtrace for this panic hook itself. Exclude
-        // that frame and all frames before it.
-        let mut start_frame_ix = 0;
-        let mut codebase_root_path = None;
-        for (ix, (path, _, _)) in backtrace.iter().enumerate() {
-            if path.ends_with(this_file_path) {
-                start_frame_ix = ix + 1;
-                codebase_root_path = path.ancestors().nth(this_file_path.components().count());
-                break;
-            }
-        }
-
-        // Exclude any subsequent frames inside of rust's panic handling system.
-        while let Some((path, _, _)) = backtrace.get(start_frame_ix) {
-            if path.starts_with("/rustc") {
-                start_frame_ix += 1;
-            } else {
-                break;
-            }
-        }
-
-        // Build two backtraces:
-        // * one for display, which includes symbol names for all frames, and files
-        //   and line numbers for symbols in this codebase
-        // * one for identification and de-duplication, which only includes symbol
-        //   names for symbols in this codebase.
-        let mut display_backtrace = Vec::new();
-        let mut identifying_backtrace = Vec::new();
-        for (path, line, symbol) in &backtrace[start_frame_ix..] {
-            display_backtrace.push(symbol.clone());
-
-            if let Some(codebase_root_path) = &codebase_root_path {
-                if let Ok(suffix) = path.strip_prefix(&codebase_root_path) {
-                    identifying_backtrace.push(symbol.clone());
-
-                    let display_path = suffix.to_string_lossy();
-                    if let Some(line) = line {
-                        display_backtrace.push(format!("    {display_path}:{line}"));
-                    } else {
-                        display_backtrace.push(format!("    {display_path}"));
-                    }
-                }
-            }
+        // Strip out leading stack frames for rust panic-handling.
+        if let Some(ix) = backtrace
+            .iter()
+            .position(|name| name == "rust_begin_unwind")
+        {
+            backtrace.drain(0..=ix);
         }
 
         let panic_data = Panic {
@@ -477,29 +451,28 @@ fn init_panic_hook(app: &App) {
                 .duration_since(UNIX_EPOCH)
                 .unwrap()
                 .as_millis(),
-            backtrace: display_backtrace,
-            identifying_backtrace: identifying_backtrace
-                .is_empty()
-                .not()
-                .then_some(identifying_backtrace),
+            backtrace,
+            installation_id: installation_id.clone(),
         };
 
-        if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
-            if is_pty {
+        if is_pty {
+            if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
                 eprintln!("{}", panic_data_json);
                 return;
             }
-
-            let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
-            let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
-            let panic_file = std::fs::OpenOptions::new()
-                .append(true)
-                .create(true)
-                .open(&panic_file_path)
-                .log_err();
-            if let Some(mut panic_file) = panic_file {
-                write!(&mut panic_file, "{}", panic_data_json).log_err();
-                panic_file.flush().log_err();
+        } else {
+            if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
+                let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
+                let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
+                let panic_file = std::fs::OpenOptions::new()
+                    .append(true)
+                    .create(true)
+                    .open(&panic_file_path)
+                    .log_err();
+                if let Some(mut panic_file) = panic_file {
+                    writeln!(&mut panic_file, "{}", panic_data_json).log_err();
+                    panic_file.flush().log_err();
+                }
             }
         }
     }));
@@ -531,23 +504,45 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
                     }
 
                     if telemetry_settings.diagnostics {
-                        let panic_data_text = smol::fs::read_to_string(&child_path)
+                        let panic_file_content = smol::fs::read_to_string(&child_path)
                             .await
                             .context("error reading panic file")?;
 
-                        let body = serde_json::to_string(&PanicRequest {
-                            panic: serde_json::from_str(&panic_data_text)?,
-                            token: ZED_SECRET_CLIENT_TOKEN.into(),
-                        })
-                        .unwrap();
-
-                        let request = Request::post(&panic_report_url)
-                            .redirect_policy(isahc::config::RedirectPolicy::Follow)
-                            .header("Content-Type", "application/json")
-                            .body(body.into())?;
-                        let response = http.send(request).await.context("error sending panic")?;
-                        if !response.status().is_success() {
-                            log::error!("Error uploading panic to server: {}", response.status());
+                        let panic = serde_json::from_str(&panic_file_content)
+                            .ok()
+                            .or_else(|| {
+                                panic_file_content
+                                    .lines()
+                                    .next()
+                                    .and_then(|line| serde_json::from_str(line).ok())
+                            })
+                            .unwrap_or_else(|| {
+                                log::error!(
+                                    "failed to deserialize panic file {:?}",
+                                    panic_file_content
+                                );
+                                None
+                            });
+
+                        if let Some(panic) = panic {
+                            let body = serde_json::to_string(&PanicRequest {
+                                panic,
+                                token: ZED_SECRET_CLIENT_TOKEN.into(),
+                            })
+                            .unwrap();
+
+                            let request = Request::post(&panic_report_url)
+                                .redirect_policy(isahc::config::RedirectPolicy::Follow)
+                                .header("Content-Type", "application/json")
+                                .body(body.into())?;
+                            let response =
+                                http.send(request).await.context("error sending panic")?;
+                            if !response.status().is_success() {
+                                log::error!(
+                                    "Error uploading panic to server: {}",
+                                    response.status()
+                                );
+                            }
                         }
                     }
 
@@ -896,6 +891,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
         ("Go to file", &file_finder::Toggle),
         ("Open command palette", &command_palette::Toggle),
         ("Open recent projects", &recent_projects::OpenRecent),
-        ("Change your settings", &zed::OpenSettings),
+        ("Change your settings", &zed_actions::OpenSettings),
     ]
 }

crates/zed/src/zed.rs πŸ”—

@@ -20,7 +20,6 @@ use feedback::{
 };
 use futures::{channel::mpsc, StreamExt};
 use gpui::{
-    actions,
     anyhow::{self, Result},
     geometry::vector::vec2f,
     impl_actions,
@@ -50,6 +49,7 @@ use workspace::{
     notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
     NewWindow, Workspace, WorkspaceSettings,
 };
+use zed_actions::*;
 
 #[derive(Deserialize, Clone, PartialEq)]
 pub struct OpenBrowser {
@@ -58,33 +58,6 @@ pub struct OpenBrowser {
 
 impl_actions!(zed, [OpenBrowser]);
 
-actions!(
-    zed,
-    [
-        About,
-        Hide,
-        HideOthers,
-        ShowAll,
-        Minimize,
-        Zoom,
-        ToggleFullScreen,
-        Quit,
-        DebugElements,
-        OpenLog,
-        OpenLicenses,
-        OpenTelemetryLog,
-        OpenKeymap,
-        OpenSettings,
-        OpenLocalSettings,
-        OpenDefaultSettings,
-        OpenDefaultKeymap,
-        IncreaseBufferFontSize,
-        DecreaseBufferFontSize,
-        ResetBufferFontSize,
-        ResetDatabase,
-    ]
-);
-
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
     cx.add_action(about);
     cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
@@ -361,15 +334,15 @@ pub fn initialize_workspace(
 
         let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
         let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
-        let assistant_panel = if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
-            None
-        } else {
-            Some(AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?)
-        };
-        let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
+        let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
+        let (project_panel, terminal_panel, assistant_panel) =
+            futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
             workspace.add_panel(project_panel, cx);
+            workspace.add_panel(terminal_panel, cx);
+            workspace.add_panel(assistant_panel, cx);
+
             if !was_deserialized
                 && workspace
                     .project()
@@ -383,11 +356,7 @@ pub fn initialize_workspace(
             {
                 workspace.toggle_dock(project_panel_position, cx);
             }
-
-            workspace.add_panel(terminal_panel, cx);
-            if let Some(assistant_panel) = assistant_panel {
-                workspace.add_panel(assistant_panel, cx);
-            }
+            cx.focus_self();
         })?;
         Ok(())
     })
@@ -739,8 +708,8 @@ mod tests {
     use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
     use fs::{FakeFs, Fs};
     use gpui::{
-        elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource,
-        Element, Entity, TestAppContext, View, ViewHandle,
+        actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext,
+        AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
     };
     use language::LanguageRegistry;
     use node_runtime::NodeRuntime;
@@ -2105,6 +2074,167 @@ mod tests {
             line!(),
         );
 
+        #[track_caller]
+        fn assert_key_bindings_for<'a>(
+            window_id: usize,
+            cx: &TestAppContext,
+            actions: Vec<(&'static str, &'a dyn Action)>,
+            line: u32,
+        ) {
+            for (key, action) in actions {
+                // assert that...
+                assert!(
+                    cx.available_actions(window_id, 0)
+                        .into_iter()
+                        .any(|(_, bound_action, b)| {
+                            // action names match...
+                            bound_action.name() == action.name()
+                        && bound_action.namespace() == action.namespace()
+                        // and key strokes contain the given key
+                        && b.iter()
+                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+                        }),
+                    "On {} Failed to find {} with key binding {}",
+                    line,
+                    action.name(),
+                    key
+                );
+            }
+        }
+    }
+
+    #[gpui::test]
+    async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+        struct TestView;
+
+        impl Entity for TestView {
+            type Event = ();
+        }
+
+        impl View for TestView {
+            fn ui_name() -> &'static str {
+                "TestView"
+            }
+
+            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+                Empty::new().into_any()
+            }
+        }
+
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        actions!(test, [A, B]);
+        // From the Atom keymap
+        actions!(workspace, [ActivatePreviousPane]);
+        // From the JetBrains keymap
+        actions!(pane, [ActivatePrevItem]);
+
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "Atom"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": "test::A"
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init(Assets, cx);
+            welcome::init(cx);
+
+            cx.add_global_action(|_: &A, _cx| {});
+            cx.add_global_action(|_: &B, _cx| {});
+            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+            let settings_rx = watch_config_file(
+                executor.clone(),
+                fs.clone(),
+                PathBuf::from("/settings.json"),
+            );
+            let keymap_rx =
+                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+            handle_keymap_file_changes(keymap_rx, cx);
+            handle_settings_file_changes(settings_rx, cx);
+        });
+
+        cx.foreground().run_until_parked();
+
+        let (window_id, _view) = cx.add_window(|_| TestView);
+
+        // Test loading the keymap base at all
+        assert_key_bindings_for(
+            window_id,
+            cx,
+            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test disabling the key binding for the base keymap
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": null
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
+
+        // Test modifying the base, while retaining the users keymap
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "JetBrains"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
+
+        #[track_caller]
         fn assert_key_bindings_for<'a>(
             window_id: usize,
             cx: &TestAppContext,
@@ -2175,7 +2305,7 @@ mod tests {
         languages.set_executor(cx.background().clone());
         let languages = Arc::new(languages);
         let http = FakeHttpClient::with_404_response();
-        let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
+        let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
         languages::init(languages.clone(), node_runtime);
         for name in languages.language_names() {
             languages.language_for_name(&name);
@@ -2191,6 +2321,7 @@ mod tests {
             state.initialize_workspace = initialize_workspace;
             state.build_window_options = build_window_options;
             theme::init((), cx);
+            audio::init((), cx);
             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);

docs/backend-development.md πŸ”—

@@ -0,0 +1,52 @@
+[β¬… Back to Index](./index.md)
+
+# Developing Zed's Backend
+
+Zed's backend consists of the following components:
+
+- The Zed.dev web site
+  - implemented in the [`zed.dev`](https://github.com/zed-industries/zed.dev) repository
+  - hosted on [Vercel](https://vercel.com/zed-industries/zed-dev).
+- The Zed Collaboration server
+  - implemented in the [`crates/collab`](https://github.com/zed-industries/zed/tree/main/crates/collab) directory of the main `zed` repository
+  - hosted on [DigitalOcean](https://cloud.digitalocean.com/projects/6c680a82-9d3b-4f1a-91e5-63a6ca4a8611), using Kubernetes
+- The Zed Postgres database
+  - defined via migrations in the [`crates/collab/migrations`](https://github.com/zed-industries/zed/tree/main/crates/collab/migrations) directory
+  - hosted on DigitalOcean
+
+---
+
+## Local Development
+
+Here's some things you need to develop backend code locally.
+
+### Dependencies
+
+- **Postgres** - download [Postgres.app](https://postgresapp.com).
+
+### Setup
+
+1. Check out the `zed` and `zed.dev` repositories into a common parent directory
+2. Set the `GITHUB_TOKEN` environment variable to one of your GitHub personal access tokens (PATs).
+
+   - You can create a PAT [here](https://github.com/settings/tokens).
+   - You may want to add something like this to your `~/.zshrc`:
+
+     ```
+     export GITHUB_TOKEN=<the personal access token>
+     ```
+
+3. In the `zed.dev` directory, run `npm install` to install dependencies.
+4. In the `zed directory`, run `script/bootstrap` to set up the database
+5. In the `zed directory`, run `foreman start` to start both servers
+
+---
+
+## Production Debugging
+
+### Datadog
+
+Zed uses Datadog to collect metrics and logs from backend services. The Zed organization lives within Datadog's _US5_ [site](https://docs.datadoghq.com/getting_started/site/), so it can be accessed at [us5.datadoghq.com](https://us5.datadoghq.com). Useful things to look at in Datadog:
+
+- The [Logs](https://us5.datadoghq.com/logs) page shows logs from Zed.dev and the Collab server, and the internals of Zed's Kubernetes cluster.
+- The [collab metrics dashboard](https://us5.datadoghq.com/dashboard/y2d-gxz-h4h/collab?from_ts=1660517946462&to_ts=1660604346462&live=true) shows metrics about the running collab server

docs/building-zed.md πŸ”—

@@ -0,0 +1,79 @@
+[β¬… Back to Index](./index.md)
+
+# Building Zed
+
+How to build Zed from source for the first time.
+
+## Process
+
+Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be.
+
+1. Install the [GitHub CLI](https://cli.github.com/):
+   - `brew install gh`
+1. Clone the `zed` repo
+   - `gh repo clone zed-industries/zed`
+1. Install Xcode from the macOS App Store
+1. Install [Postgres](https://postgresapp.com)
+1. Install rust/rustup
+   - `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
+1. Install the wasm toolchain
+   - `rustup target add wasm32-wasi`
+1. Generate an GitHub API Key
+   - Go to https://github.com/settings/tokens and Generate new token
+   - GitHub currently provides two kinds of tokens:
+     - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected
+       Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories
+     - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos
+   - Keep the token in the browser tab/editor for the next two steps
+1. Open Postgres.app
+1. From `./path/to/zed/`:
+   - Run:
+     - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap`
+     - Replace `{yourGithubAPIToken}` with the API token you generated above.
+   - Consider removing the token (if it's fine for you to crecreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault).
+   - If you get:
+     - ```bash
+       Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)!
+       Please create a new installation in /opt/homebrew using one of the
+       "Alternative Installs" from:
+       https://docs.brew.sh/Installation
+       ```
+     - In that case try:
+       - `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
+   - If Homebrew is not in your PATH:
+     - Replace `{username}` with your home folder name (usually your login name)
+     - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile`
+     - `eval "$(/opt/homebrew/bin/brew shellenv)"`
+1. To run the Zed app:
+    - If you are working on zed:
+      - `cargo run`
+    - If you are just using the latest version, but not working on zed:
+      - `cargo run --release`
+    - If you need to run the collaboration server locally:
+      - `script/zed-with-local-servers`
+
+## Troubleshooting
+
+### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`
+
+- Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
+
+### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
+
+### Seeding errors during `script/bootstrap` runs
+
+```
+seeding database...
+thread 'main' panicked at 'failed to deserialize github user from 'https://api.github.com/orgs/zed-industries/teams/staff/members': reqwest::Error { kind: Decode, source: Error("invalid type: map, expected a sequence", line: 1, column: 0) }', crates/collab/src/bin/seed.rs:111:10
+```
+
+Wrong permissions for `GITHUB_TOKEN` token used, the token needs to be able to read from private repos.
+For Classic GitHub Tokens, that required OAuth scope `repo` (seacrh the scope name above for more details)
+
+Same command
+
+`sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`
+
+### If you experience errors that mention some dependency is using unstable features
+
+Try `cargo clean` and `cargo build`

docs/company-and-vision.md πŸ”—

@@ -0,0 +1,34 @@
+[β¬… Back to Index](./index.md)
+
+# Company & Vision
+
+## Vision
+
+Our goal is to make Zed the primary tool software teams use to collaborate.
+
+To do this, Zed will...
+
+* Make collaboration a first-class feature of the code authoring environment.
+* Enable text-based conversations about any piece of text, independent of whether/when it was committed to version control.
+* Make it smooth to edit and discuss code with teammates in real time.
+* Make it easy to recall past conversations any area of the code.
+
+We believe the best way to make collaboration amazing is to build it into a new editor rather than retrofitting an existing editor. This means that in order for a team to adopt Zed for collaboration, each team member will need to adopt it as their editor as well.
+
+For this reason, we need to deliver a clearly superior experience as a single-user code editor in addition to being an excellent collaboration tool. This will take time, but we believe the dominance of VS Code demonstrates that it's possible for a single tool to capture substantial market share. We can proceed incrementally, capturing one team at a time and gradually transitioning conversations away from GitHub.
+
+## Zed Values
+
+Everyone wants to work quickly and have a lot of users. What are we unwilling to sacrifice in pursuit of those goals?
+
+- **Performance.** Speed is core to our brand and value proposition. It's important that we consistently deliver a response in less than 8ms on modern hardware for fine-grained actions. Coarse-grained actions should render feedback within 50ms. We consider the performance goals of the product at all times, and take the time to ensure our code meets them with reasonable usage. Once we have met our goals, we assess the impact vs effort of further performance investment and know when to say when. We measure our performance in the field and make an effort to maintain or improve real-world performance and promptly address regressions.
+
+- **Craftsmanship.** Zed is a premium product, and we put care into design and user experience. We can always cut scope, but what we do ship should be quality. Incomplete is okay, so long as we're executing on a coherent subset well. Half-baked, unintuitive, or broken is not okay.
+
+- **Shipping.** Knowledge matters only in as much as it drives results. We're here to build a real product in the real world. We care a lot about the experience of developing Zed, but we care about the user's experience more.
+
+- **Code quality.** This enables craftsmanship. Nobody is creative in a trash heap, and we're willing to dedicate time to keep our codebase clean. If we're spending no time refactoring, we are likely underinvesting. When we realize a design flaw, we assess its centrality to the rest of the system and consider budgeting time to address it. If we're spending all of our time refactoring, we are likely either overinvesting or paying off debt from past underinvestment. It's up to each engineer to allocate a reasonable refactoring budget. We shouldn't be navel gazing, but we also shouldn't be afraid to invest.
+
+- **Pairing.** Zed depends on regular pair programming to promote cohesion on our remote team. We believe pairing is a powerful substitute for beuracratic management, excessive documentation, and tedious code review. Nobody has to pair all day, every day, but everyone is responsible for pairing at least 2 hours a week with a variety of other engineers. If anyone wants to pair all day every day, that is explicitly endorsed and credited. If pairing temporarily reduces our throughput due to working on one thing instead of two, we trust that it will pay for itself in the long term by increasing our velocity and allowing us to more effectively grow our team.
+
+- **Long-term thinking.** The Zed vision began several years ago, and we expect Zed to be around many years from today. We must always be mindful to avoid overengineering for the future, but we should also keep the long-term in mind. Are we building a system our future selves would want to work on in 5 years?

docs/design-tools.md πŸ”—

@@ -0,0 +1,74 @@
+[β¬… Back to Index](./index.md)
+
+# Design Tools & Links
+
+Generally useful tools and resources for design.
+
+## General
+
+[Names of Signs & Symbols](https://www.prepressure.com/fonts/basics/character-names#curlybrackets)
+
+[The Noun Project](https://thenounproject.com/) - Icons for everything, attempts to describe all of human language visually.
+
+[SVG Repo](https://www.svgrepo.com/) - Open-licensed SVG Vector and Icons
+
+[Font Awsesome](https://fontawesome.com/) - High quality icons, has been around for many years.
+
+---
+
+## Color
+
+[Opacity/Transparency Hex Values](https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4)
+
+[Color Ramp Generator](https://lyft-colorbox.herokuapp.com)
+
+[Designing a Comprehensive Color System
+](https://www.rethinkhq.com/videos/designing-a-comprehensive-color-system-for-lyft) - [Linda Dong](https://twitter.com/lindadong)
+
+---
+
+## Figma & Plugins
+
+[Figma Plugins for Designers](https://www.uiprep.com/blog/21-best-figma-plugins-for-designers-in-2021)
+
+[Icon Resizer](https://www.figma.com/community/plugin/739117729229117975/Icon-Resizer)
+
+[Code Syntax Highlighter](https://www.figma.com/community/plugin/938793197191698232/Code-Syntax-Highlighter)
+
+[Proportional Scale](https://www.figma.com/community/plugin/756895186298946525/Proportional-Scale)
+
+[LilGrid](https://www.figma.com/community/plugin/795397421598343178/LilGrid)
+
+Organize your selection into a grid.
+
+[Automator](https://www.figma.com/community/plugin/1005114571859948695/Automator)
+
+Build photoshop-style batch actions to automate things.
+
+[Figma Tokens](https://www.figma.com/community/plugin/843461159747178978/Figma-Tokens)
+
+Use tokens in Figma and generate JSON from them.
+
+---
+
+## Design Systems
+
+### Naming
+
+[Naming Design Tokens](https://uxdesign.cc/naming-design-tokens-9454818ed7cb)
+
+### Storybook
+
+[Collaboration with design tokens and storybook](https://zure.com/blog/collaboration-with-design-tokens-and-storybook/)
+
+### Example DS Documentation
+
+[Tailwind CSS Documentation](https://tailwindcss.com/docs/container)
+
+[Material Design Docs](https://material.io/design/color/the-color-system.html#color-usage-and-palettes)
+
+[Carbon Design System Docs](https://www.carbondesignsystem.com)
+
+[Adobe Spectrum](https://spectrum.adobe.com/)
+  - Great documentation, like [Color System](https://spectrum.adobe.com/page/color-system/) and [Design Tokens](https://spectrum.adobe.com/page/design-tokens/).
+  - A good place to start if thinking about building a design system.

docs/index.md πŸ”—

@@ -0,0 +1,14 @@
+[β¬… Back to Index](./index.md)
+
+# Welcome to Zed
+
+Welcome! These internal docs are a work in progress. You can contribute to them by submitting a PR directly!
+
+## Contents
+
+- [The Company](./company-and-vision.md)
+- [Tools We Use](./tools.md)
+- [Building Zed](./building-zed.md)
+- [Release Process](./release-process.md)
+- [Backend Development](./backend-development.md)
+- [Design Tools & Links](./design-tools.md)

docs/local-collaboration.md πŸ”—

@@ -0,0 +1,22 @@
+# Local Collaboration
+
+## Setting up the local collaboration server
+
+### Setting up for the first time?
+
+1. Make sure you have livekit installed (`brew install livekit`)
+1. Install [Postgres](https://postgresapp.com) and run it.
+1. Then, from the root of the repo, run `script/bootstrap`.
+
+### Have a db that is out of date? / Need to migrate?
+
+1. Make sure you have livekit installed (`brew install livekit`)
+1. Try `cd crates/collab && cargo run -- migrate` from the root of the repo.
+1. Run `script/seed-db`
+
+## Testing collab locally
+
+1. Run `foreman start` from the root of the repo.
+1. In another terminal run `script/start-local-collaboration`.
+1. Two copies of Zed will open. Add yourself as a contact in the one that is not you.
+1. Start a collaboration session as normal with any open project.

docs/release-process.md πŸ”—

@@ -0,0 +1,96 @@
+[β¬… Back to Index](./index.md)
+
+# Zed's Release Process
+
+The process to create and ship a Zed release
+
+## Overview
+
+### Release Channels
+
+Users of Zed can choose between two _release channels_ - 'Stable' and 'Preview'. Most people use Stable, but Preview exists so that the Zed team and other early-adopters can test new features before they are released to our general user-base.
+
+### Weekly (Minor) Releases
+
+We normally publish new releases of Zed on Wednesdays, for both the Stable and Preview channels. For each of these releases, we bump Zed's _minor_ version number.
+
+For the Preview channel, we build the new release based on what's on the `main` branch. For the Stable channel, we build the new release based on the last Preview release.
+
+### Hotfix (Patch) Releases
+
+When we find a _regression_ in Zed (a bug that wasn't present in an earlier version), or find a significant bug in a newly-released feature, we typically publish a hotfix release. For these releases, we bump Zed's _patch_ version number.
+
+### Server Deployments
+
+Often, changes in the Zed app require corresponding changes in the `collab` server. At the currente stage of our copmany, we don't attempt to keep our server backwards-compatible with older versions of the app. Instead, when making a change, we simply bump Zed's _protocol version_ number (in the `rpc` crate), which causes the server to recognize that it isn't compatible with earlier versions of the Zed app.
+
+This means that when releasing a new version of Zed that has changes to the RPC protocol, we need to deploy a new version of the `collab` server at the same time.
+
+## Instructions
+
+### Publishing a Minor Release
+
+1. Announce your intent to publish a new version in Discord. This gives other people a chance to raise concerns or postpone the release if they want to get something merged before publishing a new version.
+1. Open your terminal and `cd` into your local copy of Zed. Checkout `main` and perform a `git pull` to ensure you have the latest version.
+1. Run the following command, which will update two git branches and two git tags (one for each release channel):
+
+   ```
+   script/bump-zed-minor-versions
+   ```
+
+1. The script will make local changes only, and print out a shell command that you can use to push all of these branches and tags.
+1. Pushing the two new tags will trigger two CI builds that, when finished, will create two draft releases (Stable and Preview) containing `Zed.dmg` files.
+1. Now you need to write the release notes for the Stable and Preview releases. For the Stable release, you can just copy the release notes from the last week's Preview release, plus any hotfixes that were published on the Preview channel since then. Some of the hotfixes may not be relevant for the Stable release notes, if they were fixing bugs that were only present in Preview.
+1. For the Preview release, you can retrieve the list of changes by running this command (make sure you have at least `Node 18` installed):
+
+   ```
+   GITHUB_ACCESS_TOKEN=your_access_token script/get-preview-channel-changes
+   ```
+
+1. The script will list all the merged pull requests and you can use it as a reference to write the release notes. If there were protocol changes, it will also emit a warning.
+1. Once CI creates the draft releases, add each release's notes and save the drafts.
+1. If there have been server-side changes since the last release, you'll need to re-deploy the `collab` server. See below.
+1. Before publishing, download the Zed.dmg and smoke test it to ensure everything looks good.
+
+### Publishing a Patch Release
+
+1. Announce your intent to publish a new patch version in Discord.
+1. Open your terminal and `cd` into your local copy of Zed. Check out the branch corresponding to the release channel where the fix is needed. For example, if the fix is for a bug in Stable, and the current stable version is `0.63.0`, then checkout the branch `v0.63.x`. Run `git pull` to ensure your branch is up-to-date.
+1. Find the merge commit where your bug-fix landed on `main`. You can browse the merged pull requests on main by running `git log main --grep Merge`.
+1. Cherry-pick those commits onto the current release branch:
+
+   ```
+   git cherry-pick -m1 <THE-COMMIT-SHA>
+   ```
+
+1. Run the following command, which will bump the version of Zed and create a new tag:
+
+   ```
+   script/bump-zed-patch-version
+   ```
+
+1. The script will make local changes only, and print out a shell command that you can use to push all the branch and tag.
+1. Pushing the new tag will trigger a CI build that, when finished, will create a draft release containing a `Zed.dmg` file.
+1. Once the draft release is created, fill in the release notes based on the bugfixes that you cherry-picked.
+1. If any of the bug-fixes require server-side changes, you'll need to re-deploy the `collab` server. See below.
+1. Before publishing, download the Zed.dmg and smoke test it to ensure everything looks good.
+1. Clicking publish on the release will cause old Zed instances to auto-update and the Zed.dev releases page to re-build and display the new release.
+
+### Deploying the Server
+
+1. Deploying the server is a two-step process that begins with pushing a tag. 1. Check out the commit you'd like to deploy. Often it will be the head of `main`, but could be on any branch.
+1. Run the following script, which will bump the version of the `collab` crate and create a new tag. The script takes an argument `minor` or `patch`, to indicate how to increment the version. If you're releasing new features, use `minor`. If it's just a bugfix, use `patch`
+
+    ```
+    script/bump-collab-version patch
+    ```
+
+1. This script will make local changes only, and print out a shell command that you can use to push the branch and tag.
+1. Pushing the new tag will trigger a CI build that, when finished will upload a new versioned docker image to the DigitalOcean docker registry.
+1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`).
+
+   ```
+   script/deploy preview 0.10.1
+   ```
+
+1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server.

docs/tools.md πŸ”—

@@ -0,0 +1,82 @@
+[β¬… Back to Index](./index.md)
+
+# Tools
+
+Tools to get started at Zed. Work in progress, submit a PR to add any missing tools here!
+
+---
+
+## Everyday Tools
+
+### Calendar
+
+To gain access to company calendar, visit [this link](https://calendar.google.com/calendar/u/0/r?cid=Y18xOGdzcGE1aG5wdHJocGRoNWtlb2tlbWxzc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t).
+
+If you would like the company calendar to be synced with a calendar application (Apple Calendar, etc.):
+
+- Add your company account (i.e `joseph@zed.dev`) to your calendar application
+- Visit [this link](https://calendar.google.com/calendar/u/0/syncselect), check `Zed Industries (Read Only)` under `Shared Calendars`, and save it.
+
+### 1Password
+
+We have a shared company 1Password with all of our credentials. To gain access:
+
+1. Go to [zed-industries.1password.com](https://zed-industries.1password.com).
+1. Sign in with your `@zed.dev` email address.
+1. Make your account and let an admin know you've signed up.
+1. Once they approve your sign up, you'll have access to all of the company credentials.
+
+### Slack
+
+Have a team member add you to the [Zed Industries](https://zed-industries.slack.com/) slack.
+
+### Discord
+
+We have a discord community. You can use [this link](https://discord.gg/SSD9eJrn6s) to join. **!Don't share this link, this is specifically for team memebers!**
+
+Once you have joined the community, let a team member know and we can add your correct role.
+
+---
+
+## Engineering
+
+### Github
+
+For now, all the Zed source code lives on [Github](https://github.com/zed-industries). A founder will need to add you to the team and set up the appropriate permissions.
+
+Useful repos:
+- [zed-industries/zed](https://github.com/zed-industries/zed) - Zed source
+- [zed-industries/zed.dev](https://github.com/zed-industries/zed.dev) - Zed.dev site and collab API
+- [zed-industries/docs](https://github.com/zed-industries/docs) - Zed public docs
+- [zed-industries/community](https://github.com/zed-industries/community) - Zed community feedback & discussion
+
+### Vercel
+
+We use Vercel for all of our web deployments and some backend things. If you sign up with your `@zed.dev` email you should be invited to join the team automatically. If not, ask a founder to invite you to the Vercel team.
+
+### Environment Variables
+
+You can get access to many of our shared enviroment variables through 1Password and Vercel. For one password search the value you are looking for, or sort by passwords or API credentials.
+
+For Vercel, go to `settings` -> `Environment Variables` (either on the entire org, or on a specific project depending on where it is shared.) For a given Vercel project if you have their CLI installed you can use `vercel pull` or `vercel env` to pull values down directly. More on those in their [CLI docs](https://vercel.com/docs/cli/env).
+
+---
+
+## Design
+
+### Figma
+
+We use Figma for all of our design work. To gain access:
+
+1. Use [this link](https://www.figma.com/team_invite/redeem/Xg4RcNXHhwP5netIvVBmKQ) to join the Figma team.
+1. You should now have access to all of the company files.
+1. You should go to the team page and "favorite" (star) any relevant projects so they show up in your sidebar.
+1. Download the [Figma app](https://www.figma.com/downloads/) for easier access on desktop.
+
+### Campsite
+
+We use Campsite to review and discuss designs. To gain access:
+
+1. Download the [Campsite app](https://campsite.design/desktop/download).
+1. Open it and sign in with your `@zed.dev` email address.
+1. You can access our company campsite directly: [app.campsite.design/zed](https://app.campsite.design/zed)

docs/zed/syntax-highlighting.md πŸ”—

@@ -0,0 +1,79 @@
+# Syntax Highlighting in Zed
+
+This doc is a work in progress!
+
+## Defining syntax highlighting rules
+
+We use tree-sitter queries to match certian properties to highlight.
+
+### Simple Example:
+
+```scheme
+(property_identifier) @property
+```
+
+```ts
+const font: FontFamily = {
+    weight: "normal",
+    underline: false,
+    italic: false,
+}
+```
+
+Match a property identifier and highlight it using the identifier `@property`. In the above example, `weight`, `underline`, and `italic` would be highlighted.
+
+### Complex example:
+
+```scheme
+(_
+  return_type: (type_annotation
+    [
+      (type_identifier) @type.return
+      (generic_type
+          name: (type_identifier) @type.return)
+    ]))
+```
+
+```ts
+function buildDefaultSyntax(colorScheme: Theme): Partial<Syntax> {
+    // ...
+}
+```
+
+Match a function return type, and highlight the type using the identifier `@type.return`. In the above example, `Partial` would be highlighted.
+
+### Example - Typescript
+
+Here is an example portion of our `highlights.scm` for TypeScript:
+
+```scheme
+; crates/zed/src/languages/typescript/highlights.scm
+
+; Variables
+
+(identifier) @variable
+
+; Properties
+
+(property_identifier) @property
+
+; Function and method calls
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (member_expression
+    property: (property_identifier) @function.method))
+
+; Function and method definitions
+
+(function
+  name: (identifier) @function)
+(function_declaration
+  name: (identifier) @function)
+(method_definition
+  name: (property_identifier) @function.method)
+
+; ...
+```

script/build-theme-types πŸ”—

@@ -0,0 +1,10 @@
+#!/bin/bash
+
+echo "running xtask"
+(cd crates/theme && cargo xtask build-theme-types)
+
+echo "updating theme packages"
+(cd styles && npm install)
+
+echo "building theme types"
+(cd styles && npm run build-types)

script/start-local-collaboration πŸ”—

@@ -54,5 +54,5 @@ sleep 0.5
 # Start the two Zed child processes. Open the given paths with the first instance.
 trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
 ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
-ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
+SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
 wait

styles/.eslintrc.js πŸ”—

@@ -0,0 +1,33 @@
+module.exports = {
+    env: {
+        node: true,
+    },
+    extends: [
+        "eslint:recommended",
+        "plugin:@typescript-eslint/recommended",
+        "plugin:import/typescript",
+    ],
+    parser: "@typescript-eslint/parser",
+    parserOptions: {
+        ecmaVersion: "latest",
+        sourceType: "module",
+    },
+    plugins: ["@typescript-eslint", "import"],
+    globals: {
+        module: true,
+    },
+    settings: {
+        "import/parsers": {
+            "@typescript-eslint/parser": [".ts"],
+        },
+        "import/resolver": {
+            typescript: true,
+            node: true,
+        },
+        "import/extensions": [".ts"],
+    },
+    rules: {
+        "linebreak-style": ["error", "unix"],
+        semi: ["error", "never"],
+    },
+}

styles/.prettierrc πŸ”—

@@ -0,0 +1,6 @@
+{
+    "semi": false,
+    "printWidth": 80,
+    "htmlWhitespaceSensitivity": "strict",
+    "tabWidth": 4
+}

styles/.zed/settings.json πŸ”—

@@ -0,0 +1,20 @@
+// Folder-specific settings
+//
+// For a full list of overridable settings, and general information on folder-specific settings,
+// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
+{
+    "languages": {
+        "TypeScript": {
+            "tab_size": 4
+        },
+        "TSX": {
+            "tab_size": 4
+        },
+        "JavaScript": {
+            "tab_size": 4
+        },
+        "JSON": {
+            "tab_size": 4
+        }
+    }
+}

styles/package-lock.json πŸ”—

@@ -1,7 +1,7 @@
 {
     "name": "styles",
     "version": "1.0.0",
-    "lockfileVersion": 2,
+    "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
@@ -12,15 +12,67 @@
                 "@tokens-studio/types": "^0.2.3",
                 "@types/chroma-js": "^2.4.0",
                 "@types/node": "^18.14.1",
+                "@typescript-eslint/eslint-plugin": "^5.60.1",
+                "@typescript-eslint/parser": "^5.60.1",
+                "@vitest/coverage-v8": "^0.32.0",
                 "ayu": "^8.0.1",
-                "bezier-easing": "^2.1.0",
-                "case-anything": "^2.1.10",
                 "chroma-js": "^2.4.2",
                 "deepmerge": "^4.3.0",
+                "eslint": "^8.43.0",
+                "eslint-import-resolver-typescript": "^3.5.5",
+                "eslint-plugin-import": "^2.27.5",
+                "json-schema-to-typescript": "^13.0.2",
                 "toml": "^3.0.0",
-                "ts-node": "^10.9.1"
+                "ts-deepmerge": "^6.0.3",
+                "ts-node": "^10.9.1",
+                "typescript": "^5.1.5",
+                "utility-types": "^3.10.0",
+                "vitest": "^0.32.0",
+                "zustand": "^4.3.8"
             }
         },
+        "node_modules/@aashutoshrathi/word-wrap": {
+            "version": "1.2.6",
+            "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+            "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/@ampproject/remapping": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
+            "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+            "dependencies": {
+                "@jridgewell/gen-mapping": "^0.3.0",
+                "@jridgewell/trace-mapping": "^0.3.9"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/@bcherny/json-schema-ref-parser": {
+            "version": "10.0.5-fork",
+            "resolved": "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz",
+            "integrity": "sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==",
+            "dependencies": {
+                "@jsdevtools/ono": "^7.1.3",
+                "@types/json-schema": "^7.0.6",
+                "call-me-maybe": "^1.0.1",
+                "js-yaml": "^4.1.0"
+            },
+            "engines": {
+                "node": ">= 16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/philsturgeon"
+            }
+        },
+        "node_modules/@bcoe/v8-coverage": {
+            "version": "0.2.3",
+            "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+            "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="
+        },
         "node_modules/@cspotcode/source-map-support": {
             "version": "0.8.1",
             "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -32,6 +84,124 @@
                 "node": ">=12"
             }
         },
+        "node_modules/@esbuild/darwin-arm64": {
+            "version": "0.17.19",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+            "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+            "cpu": [
+                "arm64"
+            ],
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@eslint-community/eslint-utils": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+            "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+            "dependencies": {
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+            }
+        },
+        "node_modules/@eslint-community/regexpp": {
+            "version": "4.5.1",
+            "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+            "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+            "engines": {
+                "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+            }
+        },
+        "node_modules/@eslint/eslintrc": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+            "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+            "dependencies": {
+                "ajv": "^6.12.4",
+                "debug": "^4.3.2",
+                "espree": "^9.5.2",
+                "globals": "^13.19.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.2.1",
+                "js-yaml": "^4.1.0",
+                "minimatch": "^3.1.2",
+                "strip-json-comments": "^3.1.1"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/@eslint/js": {
+            "version": "8.43.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
+            "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            }
+        },
+        "node_modules/@humanwhocodes/config-array": {
+            "version": "0.11.10",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+            "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+            "dependencies": {
+                "@humanwhocodes/object-schema": "^1.2.1",
+                "debug": "^4.1.1",
+                "minimatch": "^3.0.5"
+            },
+            "engines": {
+                "node": ">=10.10.0"
+            }
+        },
+        "node_modules/@humanwhocodes/module-importer": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+            "engines": {
+                "node": ">=12.22"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/nzakas"
+            }
+        },
+        "node_modules/@humanwhocodes/object-schema": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+            "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+        },
+        "node_modules/@istanbuljs/schema": {
+            "version": "0.1.3",
+            "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+            "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@jridgewell/gen-mapping": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+            "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+            "dependencies": {
+                "@jridgewell/set-array": "^1.0.1",
+                "@jridgewell/sourcemap-codec": "^1.4.10",
+                "@jridgewell/trace-mapping": "^0.3.9"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
         "node_modules/@jridgewell/resolve-uri": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
@@ -40,6 +210,14 @@
                 "node": ">=6.0.0"
             }
         },
+        "node_modules/@jridgewell/set-array": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+            "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
         "node_modules/@jridgewell/sourcemap-codec": {
             "version": "1.4.14",
             "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
@@ -54,6 +232,67 @@
                 "@jridgewell/sourcemap-codec": "^1.4.10"
             }
         },
+        "node_modules/@jsdevtools/ono": {
+            "version": "7.1.3",
+            "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+            "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
+        },
+        "node_modules/@nodelib/fs.scandir": {
+            "version": "2.1.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+            "dependencies": {
+                "@nodelib/fs.stat": "2.0.5",
+                "run-parallel": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.stat": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@nodelib/fs.walk": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+            "dependencies": {
+                "@nodelib/fs.scandir": "2.1.5",
+                "fastq": "^1.6.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/@pkgr/utils": {
+            "version": "2.4.1",
+            "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz",
+            "integrity": "sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w==",
+            "dependencies": {
+                "cross-spawn": "^7.0.3",
+                "fast-glob": "^3.2.12",
+                "is-glob": "^4.0.3",
+                "open": "^9.1.0",
+                "picocolors": "^1.0.0",
+                "tslib": "^2.5.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/unts"
+            }
+        },
+        "node_modules/@pkgr/utils/node_modules/tslib": {
+            "version": "2.6.0",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
+            "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
+        },
         "node_modules/@tokens-studio/types": {
             "version": "0.2.3",
             "resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.2.3.tgz",
@@ -79,344 +318,4055 @@
             "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
             "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
         },
+        "node_modules/@types/chai": {
+            "version": "4.3.5",
+            "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz",
+            "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng=="
+        },
+        "node_modules/@types/chai-subset": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz",
+            "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==",
+            "dependencies": {
+                "@types/chai": "*"
+            }
+        },
         "node_modules/@types/chroma-js": {
             "version": "2.4.0",
             "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
             "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
         },
+        "node_modules/@types/glob": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
+            "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
+            "dependencies": {
+                "@types/minimatch": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/istanbul-lib-coverage": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+            "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g=="
+        },
+        "node_modules/@types/json-schema": {
+            "version": "7.0.12",
+            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
+            "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA=="
+        },
+        "node_modules/@types/json5": {
+            "version": "0.0.29",
+            "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+            "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
+        },
+        "node_modules/@types/lodash": {
+            "version": "4.14.195",
+            "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
+            "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg=="
+        },
+        "node_modules/@types/minimatch": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+            "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="
+        },
         "node_modules/@types/node": {
             "version": "18.14.1",
             "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
             "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
         },
-        "node_modules/acorn": {
-            "version": "8.8.2",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
-            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
-            "bin": {
-                "acorn": "bin/acorn"
+        "node_modules/@types/prettier": {
+            "version": "2.7.3",
+            "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
+            "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
+        },
+        "node_modules/@types/semver": {
+            "version": "7.5.0",
+            "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+            "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw=="
+        },
+        "node_modules/@typescript-eslint/eslint-plugin": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz",
+            "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==",
+            "dependencies": {
+                "@eslint-community/regexpp": "^4.4.0",
+                "@typescript-eslint/scope-manager": "5.60.1",
+                "@typescript-eslint/type-utils": "5.60.1",
+                "@typescript-eslint/utils": "5.60.1",
+                "debug": "^4.3.4",
+                "grapheme-splitter": "^1.0.4",
+                "ignore": "^5.2.0",
+                "natural-compare-lite": "^1.4.0",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
             },
             "engines": {
-                "node": ">=0.4.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "@typescript-eslint/parser": "^5.0.0",
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/acorn-walk": {
-            "version": "8.2.0",
-            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
-            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+        "node_modules/@typescript-eslint/parser": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
+            "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==",
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "5.60.1",
+                "@typescript-eslint/types": "5.60.1",
+                "@typescript-eslint/typescript-estree": "5.60.1",
+                "debug": "^4.3.4"
+            },
             "engines": {
-                "node": ">=0.4.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/arg": {
-            "version": "4.1.3",
-            "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
-            "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
-        },
-        "node_modules/ayu": {
-            "version": "8.0.1",
-            "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz",
-            "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==",
+        "node_modules/@typescript-eslint/scope-manager": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz",
+            "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==",
             "dependencies": {
-                "@types/chroma-js": "^2.0.0",
-                "chroma-js": "^2.1.0",
-                "nonenumerable": "^1.1.1"
-            }
-        },
-        "node_modules/bezier-easing": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
-            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
-        },
-        "node_modules/case-anything": {
-            "version": "2.1.10",
-            "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
-            "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==",
+                "@typescript-eslint/types": "5.60.1",
+                "@typescript-eslint/visitor-keys": "5.60.1"
+            },
             "engines": {
-                "node": ">=12.13"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             },
             "funding": {
-                "url": "https://github.com/sponsors/mesqueeb"
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
             }
         },
-        "node_modules/chroma-js": {
-            "version": "2.4.2",
-            "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
-            "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
-        },
-        "node_modules/create-require": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
-        },
-        "node_modules/deepmerge": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
-            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
+        "node_modules/@typescript-eslint/type-utils": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz",
+            "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==",
+            "dependencies": {
+                "@typescript-eslint/typescript-estree": "5.60.1",
+                "@typescript-eslint/utils": "5.60.1",
+                "debug": "^4.3.4",
+                "tsutils": "^3.21.0"
+            },
             "engines": {
-                "node": ">=0.10.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "*"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/diff": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+        "node_modules/@typescript-eslint/types": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz",
+            "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==",
             "engines": {
-                "node": ">=0.3.1"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
             }
         },
-        "node_modules/make-error": {
-            "version": "1.3.6",
-            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
-        },
-        "node_modules/nonenumerable": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz",
-            "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q=="
-        },
-        "node_modules/toml": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
-            "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
-        },
-        "node_modules/ts-node": {
-            "version": "10.9.1",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
-            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+        "node_modules/@typescript-eslint/typescript-estree": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz",
+            "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==",
             "dependencies": {
-                "@cspotcode/source-map-support": "^0.8.0",
-                "@tsconfig/node10": "^1.0.7",
-                "@tsconfig/node12": "^1.0.7",
-                "@tsconfig/node14": "^1.0.0",
-                "@tsconfig/node16": "^1.0.2",
-                "acorn": "^8.4.1",
-                "acorn-walk": "^8.1.1",
-                "arg": "^4.1.0",
-                "create-require": "^1.1.0",
-                "diff": "^4.0.1",
-                "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.1",
-                "yn": "3.1.1"
+                "@typescript-eslint/types": "5.60.1",
+                "@typescript-eslint/visitor-keys": "5.60.1",
+                "debug": "^4.3.4",
+                "globby": "^11.1.0",
+                "is-glob": "^4.0.3",
+                "semver": "^7.3.7",
+                "tsutils": "^3.21.0"
             },
-            "bin": {
-                "ts-node": "dist/bin.js",
-                "ts-node-cwd": "dist/bin-cwd.js",
-                "ts-node-esm": "dist/bin-esm.js",
-                "ts-node-script": "dist/bin-script.js",
-                "ts-node-transpile-only": "dist/bin-transpile.js",
-                "ts-script": "dist/bin-script-deprecated.js"
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
             },
-            "peerDependencies": {
-                "@swc/core": ">=1.2.50",
-                "@swc/wasm": ">=1.2.50",
-                "@types/node": "*",
-                "typescript": ">=2.7"
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
             },
             "peerDependenciesMeta": {
-                "@swc/core": {
-                    "optional": true
-                },
-                "@swc/wasm": {
+                "typescript": {
                     "optional": true
                 }
             }
         },
-        "node_modules/typescript": {
-            "version": "4.9.5",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
-            "peer": true,
-            "bin": {
-                "tsc": "bin/tsc",
-                "tsserver": "bin/tsserver"
+        "node_modules/@typescript-eslint/utils": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz",
+            "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==",
+            "dependencies": {
+                "@eslint-community/eslint-utils": "^4.2.0",
+                "@types/json-schema": "^7.0.9",
+                "@types/semver": "^7.3.12",
+                "@typescript-eslint/scope-manager": "5.60.1",
+                "@typescript-eslint/types": "5.60.1",
+                "@typescript-eslint/typescript-estree": "5.60.1",
+                "eslint-scope": "^5.1.1",
+                "semver": "^7.3.7"
             },
             "engines": {
-                "node": ">=4.2.0"
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            },
+            "peerDependencies": {
+                "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
             }
         },
-        "node_modules/v8-compile-cache-lib": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
-        },
-        "node_modules/yn": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+        "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+            "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^4.1.1"
+            },
             "engines": {
-                "node": ">=6"
+                "node": ">=8.0.0"
             }
-        }
-    },
-    "dependencies": {
-        "@cspotcode/source-map-support": {
-            "version": "0.8.1",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
-            "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
-            "requires": {
-                "@jridgewell/trace-mapping": "0.3.9"
-            }
-        },
-        "@jridgewell/resolve-uri": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
-            "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
         },
-        "@jridgewell/sourcemap-codec": {
-            "version": "1.4.14",
-            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
-            "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
-        },
-        "@jridgewell/trace-mapping": {
-            "version": "0.3.9",
-            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
-            "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
-            "requires": {
-                "@jridgewell/resolve-uri": "^3.0.3",
-                "@jridgewell/sourcemap-codec": "^1.4.10"
+        "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+            "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+            "engines": {
+                "node": ">=4.0"
             }
         },
-        "@tokens-studio/types": {
-            "version": "0.2.3",
-            "resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.2.3.tgz",
-            "integrity": "sha512-2KN3V0JPf+Zh8aoVMwykJq29Lsi7vYgKGYBQ/zQ+FbDEmrH6T/Vwn8kG7cvbTmW1JAAvgxVxMIivgC9PmFelNA=="
+        "node_modules/@typescript-eslint/visitor-keys": {
+            "version": "5.60.1",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz",
+            "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==",
+            "dependencies": {
+                "@typescript-eslint/types": "5.60.1",
+                "eslint-visitor-keys": "^3.3.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/typescript-eslint"
+            }
         },
-        "@tsconfig/node10": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
-            "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
+        "node_modules/@vitest/coverage-v8": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.32.0.tgz",
+            "integrity": "sha512-VXXlWq9X/NbsoP/l/CHLBjutsFFww1UY1qEhzGjn/DY7Tqe+z0Nu8XKc8im/XUAmjiWsh2XV7sy/F0IKAl4eaw==",
+            "dependencies": {
+                "@ampproject/remapping": "^2.2.1",
+                "@bcoe/v8-coverage": "^0.2.3",
+                "istanbul-lib-coverage": "^3.2.0",
+                "istanbul-lib-report": "^3.0.0",
+                "istanbul-lib-source-maps": "^4.0.1",
+                "istanbul-reports": "^3.1.5",
+                "magic-string": "^0.30.0",
+                "picocolors": "^1.0.0",
+                "std-env": "^3.3.2",
+                "test-exclude": "^6.0.0",
+                "v8-to-istanbul": "^9.1.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            },
+            "peerDependencies": {
+                "vitest": ">=0.32.0 <1"
+            }
         },
-        "@tsconfig/node12": {
-            "version": "1.0.11",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
-            "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
+        "node_modules/@vitest/expect": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.0.tgz",
+            "integrity": "sha512-VxVHhIxKw9Lux+O9bwLEEk2gzOUe93xuFHy9SzYWnnoYZFYg1NfBtnfnYWiJN7yooJ7KNElCK5YtA7DTZvtXtg==",
+            "dependencies": {
+                "@vitest/spy": "0.32.0",
+                "@vitest/utils": "0.32.0",
+                "chai": "^4.3.7"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            }
         },
-        "@tsconfig/node14": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
-            "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
+        "node_modules/@vitest/runner": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.0.tgz",
+            "integrity": "sha512-QpCmRxftHkr72xt5A08xTEs9I4iWEXIOCHWhQQguWOKE4QH7DXSKZSOFibuwEIMAD7G0ERvtUyQn7iPWIqSwmw==",
+            "dependencies": {
+                "@vitest/utils": "0.32.0",
+                "concordance": "^5.0.4",
+                "p-limit": "^4.0.0",
+                "pathe": "^1.1.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            }
         },
-        "@tsconfig/node16": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
-            "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
+        "node_modules/@vitest/snapshot": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.0.tgz",
+            "integrity": "sha512-yCKorPWjEnzpUxQpGlxulujTcSPgkblwGzAUEL+z01FTUg/YuCDZ8dxr9sHA08oO2EwxzHXNLjQKWJ2zc2a19Q==",
+            "dependencies": {
+                "magic-string": "^0.30.0",
+                "pathe": "^1.1.0",
+                "pretty-format": "^27.5.1"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            }
         },
-        "@types/chroma-js": {
-            "version": "2.4.0",
-            "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
-            "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
+        "node_modules/@vitest/spy": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.0.tgz",
+            "integrity": "sha512-MruAPlM0uyiq3d53BkwTeShXY0rYEfhNGQzVO5GHBmmX3clsxcWp79mMnkOVcV244sNTeDcHbcPFWIjOI4tZvw==",
+            "dependencies": {
+                "tinyspy": "^2.1.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            }
         },
-        "@types/node": {
-            "version": "18.14.1",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
-            "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
+        "node_modules/@vitest/utils": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.0.tgz",
+            "integrity": "sha512-53yXunzx47MmbuvcOPpLaVljHaeSu1G2dHdmy7+9ngMnQIkBQcvwOcoclWFnxDMxFbnq8exAfh3aKSZaK71J5A==",
+            "dependencies": {
+                "concordance": "^5.0.4",
+                "loupe": "^2.3.6",
+                "pretty-format": "^27.5.1"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            }
         },
-        "acorn": {
+        "node_modules/acorn": {
             "version": "8.8.2",
             "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
-            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="
+            "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+            "bin": {
+                "acorn": "bin/acorn"
+            },
+            "engines": {
+                "node": ">=0.4.0"
+            }
         },
-        "acorn-walk": {
+        "node_modules/acorn-jsx": {
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+            "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+            "peerDependencies": {
+                "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+            }
+        },
+        "node_modules/acorn-walk": {
             "version": "8.2.0",
             "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
-            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
+            "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+            "engines": {
+                "node": ">=0.4.0"
+            }
+        },
+        "node_modules/ajv": {
+            "version": "6.12.6",
+            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+            "dependencies": {
+                "fast-deep-equal": "^3.1.1",
+                "fast-json-stable-stringify": "^2.0.0",
+                "json-schema-traverse": "^0.4.1",
+                "uri-js": "^4.2.2"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/epoberezkin"
+            }
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "engines": {
+                "node": ">=8"
+            }
         },
-        "arg": {
+        "node_modules/ansi-styles": {
+            "version": "5.2.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+            "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/any-promise": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+            "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+        },
+        "node_modules/arg": {
             "version": "4.1.3",
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
-        "ayu": {
+        "node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+        },
+        "node_modules/array-buffer-byte-length": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+            "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "is-array-buffer": "^3.0.1"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/array-includes": {
+            "version": "3.1.6",
+            "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+            "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4",
+                "get-intrinsic": "^1.1.3",
+                "is-string": "^1.0.7"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/array-union": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+            "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/array.prototype.flat": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+            "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4",
+                "es-shim-unscopables": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/array.prototype.flatmap": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+            "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4",
+                "es-shim-unscopables": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/assertion-error": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+            "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/available-typed-arrays": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+            "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/ayu": {
             "version": "8.0.1",
             "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz",
             "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==",
-            "requires": {
+            "dependencies": {
                 "@types/chroma-js": "^2.0.0",
                 "chroma-js": "^2.1.0",
                 "nonenumerable": "^1.1.1"
             }
         },
-        "bezier-easing": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
-            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        "node_modules/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
         },
-        "case-anything": {
-            "version": "2.1.10",
-            "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
-            "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ=="
+        "node_modules/big-integer": {
+            "version": "1.6.51",
+            "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+            "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+            "engines": {
+                "node": ">=0.6"
+            }
         },
-        "chroma-js": {
-            "version": "2.4.2",
-            "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
-            "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+        "node_modules/blueimp-md5": {
+            "version": "2.19.0",
+            "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
+            "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
         },
-        "create-require": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+        "node_modules/bplist-parser": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+            "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+            "dependencies": {
+                "big-integer": "^1.6.44"
+            },
+            "engines": {
+                "node": ">= 5.10.0"
+            }
         },
-        "deepmerge": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
-            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
         },
-        "diff": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
+        "node_modules/braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dependencies": {
+                "fill-range": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
         },
-        "make-error": {
-            "version": "1.3.6",
-            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+        "node_modules/bundle-name": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+            "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+            "dependencies": {
+                "run-applescript": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
         },
-        "nonenumerable": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz",
-            "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q=="
+        "node_modules/cac": {
+            "version": "6.7.14",
+            "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+            "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+            "engines": {
+                "node": ">=8"
+            }
         },
-        "toml": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
-            "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
+        "node_modules/call-bind": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+            "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+            "dependencies": {
+                "function-bind": "^1.1.1",
+                "get-intrinsic": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
         },
-        "ts-node": {
-            "version": "10.9.1",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
-            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
-            "requires": {
-                "@cspotcode/source-map-support": "^0.8.0",
-                "@tsconfig/node10": "^1.0.7",
-                "@tsconfig/node12": "^1.0.7",
-                "@tsconfig/node14": "^1.0.0",
-                "@tsconfig/node16": "^1.0.2",
-                "acorn": "^8.4.1",
-                "acorn-walk": "^8.1.1",
-                "arg": "^4.1.0",
-                "create-require": "^1.1.0",
-                "diff": "^4.0.1",
-                "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.1",
-                "yn": "3.1.1"
+        "node_modules/call-me-maybe": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+            "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
+        },
+        "node_modules/callsites": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+            "engines": {
+                "node": ">=6"
             }
         },
-        "typescript": {
-            "version": "4.9.5",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
-            "peer": true
+        "node_modules/chai": {
+            "version": "4.3.7",
+            "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
+            "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
+            "dependencies": {
+                "assertion-error": "^1.1.0",
+                "check-error": "^1.0.2",
+                "deep-eql": "^4.1.2",
+                "get-func-name": "^2.0.0",
+                "loupe": "^2.3.1",
+                "pathval": "^1.1.1",
+                "type-detect": "^4.0.5"
+            },
+            "engines": {
+                "node": ">=4"
+            }
         },
-        "v8-compile-cache-lib": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
+        "node_modules/chalk": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+            "dependencies": {
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/chalk?sponsor=1"
+            }
         },
-        "yn": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
+        "node_modules/chalk/node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/check-error": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+            "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/chroma-js": {
+            "version": "2.4.2",
+            "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
+            "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
+        },
+        "node_modules/cli-color": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz",
+            "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==",
+            "dependencies": {
+                "d": "^1.0.1",
+                "es5-ext": "^0.10.61",
+                "es6-iterator": "^2.0.3",
+                "memoizee": "^0.4.15",
+                "timers-ext": "^0.1.7"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+        },
+        "node_modules/concordance": {
+            "version": "5.0.4",
+            "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz",
+            "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==",
+            "dependencies": {
+                "date-time": "^3.1.0",
+                "esutils": "^2.0.3",
+                "fast-diff": "^1.2.0",
+                "js-string-escape": "^1.0.1",
+                "lodash": "^4.17.15",
+                "md5-hex": "^3.0.1",
+                "semver": "^7.3.2",
+                "well-known-symbols": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14"
+            }
+        },
+        "node_modules/convert-source-map": {
+            "version": "1.9.0",
+            "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+            "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+        },
+        "node_modules/create-require": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+            "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+        },
+        "node_modules/cross-spawn": {
+            "version": "7.0.3",
+            "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+            "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+            "dependencies": {
+                "path-key": "^3.1.0",
+                "shebang-command": "^2.0.0",
+                "which": "^2.0.1"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/d": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+            "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+            "dependencies": {
+                "es5-ext": "^0.10.50",
+                "type": "^1.0.1"
+            }
+        },
+        "node_modules/date-time": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz",
+            "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==",
+            "dependencies": {
+                "time-zone": "^1.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/debug": {
+            "version": "4.3.4",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+            "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+            "dependencies": {
+                "ms": "2.1.2"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/deep-eql": {
+            "version": "4.1.3",
+            "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
+            "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+            "dependencies": {
+                "type-detect": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/deep-is": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+        },
+        "node_modules/deepmerge": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
+            "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/default-browser": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+            "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+            "dependencies": {
+                "bundle-name": "^3.0.0",
+                "default-browser-id": "^3.0.0",
+                "execa": "^7.1.1",
+                "titleize": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/default-browser-id": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+            "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+            "dependencies": {
+                "bplist-parser": "^0.2.0",
+                "untildify": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/define-lazy-prop": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+            "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/define-properties": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+            "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+            "dependencies": {
+                "has-property-descriptors": "^1.0.0",
+                "object-keys": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/diff": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+            "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+            "engines": {
+                "node": ">=0.3.1"
+            }
+        },
+        "node_modules/dir-glob": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+            "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+            "dependencies": {
+                "path-type": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/doctrine": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+            "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+            "dependencies": {
+                "esutils": "^2.0.2"
+            },
+            "engines": {
+                "node": ">=6.0.0"
+            }
+        },
+        "node_modules/enhanced-resolve": {
+            "version": "5.15.0",
+            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+            "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+            "dependencies": {
+                "graceful-fs": "^4.2.4",
+                "tapable": "^2.2.0"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            }
+        },
+        "node_modules/es-abstract": {
+            "version": "1.21.2",
+            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
+            "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
+            "dependencies": {
+                "array-buffer-byte-length": "^1.0.0",
+                "available-typed-arrays": "^1.0.5",
+                "call-bind": "^1.0.2",
+                "es-set-tostringtag": "^2.0.1",
+                "es-to-primitive": "^1.2.1",
+                "function.prototype.name": "^1.1.5",
+                "get-intrinsic": "^1.2.0",
+                "get-symbol-description": "^1.0.0",
+                "globalthis": "^1.0.3",
+                "gopd": "^1.0.1",
+                "has": "^1.0.3",
+                "has-property-descriptors": "^1.0.0",
+                "has-proto": "^1.0.1",
+                "has-symbols": "^1.0.3",
+                "internal-slot": "^1.0.5",
+                "is-array-buffer": "^3.0.2",
+                "is-callable": "^1.2.7",
+                "is-negative-zero": "^2.0.2",
+                "is-regex": "^1.1.4",
+                "is-shared-array-buffer": "^1.0.2",
+                "is-string": "^1.0.7",
+                "is-typed-array": "^1.1.10",
+                "is-weakref": "^1.0.2",
+                "object-inspect": "^1.12.3",
+                "object-keys": "^1.1.1",
+                "object.assign": "^4.1.4",
+                "regexp.prototype.flags": "^1.4.3",
+                "safe-regex-test": "^1.0.0",
+                "string.prototype.trim": "^1.2.7",
+                "string.prototype.trimend": "^1.0.6",
+                "string.prototype.trimstart": "^1.0.6",
+                "typed-array-length": "^1.0.4",
+                "unbox-primitive": "^1.0.2",
+                "which-typed-array": "^1.1.9"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/es-set-tostringtag": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+            "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+            "dependencies": {
+                "get-intrinsic": "^1.1.3",
+                "has": "^1.0.3",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/es-shim-unscopables": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+            "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+            "dependencies": {
+                "has": "^1.0.3"
+            }
+        },
+        "node_modules/es-to-primitive": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+            "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+            "dependencies": {
+                "is-callable": "^1.1.4",
+                "is-date-object": "^1.0.1",
+                "is-symbol": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/es5-ext": {
+            "version": "0.10.62",
+            "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
+            "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "es6-iterator": "^2.0.3",
+                "es6-symbol": "^3.1.3",
+                "next-tick": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/es6-iterator": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+            "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+            "dependencies": {
+                "d": "1",
+                "es5-ext": "^0.10.35",
+                "es6-symbol": "^3.1.1"
+            }
+        },
+        "node_modules/es6-symbol": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+            "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+            "dependencies": {
+                "d": "^1.0.1",
+                "ext": "^1.1.2"
+            }
+        },
+        "node_modules/es6-weak-map": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+            "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+            "dependencies": {
+                "d": "1",
+                "es5-ext": "^0.10.46",
+                "es6-iterator": "^2.0.3",
+                "es6-symbol": "^3.1.1"
+            }
+        },
+        "node_modules/esbuild": {
+            "version": "0.17.19",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+            "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+            "hasInstallScript": true,
+            "bin": {
+                "esbuild": "bin/esbuild"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "optionalDependencies": {
+                "@esbuild/android-arm": "0.17.19",
+                "@esbuild/android-arm64": "0.17.19",
+                "@esbuild/android-x64": "0.17.19",
+                "@esbuild/darwin-arm64": "0.17.19",
+                "@esbuild/darwin-x64": "0.17.19",
+                "@esbuild/freebsd-arm64": "0.17.19",
+                "@esbuild/freebsd-x64": "0.17.19",
+                "@esbuild/linux-arm": "0.17.19",
+                "@esbuild/linux-arm64": "0.17.19",
+                "@esbuild/linux-ia32": "0.17.19",
+                "@esbuild/linux-loong64": "0.17.19",
+                "@esbuild/linux-mips64el": "0.17.19",
+                "@esbuild/linux-ppc64": "0.17.19",
+                "@esbuild/linux-riscv64": "0.17.19",
+                "@esbuild/linux-s390x": "0.17.19",
+                "@esbuild/linux-x64": "0.17.19",
+                "@esbuild/netbsd-x64": "0.17.19",
+                "@esbuild/openbsd-x64": "0.17.19",
+                "@esbuild/sunos-x64": "0.17.19",
+                "@esbuild/win32-arm64": "0.17.19",
+                "@esbuild/win32-ia32": "0.17.19",
+                "@esbuild/win32-x64": "0.17.19"
+            }
+        },
+        "node_modules/escape-string-regexp": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+            "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/eslint": {
+            "version": "8.43.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
+            "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
+            "dependencies": {
+                "@eslint-community/eslint-utils": "^4.2.0",
+                "@eslint-community/regexpp": "^4.4.0",
+                "@eslint/eslintrc": "^2.0.3",
+                "@eslint/js": "8.43.0",
+                "@humanwhocodes/config-array": "^0.11.10",
+                "@humanwhocodes/module-importer": "^1.0.1",
+                "@nodelib/fs.walk": "^1.2.8",
+                "ajv": "^6.10.0",
+                "chalk": "^4.0.0",
+                "cross-spawn": "^7.0.2",
+                "debug": "^4.3.2",
+                "doctrine": "^3.0.0",
+                "escape-string-regexp": "^4.0.0",
+                "eslint-scope": "^7.2.0",
+                "eslint-visitor-keys": "^3.4.1",
+                "espree": "^9.5.2",
+                "esquery": "^1.4.2",
+                "esutils": "^2.0.2",
+                "fast-deep-equal": "^3.1.3",
+                "file-entry-cache": "^6.0.1",
+                "find-up": "^5.0.0",
+                "glob-parent": "^6.0.2",
+                "globals": "^13.19.0",
+                "graphemer": "^1.4.0",
+                "ignore": "^5.2.0",
+                "import-fresh": "^3.0.0",
+                "imurmurhash": "^0.1.4",
+                "is-glob": "^4.0.0",
+                "is-path-inside": "^3.0.3",
+                "js-yaml": "^4.1.0",
+                "json-stable-stringify-without-jsonify": "^1.0.1",
+                "levn": "^0.4.1",
+                "lodash.merge": "^4.6.2",
+                "minimatch": "^3.1.2",
+                "natural-compare": "^1.4.0",
+                "optionator": "^0.9.1",
+                "strip-ansi": "^6.0.1",
+                "strip-json-comments": "^3.1.0",
+                "text-table": "^0.2.0"
+            },
+            "bin": {
+                "eslint": "bin/eslint.js"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/eslint-import-resolver-node": {
+            "version": "0.3.7",
+            "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+            "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
+            "dependencies": {
+                "debug": "^3.2.7",
+                "is-core-module": "^2.11.0",
+                "resolve": "^1.22.1"
+            }
+        },
+        "node_modules/eslint-import-resolver-node/node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/eslint-import-resolver-typescript": {
+            "version": "3.5.5",
+            "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz",
+            "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==",
+            "dependencies": {
+                "debug": "^4.3.4",
+                "enhanced-resolve": "^5.12.0",
+                "eslint-module-utils": "^2.7.4",
+                "get-tsconfig": "^4.5.0",
+                "globby": "^13.1.3",
+                "is-core-module": "^2.11.0",
+                "is-glob": "^4.0.3",
+                "synckit": "^0.8.5"
+            },
+            "engines": {
+                "node": "^14.18.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+            },
+            "peerDependencies": {
+                "eslint": "*",
+                "eslint-plugin-import": "*"
+            }
+        },
+        "node_modules/eslint-import-resolver-typescript/node_modules/globby": {
+            "version": "13.2.0",
+            "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.0.tgz",
+            "integrity": "sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==",
+            "dependencies": {
+                "dir-glob": "^3.0.1",
+                "fast-glob": "^3.2.11",
+                "ignore": "^5.2.0",
+                "merge2": "^1.4.1",
+                "slash": "^4.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/eslint-import-resolver-typescript/node_modules/slash": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+            "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/eslint-module-utils": {
+            "version": "2.8.0",
+            "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+            "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+            "dependencies": {
+                "debug": "^3.2.7"
+            },
+            "engines": {
+                "node": ">=4"
+            },
+            "peerDependenciesMeta": {
+                "eslint": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/eslint-module-utils/node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/eslint-plugin-import": {
+            "version": "2.27.5",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
+            "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
+            "dependencies": {
+                "array-includes": "^3.1.6",
+                "array.prototype.flat": "^1.3.1",
+                "array.prototype.flatmap": "^1.3.1",
+                "debug": "^3.2.7",
+                "doctrine": "^2.1.0",
+                "eslint-import-resolver-node": "^0.3.7",
+                "eslint-module-utils": "^2.7.4",
+                "has": "^1.0.3",
+                "is-core-module": "^2.11.0",
+                "is-glob": "^4.0.3",
+                "minimatch": "^3.1.2",
+                "object.values": "^1.1.6",
+                "resolve": "^1.22.1",
+                "semver": "^6.3.0",
+                "tsconfig-paths": "^3.14.1"
+            },
+            "engines": {
+                "node": ">=4"
+            },
+            "peerDependencies": {
+                "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+            }
+        },
+        "node_modules/eslint-plugin-import/node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/eslint-plugin-import/node_modules/doctrine": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+            "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+            "dependencies": {
+                "esutils": "^2.0.2"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/eslint-plugin-import/node_modules/semver": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "node_modules/eslint-scope": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
+            "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+            "dependencies": {
+                "esrecurse": "^4.3.0",
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/eslint-visitor-keys": {
+            "version": "3.4.1",
+            "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+            "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/espree": {
+            "version": "9.5.2",
+            "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
+            "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
+            "dependencies": {
+                "acorn": "^8.8.0",
+                "acorn-jsx": "^5.3.2",
+                "eslint-visitor-keys": "^3.4.1"
+            },
+            "engines": {
+                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/eslint"
+            }
+        },
+        "node_modules/esquery": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+            "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+            "dependencies": {
+                "estraverse": "^5.1.0"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/esrecurse": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+            "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+            "dependencies": {
+                "estraverse": "^5.2.0"
+            },
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/estraverse": {
+            "version": "5.3.0",
+            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+            "engines": {
+                "node": ">=4.0"
+            }
+        },
+        "node_modules/esutils": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/event-emitter": {
+            "version": "0.3.5",
+            "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+            "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+            "dependencies": {
+                "d": "1",
+                "es5-ext": "~0.10.14"
+            }
+        },
+        "node_modules/execa": {
+            "version": "7.1.1",
+            "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
+            "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==",
+            "dependencies": {
+                "cross-spawn": "^7.0.3",
+                "get-stream": "^6.0.1",
+                "human-signals": "^4.3.0",
+                "is-stream": "^3.0.0",
+                "merge-stream": "^2.0.0",
+                "npm-run-path": "^5.1.0",
+                "onetime": "^6.0.0",
+                "signal-exit": "^3.0.7",
+                "strip-final-newline": "^3.0.0"
+            },
+            "engines": {
+                "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sindresorhus/execa?sponsor=1"
+            }
+        },
+        "node_modules/ext": {
+            "version": "1.7.0",
+            "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+            "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+            "dependencies": {
+                "type": "^2.7.2"
+            }
+        },
+        "node_modules/ext/node_modules/type": {
+            "version": "2.7.2",
+            "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+            "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw=="
+        },
+        "node_modules/fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+        },
+        "node_modules/fast-diff": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+            "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
+        },
+        "node_modules/fast-glob": {
+            "version": "3.2.12",
+            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+            "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+            "dependencies": {
+                "@nodelib/fs.stat": "^2.0.2",
+                "@nodelib/fs.walk": "^1.2.3",
+                "glob-parent": "^5.1.2",
+                "merge2": "^1.3.0",
+                "micromatch": "^4.0.4"
+            },
+            "engines": {
+                "node": ">=8.6.0"
+            }
+        },
+        "node_modules/fast-glob/node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/fast-json-stable-stringify": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+        },
+        "node_modules/fast-levenshtein": {
+            "version": "2.0.6",
+            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+        },
+        "node_modules/fastq": {
+            "version": "1.15.0",
+            "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+            "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+            "dependencies": {
+                "reusify": "^1.0.4"
+            }
+        },
+        "node_modules/file-entry-cache": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+            "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+            "dependencies": {
+                "flat-cache": "^3.0.4"
+            },
+            "engines": {
+                "node": "^10.12.0 || >=12.0.0"
+            }
+        },
+        "node_modules/fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/find-up": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+            "dependencies": {
+                "locate-path": "^6.0.0",
+                "path-exists": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/flat-cache": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+            "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+            "dependencies": {
+                "flatted": "^3.1.0",
+                "rimraf": "^3.0.2"
+            },
+            "engines": {
+                "node": "^10.12.0 || >=12.0.0"
+            }
+        },
+        "node_modules/flatted": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+            "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
+        },
+        "node_modules/for-each": {
+            "version": "0.3.3",
+            "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+            "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+            "dependencies": {
+                "is-callable": "^1.1.3"
+            }
+        },
+        "node_modules/fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+        },
+        "node_modules/fsevents": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+            "hasInstallScript": true,
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+            }
+        },
+        "node_modules/function-bind": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+        },
+        "node_modules/function.prototype.name": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+            "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.3",
+                "es-abstract": "^1.19.0",
+                "functions-have-names": "^1.2.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/functions-have-names": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+            "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-func-name": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+            "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/get-intrinsic": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+            "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+            "dependencies": {
+                "function-bind": "^1.1.1",
+                "has": "^1.0.3",
+                "has-proto": "^1.0.1",
+                "has-symbols": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-stdin": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
+            "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/get-stream": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+            "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/get-symbol-description": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+            "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "get-intrinsic": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/get-tsconfig": {
+            "version": "4.6.2",
+            "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz",
+            "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==",
+            "dependencies": {
+                "resolve-pkg-maps": "^1.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+            }
+        },
+        "node_modules/glob": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+            "dependencies": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.1.1",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/glob-parent": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+            "dependencies": {
+                "is-glob": "^4.0.3"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            }
+        },
+        "node_modules/glob-promise": {
+            "version": "4.2.2",
+            "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-4.2.2.tgz",
+            "integrity": "sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==",
+            "dependencies": {
+                "@types/glob": "^7.1.3"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "type": "individual",
+                "url": "https://github.com/sponsors/ahmadnassri"
+            },
+            "peerDependencies": {
+                "glob": "^7.1.6"
+            }
+        },
+        "node_modules/globals": {
+            "version": "13.20.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+            "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+            "dependencies": {
+                "type-fest": "^0.20.2"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/globalthis": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+            "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+            "dependencies": {
+                "define-properties": "^1.1.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/globby": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+            "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+            "dependencies": {
+                "array-union": "^2.1.0",
+                "dir-glob": "^3.0.1",
+                "fast-glob": "^3.2.9",
+                "ignore": "^5.2.0",
+                "merge2": "^1.4.1",
+                "slash": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/gopd": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+            "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+            "dependencies": {
+                "get-intrinsic": "^1.1.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/graceful-fs": {
+            "version": "4.2.11",
+            "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+            "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+        },
+        "node_modules/grapheme-splitter": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+            "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
+        },
+        "node_modules/graphemer": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+            "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
+        },
+        "node_modules/has": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+            "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+            "dependencies": {
+                "function-bind": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4.0"
+            }
+        },
+        "node_modules/has-bigints": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+            "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-flag": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/has-property-descriptors": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+            "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+            "dependencies": {
+                "get-intrinsic": "^1.1.1"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+            "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-symbols": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+            "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-tostringtag": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+            "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+            "dependencies": {
+                "has-symbols": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/html-escaper": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+            "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="
+        },
+        "node_modules/human-signals": {
+            "version": "4.3.1",
+            "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+            "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+            "engines": {
+                "node": ">=14.18.0"
+            }
+        },
+        "node_modules/ignore": {
+            "version": "5.2.4",
+            "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+            "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+            "engines": {
+                "node": ">= 4"
+            }
+        },
+        "node_modules/import-fresh": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+            "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+            "dependencies": {
+                "parent-module": "^1.0.0",
+                "resolve-from": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/imurmurhash": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+            "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+            "engines": {
+                "node": ">=0.8.19"
+            }
+        },
+        "node_modules/inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+            "dependencies": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "node_modules/inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+        },
+        "node_modules/internal-slot": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
+            "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
+            "dependencies": {
+                "get-intrinsic": "^1.2.0",
+                "has": "^1.0.3",
+                "side-channel": "^1.0.4"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/is-array-buffer": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
+            "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "get-intrinsic": "^1.2.0",
+                "is-typed-array": "^1.1.10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-bigint": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+            "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+            "dependencies": {
+                "has-bigints": "^1.0.1"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-boolean-object": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+            "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-callable": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+            "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-core-module": {
+            "version": "2.12.1",
+            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
+            "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+            "dependencies": {
+                "has": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-date-object": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+            "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+            "dependencies": {
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-docker": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+            "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+            "bin": {
+                "is-docker": "cli.js"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dependencies": {
+                "is-extglob": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-inside-container": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+            "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+            "dependencies": {
+                "is-docker": "^3.0.0"
+            },
+            "bin": {
+                "is-inside-container": "cli.js"
+            },
+            "engines": {
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/is-negative-zero": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+            "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
+        "node_modules/is-number-object": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+            "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+            "dependencies": {
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-path-inside": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-promise": {
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+            "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
+        },
+        "node_modules/is-regex": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+            "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-shared-array-buffer": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+            "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+            "dependencies": {
+                "call-bind": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-stream": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+            "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/is-string": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+            "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+            "dependencies": {
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-symbol": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+            "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+            "dependencies": {
+                "has-symbols": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-typed-array": {
+            "version": "1.1.10",
+            "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+            "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+            "dependencies": {
+                "available-typed-arrays": "^1.0.5",
+                "call-bind": "^1.0.2",
+                "for-each": "^0.3.3",
+                "gopd": "^1.0.1",
+                "has-tostringtag": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-weakref": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+            "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+            "dependencies": {
+                "call-bind": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/is-wsl": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+            "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+            "dependencies": {
+                "is-docker": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-wsl/node_modules/is-docker": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+            "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+            "bin": {
+                "is-docker": "cli.js"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/isexe": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+            "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+        },
+        "node_modules/istanbul-lib-coverage": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
+            "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/istanbul-lib-report": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+            "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+            "dependencies": {
+                "istanbul-lib-coverage": "^3.0.0",
+                "make-dir": "^3.0.0",
+                "supports-color": "^7.1.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/istanbul-lib-source-maps": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+            "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+            "dependencies": {
+                "debug": "^4.1.1",
+                "istanbul-lib-coverage": "^3.0.0",
+                "source-map": "^0.6.1"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/istanbul-reports": {
+            "version": "3.1.5",
+            "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz",
+            "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==",
+            "dependencies": {
+                "html-escaper": "^2.0.0",
+                "istanbul-lib-report": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/js-string-escape": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
+            "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/js-tokens": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+            "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+            "peer": true
+        },
+        "node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/json-schema-to-typescript": {
+            "version": "13.0.2",
+            "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-13.0.2.tgz",
+            "integrity": "sha512-TCaEVW4aI2FmMQe7f98mvr3/oiVmXEC1xZjkTZ9L/BSoTXFlC7p64mD5AD2d8XWycNBQZUnHwXL5iVXt1HWwNQ==",
+            "dependencies": {
+                "@bcherny/json-schema-ref-parser": "10.0.5-fork",
+                "@types/json-schema": "^7.0.11",
+                "@types/lodash": "^4.14.182",
+                "@types/prettier": "^2.6.1",
+                "cli-color": "^2.0.2",
+                "get-stdin": "^8.0.0",
+                "glob": "^7.1.6",
+                "glob-promise": "^4.2.2",
+                "is-glob": "^4.0.3",
+                "lodash": "^4.17.21",
+                "minimist": "^1.2.6",
+                "mkdirp": "^1.0.4",
+                "mz": "^2.7.0",
+                "prettier": "^2.6.2"
+            },
+            "bin": {
+                "json2ts": "dist/src/cli.js"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/json-schema-traverse": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+        },
+        "node_modules/json-stable-stringify-without-jsonify": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+        },
+        "node_modules/json5": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+            "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+            "dependencies": {
+                "minimist": "^1.2.0"
+            },
+            "bin": {
+                "json5": "lib/cli.js"
+            }
+        },
+        "node_modules/jsonc-parser": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
+            "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
+        },
+        "node_modules/levn": {
+            "version": "0.4.1",
+            "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+            "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+            "dependencies": {
+                "prelude-ls": "^1.2.1",
+                "type-check": "~0.4.0"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/local-pkg": {
+            "version": "0.4.3",
+            "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",
+            "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            }
+        },
+        "node_modules/locate-path": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+            "dependencies": {
+                "p-locate": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/lodash": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+        },
+        "node_modules/lodash.merge": {
+            "version": "4.6.2",
+            "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+            "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+        },
+        "node_modules/loose-envify": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+            "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+            "peer": true,
+            "dependencies": {
+                "js-tokens": "^3.0.0 || ^4.0.0"
+            },
+            "bin": {
+                "loose-envify": "cli.js"
+            }
+        },
+        "node_modules/loupe": {
+            "version": "2.3.6",
+            "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
+            "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
+            "dependencies": {
+                "get-func-name": "^2.0.0"
+            }
+        },
+        "node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/lru-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
+            "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==",
+            "dependencies": {
+                "es5-ext": "~0.10.2"
+            }
+        },
+        "node_modules/magic-string": {
+            "version": "0.30.0",
+            "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
+            "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
+            "dependencies": {
+                "@jridgewell/sourcemap-codec": "^1.4.13"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/make-dir": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "dependencies": {
+                "semver": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/make-dir/node_modules/semver": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+        },
+        "node_modules/md5-hex": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
+            "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==",
+            "dependencies": {
+                "blueimp-md5": "^2.10.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/memoizee": {
+            "version": "0.4.15",
+            "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz",
+            "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==",
+            "dependencies": {
+                "d": "^1.0.1",
+                "es5-ext": "^0.10.53",
+                "es6-weak-map": "^2.0.3",
+                "event-emitter": "^0.3.5",
+                "is-promise": "^2.2.2",
+                "lru-queue": "^0.1.0",
+                "next-tick": "^1.1.0",
+                "timers-ext": "^0.1.7"
+            }
+        },
+        "node_modules/merge-stream": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+            "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+        },
+        "node_modules/merge2": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+            "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/micromatch": {
+            "version": "4.0.5",
+            "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+            "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+            "dependencies": {
+                "braces": "^3.0.2",
+                "picomatch": "^2.3.1"
+            },
+            "engines": {
+                "node": ">=8.6"
+            }
+        },
+        "node_modules/mimic-fn": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+            "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minimist": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+            "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/mkdirp": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/mlly": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz",
+            "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==",
+            "dependencies": {
+                "acorn": "^8.8.2",
+                "pathe": "^1.1.0",
+                "pkg-types": "^1.0.3",
+                "ufo": "^1.1.2"
+            }
+        },
+        "node_modules/ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "node_modules/mz": {
+            "version": "2.7.0",
+            "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+            "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+            "dependencies": {
+                "any-promise": "^1.0.0",
+                "object-assign": "^4.0.1",
+                "thenify-all": "^1.0.0"
+            }
+        },
+        "node_modules/nanoid": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+            "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "bin": {
+                "nanoid": "bin/nanoid.cjs"
+            },
+            "engines": {
+                "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+            }
+        },
+        "node_modules/natural-compare": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
+        },
+        "node_modules/natural-compare-lite": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+            "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="
+        },
+        "node_modules/next-tick": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+            "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+        },
+        "node_modules/nonenumerable": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz",
+            "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q=="
+        },
+        "node_modules/npm-run-path": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+            "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+            "dependencies": {
+                "path-key": "^4.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/npm-run-path/node_modules/path-key": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+            "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/object-assign": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+            "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/object-inspect": {
+            "version": "1.12.3",
+            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+            "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/object-keys": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+            "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/object.assign": {
+            "version": "4.1.4",
+            "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+            "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "has-symbols": "^1.0.3",
+                "object-keys": "^1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/object.values": {
+            "version": "1.1.6",
+            "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+            "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/onetime": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+            "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+            "dependencies": {
+                "mimic-fn": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/open": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+            "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+            "dependencies": {
+                "default-browser": "^4.0.0",
+                "define-lazy-prop": "^3.0.0",
+                "is-inside-container": "^1.0.0",
+                "is-wsl": "^2.2.0"
+            },
+            "engines": {
+                "node": ">=14.16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/optionator": {
+            "version": "0.9.3",
+            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+            "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+            "dependencies": {
+                "@aashutoshrathi/word-wrap": "^1.2.3",
+                "deep-is": "^0.1.3",
+                "fast-levenshtein": "^2.0.6",
+                "levn": "^0.4.1",
+                "prelude-ls": "^1.2.1",
+                "type-check": "^0.4.0"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/p-limit": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+            "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+            "dependencies": {
+                "yocto-queue": "^1.0.0"
+            },
+            "engines": {
+                "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/p-locate": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+            "dependencies": {
+                "p-limit": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/p-locate/node_modules/p-limit": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+            "dependencies": {
+                "yocto-queue": "^0.1.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/p-locate/node_modules/yocto-queue": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/parent-module": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+            "dependencies": {
+                "callsites": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/path-exists": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/path-key": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/path-parse": {
+            "version": "1.0.7",
+            "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+        },
+        "node_modules/path-type": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+            "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/pathe": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz",
+            "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q=="
+        },
+        "node_modules/pathval": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+            "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/picocolors": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+            "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/pkg-types": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz",
+            "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==",
+            "dependencies": {
+                "jsonc-parser": "^3.2.0",
+                "mlly": "^1.2.0",
+                "pathe": "^1.1.0"
+            }
+        },
+        "node_modules/postcss": {
+            "version": "8.4.24",
+            "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+            "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+            "funding": [
+                {
+                    "type": "opencollective",
+                    "url": "https://opencollective.com/postcss/"
+                },
+                {
+                    "type": "tidelift",
+                    "url": "https://tidelift.com/funding/github/npm/postcss"
+                },
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/ai"
+                }
+            ],
+            "dependencies": {
+                "nanoid": "^3.3.6",
+                "picocolors": "^1.0.0",
+                "source-map-js": "^1.0.2"
+            },
+            "engines": {
+                "node": "^10 || ^12 || >=14"
+            }
+        },
+        "node_modules/prelude-ls": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/prettier": {
+            "version": "2.8.8",
+            "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+            "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+            "bin": {
+                "prettier": "bin-prettier.js"
+            },
+            "engines": {
+                "node": ">=10.13.0"
+            },
+            "funding": {
+                "url": "https://github.com/prettier/prettier?sponsor=1"
+            }
+        },
+        "node_modules/pretty-format": {
+            "version": "27.5.1",
+            "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+            "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+            "dependencies": {
+                "ansi-regex": "^5.0.1",
+                "ansi-styles": "^5.0.0",
+                "react-is": "^17.0.1"
+            },
+            "engines": {
+                "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+            }
+        },
+        "node_modules/punycode": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+            "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/react": {
+            "version": "18.2.0",
+            "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+            "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+            "peer": true,
+            "dependencies": {
+                "loose-envify": "^1.1.0"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/react-is": {
+            "version": "17.0.2",
+            "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+            "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+        },
+        "node_modules/regexp.prototype.flags": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
+            "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.2.0",
+                "functions-have-names": "^1.2.3"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/resolve": {
+            "version": "1.22.2",
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+            "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+            "dependencies": {
+                "is-core-module": "^2.11.0",
+                "path-parse": "^1.0.7",
+                "supports-preserve-symlinks-flag": "^1.0.0"
+            },
+            "bin": {
+                "resolve": "bin/resolve"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/resolve-from": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/resolve-pkg-maps": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+            "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+            "funding": {
+                "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+            }
+        },
+        "node_modules/reusify": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+            "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+            "engines": {
+                "iojs": ">=1.0.0",
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/rollup": {
+            "version": "3.25.1",
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz",
+            "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==",
+            "bin": {
+                "rollup": "dist/bin/rollup"
+            },
+            "engines": {
+                "node": ">=14.18.0",
+                "npm": ">=8.0.0"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
+        "node_modules/run-applescript": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+            "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+            "dependencies": {
+                "execa": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/run-applescript/node_modules/execa": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+            "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+            "dependencies": {
+                "cross-spawn": "^7.0.3",
+                "get-stream": "^6.0.0",
+                "human-signals": "^2.1.0",
+                "is-stream": "^2.0.0",
+                "merge-stream": "^2.0.0",
+                "npm-run-path": "^4.0.1",
+                "onetime": "^5.1.2",
+                "signal-exit": "^3.0.3",
+                "strip-final-newline": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sindresorhus/execa?sponsor=1"
+            }
+        },
+        "node_modules/run-applescript/node_modules/human-signals": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+            "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+            "engines": {
+                "node": ">=10.17.0"
+            }
+        },
+        "node_modules/run-applescript/node_modules/is-stream": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/run-applescript/node_modules/mimic-fn": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+            "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/run-applescript/node_modules/npm-run-path": {
+            "version": "4.0.1",
+            "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+            "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+            "dependencies": {
+                "path-key": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/run-applescript/node_modules/onetime": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+            "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+            "dependencies": {
+                "mimic-fn": "^2.1.0"
+            },
+            "engines": {
+                "node": ">=6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/run-applescript/node_modules/strip-final-newline": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+            "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/run-parallel": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "dependencies": {
+                "queue-microtask": "^1.2.2"
+            }
+        },
+        "node_modules/safe-regex-test": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+            "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "get-intrinsic": "^1.1.3",
+                "is-regex": "^1.1.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/semver": {
+            "version": "7.5.2",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+            "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/shebang-command": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+            "dependencies": {
+                "shebang-regex": "^3.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/shebang-regex": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/side-channel": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+            "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+            "dependencies": {
+                "call-bind": "^1.0.0",
+                "get-intrinsic": "^1.0.2",
+                "object-inspect": "^1.9.0"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/siginfo": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+            "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="
+        },
+        "node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+        },
+        "node_modules/slash": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+            "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/source-map": {
+            "version": "0.6.1",
+            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/source-map-js": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+            "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/stackback": {
+            "version": "0.0.2",
+            "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+            "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="
+        },
+        "node_modules/std-env": {
+            "version": "3.3.3",
+            "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz",
+            "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg=="
+        },
+        "node_modules/string.prototype.trim": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
+            "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/string.prototype.trimend": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+            "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/string.prototype.trimstart": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+            "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "define-properties": "^1.1.4",
+                "es-abstract": "^1.20.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-bom": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+            "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/strip-final-newline": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+            "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/strip-json-comments": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/strip-literal": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz",
+            "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==",
+            "dependencies": {
+                "acorn": "^8.8.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/antfu"
+            }
+        },
+        "node_modules/supports-color": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+            "dependencies": {
+                "has-flag": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/supports-preserve-symlinks-flag": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+            "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/synckit": {
+            "version": "0.8.5",
+            "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+            "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+            "dependencies": {
+                "@pkgr/utils": "^2.3.1",
+                "tslib": "^2.5.0"
+            },
+            "engines": {
+                "node": "^14.18.0 || >=16.0.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/unts"
+            }
+        },
+        "node_modules/synckit/node_modules/tslib": {
+            "version": "2.6.0",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
+            "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
+        },
+        "node_modules/tapable": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+            "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/test-exclude": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+            "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+            "dependencies": {
+                "@istanbuljs/schema": "^0.1.2",
+                "glob": "^7.1.4",
+                "minimatch": "^3.0.4"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/text-table": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+        },
+        "node_modules/thenify": {
+            "version": "3.3.1",
+            "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+            "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+            "dependencies": {
+                "any-promise": "^1.0.0"
+            }
+        },
+        "node_modules/thenify-all": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+            "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+            "dependencies": {
+                "thenify": ">= 3.1.0 < 4"
+            },
+            "engines": {
+                "node": ">=0.8"
+            }
+        },
+        "node_modules/time-zone": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz",
+            "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/timers-ext": {
+            "version": "0.1.7",
+            "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
+            "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
+            "dependencies": {
+                "es5-ext": "~0.10.46",
+                "next-tick": "1"
+            }
+        },
+        "node_modules/tinybench": {
+            "version": "2.5.0",
+            "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz",
+            "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA=="
+        },
+        "node_modules/tinypool": {
+            "version": "0.5.0",
+            "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz",
+            "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==",
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/tinyspy": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz",
+            "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==",
+            "engines": {
+                "node": ">=14.0.0"
+            }
+        },
+        "node_modules/titleize": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+            "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
+        "node_modules/toml": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
+            "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
+        },
+        "node_modules/ts-deepmerge": {
+            "version": "6.0.3",
+            "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.0.3.tgz",
+            "integrity": "sha512-MBBJL0UK/mMnZRONMz4J1CRu5NsGtsh+gR1nkn8KLE9LXo/PCzeHhQduhNary8m5/m9ryOOyFwVKxq81cPlaow==",
+            "engines": {
+                "node": ">=14.13.1"
+            }
+        },
+        "node_modules/ts-node": {
+            "version": "10.9.1",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+            "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+            "dependencies": {
+                "@cspotcode/source-map-support": "^0.8.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.1",
+                "yn": "3.1.1"
+            },
+            "bin": {
+                "ts-node": "dist/bin.js",
+                "ts-node-cwd": "dist/bin-cwd.js",
+                "ts-node-esm": "dist/bin-esm.js",
+                "ts-node-script": "dist/bin-script.js",
+                "ts-node-transpile-only": "dist/bin-transpile.js",
+                "ts-script": "dist/bin-script-deprecated.js"
+            },
+            "peerDependencies": {
+                "@swc/core": ">=1.2.50",
+                "@swc/wasm": ">=1.2.50",
+                "@types/node": "*",
+                "typescript": ">=2.7"
+            },
+            "peerDependenciesMeta": {
+                "@swc/core": {
+                    "optional": true
+                },
+                "@swc/wasm": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/tsconfig-paths": {
+            "version": "3.14.2",
+            "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+            "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+            "dependencies": {
+                "@types/json5": "^0.0.29",
+                "json5": "^1.0.2",
+                "minimist": "^1.2.6",
+                "strip-bom": "^3.0.0"
+            }
+        },
+        "node_modules/tslib": {
+            "version": "1.14.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+            "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+        },
+        "node_modules/tsutils": {
+            "version": "3.21.0",
+            "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+            "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+            "dependencies": {
+                "tslib": "^1.8.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            },
+            "peerDependencies": {
+                "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+            }
+        },
+        "node_modules/type": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+            "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+        },
+        "node_modules/type-check": {
+            "version": "0.4.0",
+            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+            "dependencies": {
+                "prelude-ls": "^1.2.1"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/type-detect": {
+            "version": "4.0.8",
+            "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+            "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/type-fest": {
+            "version": "0.20.2",
+            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/typed-array-length": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+            "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "for-each": "^0.3.3",
+                "is-typed-array": "^1.1.9"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "5.1.5",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.5.tgz",
+            "integrity": "sha512-FOH+WN/DQjUvN6WgW+c4Ml3yi0PH+a/8q+kNIfRehv1wLhWONedw85iu+vQ39Wp49IzTJEsZ2lyLXpBF7mkF1g==",
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=14.17"
+            }
+        },
+        "node_modules/ufo": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz",
+            "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ=="
+        },
+        "node_modules/unbox-primitive": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+            "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+            "dependencies": {
+                "call-bind": "^1.0.2",
+                "has-bigints": "^1.0.2",
+                "has-symbols": "^1.0.3",
+                "which-boxed-primitive": "^1.0.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/untildify": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+            "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+            "dependencies": {
+                "punycode": "^2.1.0"
+            }
+        },
+        "node_modules/use-sync-external-store": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+            "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+            "peerDependencies": {
+                "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+            }
+        },
+        "node_modules/utility-types": {
+            "version": "3.10.0",
+            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
+            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
+            "engines": {
+                "node": ">= 4"
+            }
+        },
+        "node_modules/v8-compile-cache-lib": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+            "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
+        },
+        "node_modules/v8-to-istanbul": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
+            "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==",
+            "dependencies": {
+                "@jridgewell/trace-mapping": "^0.3.12",
+                "@types/istanbul-lib-coverage": "^2.0.1",
+                "convert-source-map": "^1.6.0"
+            },
+            "engines": {
+                "node": ">=10.12.0"
+            }
+        },
+        "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": {
+            "version": "0.3.18",
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
+            "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+            "dependencies": {
+                "@jridgewell/resolve-uri": "3.1.0",
+                "@jridgewell/sourcemap-codec": "1.4.14"
+            }
+        },
+        "node_modules/vite": {
+            "version": "4.3.9",
+            "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
+            "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
+            "dependencies": {
+                "esbuild": "^0.17.5",
+                "postcss": "^8.4.23",
+                "rollup": "^3.21.0"
+            },
+            "bin": {
+                "vite": "bin/vite.js"
+            },
+            "engines": {
+                "node": "^14.18.0 || >=16.0.0"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            },
+            "peerDependencies": {
+                "@types/node": ">= 14",
+                "less": "*",
+                "sass": "*",
+                "stylus": "*",
+                "sugarss": "*",
+                "terser": "^5.4.0"
+            },
+            "peerDependenciesMeta": {
+                "@types/node": {
+                    "optional": true
+                },
+                "less": {
+                    "optional": true
+                },
+                "sass": {
+                    "optional": true
+                },
+                "stylus": {
+                    "optional": true
+                },
+                "sugarss": {
+                    "optional": true
+                },
+                "terser": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/vite-node": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.0.tgz",
+            "integrity": "sha512-220P/y8YacYAU+daOAqiGEFXx2A8AwjadDzQqos6wSukjvvTWNqleJSwoUn0ckyNdjHIKoxn93Nh1vWBqEKr3Q==",
+            "dependencies": {
+                "cac": "^6.7.14",
+                "debug": "^4.3.4",
+                "mlly": "^1.2.0",
+                "pathe": "^1.1.0",
+                "picocolors": "^1.0.0",
+                "vite": "^3.0.0 || ^4.0.0"
+            },
+            "bin": {
+                "vite-node": "vite-node.mjs"
+            },
+            "engines": {
+                "node": ">=v14.18.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            }
+        },
+        "node_modules/vitest": {
+            "version": "0.32.0",
+            "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.0.tgz",
+            "integrity": "sha512-SW83o629gCqnV3BqBnTxhB10DAwzwEx3z+rqYZESehUB+eWsJxwcBQx7CKy0otuGMJTYh7qCVuUX23HkftGl/Q==",
+            "dependencies": {
+                "@types/chai": "^4.3.5",
+                "@types/chai-subset": "^1.3.3",
+                "@types/node": "*",
+                "@vitest/expect": "0.32.0",
+                "@vitest/runner": "0.32.0",
+                "@vitest/snapshot": "0.32.0",
+                "@vitest/spy": "0.32.0",
+                "@vitest/utils": "0.32.0",
+                "acorn": "^8.8.2",
+                "acorn-walk": "^8.2.0",
+                "cac": "^6.7.14",
+                "chai": "^4.3.7",
+                "concordance": "^5.0.4",
+                "debug": "^4.3.4",
+                "local-pkg": "^0.4.3",
+                "magic-string": "^0.30.0",
+                "pathe": "^1.1.0",
+                "picocolors": "^1.0.0",
+                "std-env": "^3.3.2",
+                "strip-literal": "^1.0.1",
+                "tinybench": "^2.5.0",
+                "tinypool": "^0.5.0",
+                "vite": "^3.0.0 || ^4.0.0",
+                "vite-node": "0.32.0",
+                "why-is-node-running": "^2.2.2"
+            },
+            "bin": {
+                "vitest": "vitest.mjs"
+            },
+            "engines": {
+                "node": ">=v14.18.0"
+            },
+            "funding": {
+                "url": "https://opencollective.com/vitest"
+            },
+            "peerDependencies": {
+                "@edge-runtime/vm": "*",
+                "@vitest/browser": "*",
+                "@vitest/ui": "*",
+                "happy-dom": "*",
+                "jsdom": "*",
+                "playwright": "*",
+                "safaridriver": "*",
+                "webdriverio": "*"
+            },
+            "peerDependenciesMeta": {
+                "@edge-runtime/vm": {
+                    "optional": true
+                },
+                "@vitest/browser": {
+                    "optional": true
+                },
+                "@vitest/ui": {
+                    "optional": true
+                },
+                "happy-dom": {
+                    "optional": true
+                },
+                "jsdom": {
+                    "optional": true
+                },
+                "playwright": {
+                    "optional": true
+                },
+                "safaridriver": {
+                    "optional": true
+                },
+                "webdriverio": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/well-known-symbols": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz",
+            "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/which": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+            "dependencies": {
+                "isexe": "^2.0.0"
+            },
+            "bin": {
+                "node-which": "bin/node-which"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/which-boxed-primitive": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+            "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+            "dependencies": {
+                "is-bigint": "^1.0.1",
+                "is-boolean-object": "^1.1.0",
+                "is-number-object": "^1.0.4",
+                "is-string": "^1.0.5",
+                "is-symbol": "^1.0.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/which-typed-array": {
+            "version": "1.1.9",
+            "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+            "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+            "dependencies": {
+                "available-typed-arrays": "^1.0.5",
+                "call-bind": "^1.0.2",
+                "for-each": "^0.3.3",
+                "gopd": "^1.0.1",
+                "has-tostringtag": "^1.0.0",
+                "is-typed-array": "^1.1.10"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/why-is-node-running": {
+            "version": "2.2.2",
+            "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz",
+            "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==",
+            "dependencies": {
+                "siginfo": "^2.0.0",
+                "stackback": "0.0.2"
+            },
+            "bin": {
+                "why-is-node-running": "cli.js"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+        },
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        },
+        "node_modules/yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/yocto-queue": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
+            "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
+            "engines": {
+                "node": ">=12.20"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/zustand": {
+            "version": "4.3.8",
+            "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
+            "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
+            "dependencies": {
+                "use-sync-external-store": "1.2.0"
+            },
+            "engines": {
+                "node": ">=12.7.0"
+            },
+            "peerDependencies": {
+                "immer": ">=9.0",
+                "react": ">=16.8"
+            },
+            "peerDependenciesMeta": {
+                "immer": {
+                    "optional": true
+                },
+                "react": {
+                    "optional": true
+                }
+            }
         }
     }
 }

styles/package.json πŸ”—

@@ -1,31 +1,37 @@
 {
     "name": "styles",
     "version": "1.0.0",
-    "description": "",
-    "main": "index.js",
+    "description": "Typescript app that builds Zed's themes",
+    "main": "./src/build_themes.ts",
     "scripts": {
-        "build": "ts-node ./src/buildThemes.ts",
-        "build-licenses": "ts-node ./src/buildLicenses.ts",
-        "build-tokens": "ts-node ./src/buildTokens.ts"
+        "build": "ts-node ./src/build_themes.ts",
+        "build-licenses": "ts-node ./src/build_licenses.ts",
+        "build-tokens": "ts-node ./src/build_tokens.ts",
+        "build-types": "ts-node ./src/build_types.ts",
+        "test": "vitest"
     },
-    "author": "",
+    "author": "Zed Industries (https://github.com/zed-industries/)",
     "license": "ISC",
     "dependencies": {
         "@tokens-studio/types": "^0.2.3",
         "@types/chroma-js": "^2.4.0",
         "@types/node": "^18.14.1",
+        "@typescript-eslint/eslint-plugin": "^5.60.1",
+        "@typescript-eslint/parser": "^5.60.1",
+        "@vitest/coverage-v8": "^0.32.0",
         "ayu": "^8.0.1",
-        "bezier-easing": "^2.1.0",
-        "case-anything": "^2.1.10",
         "chroma-js": "^2.4.2",
         "deepmerge": "^4.3.0",
+        "eslint": "^8.43.0",
+        "eslint-import-resolver-typescript": "^3.5.5",
+        "eslint-plugin-import": "^2.27.5",
+        "json-schema-to-typescript": "^13.0.2",
         "toml": "^3.0.0",
-        "ts-node": "^10.9.1"
-    },
-    "prettier": {
-        "semi": false,
-        "printWidth": 80,
-        "htmlWhitespaceSensitivity": "strict",
-        "tabWidth": 4
+        "ts-deepmerge": "^6.0.3",
+        "ts-node": "^10.9.1",
+        "typescript": "^5.1.5",
+        "utility-types": "^3.10.0",
+        "vitest": "^0.32.0",
+        "zustand": "^4.3.8"
     }
 }

styles/src/buildLicenses.ts πŸ”—

@@ -1,50 +0,0 @@
-import * as fs from "fs"
-import toml from "toml"
-import { themes } from "./themes"
-import { ThemeConfig } from "./common"
-
-const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
-
-// Use the cargo-about configuration file as the source of truth for supported licenses.
-function parseAcceptedToml(file: string): string[] {
-    let buffer = fs.readFileSync(file).toString()
-
-    let obj = toml.parse(buffer)
-
-    if (!Array.isArray(obj.accepted)) {
-        throw Error("Accepted license source is malformed")
-    }
-
-    return obj.accepted
-}
-
-function checkLicenses(themes: ThemeConfig[]) {
-    for (const theme of themes) {
-        if (!theme.licenseFile) {
-            throw Error(`Theme ${theme.name} should have a LICENSE file`)
-        }
-    }
-}
-
-function generateLicenseFile(themes: ThemeConfig[]) {
-    checkLicenses(themes)
-    for (const theme of themes) {
-        const licenseText = fs.readFileSync(theme.licenseFile).toString()
-        writeLicense(theme.name, licenseText, theme.licenseUrl)
-    }
-}
-
-function writeLicense(
-    themeName: string,
-    licenseText: string,
-    licenseUrl?: string
-) {
-    process.stdout.write(
-        licenseUrl
-            ? `## [${themeName}](${licenseUrl})\n\n${licenseText}\n********************************************************************************\n\n`
-            : `## ${themeName}\n\n${licenseText}\n********************************************************************************\n\n`
-    )
-}
-
-const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE)
-generateLicenseFile(themes)

styles/src/buildThemes.ts πŸ”—

@@ -1,43 +0,0 @@
-import * as fs from "fs"
-import { tmpdir } from "os"
-import * as path from "path"
-import app from "./styleTree/app"
-import { ColorScheme, createColorScheme } from "./theme/colorScheme"
-import snakeCase from "./utils/snakeCase"
-import { themes } from "./themes"
-
-const assetsDirectory = `${__dirname}/../../assets`
-const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
-
-// Clear existing themes
-function clearThemes(themeDirectory: string) {
-    if (!fs.existsSync(themeDirectory)) {
-        fs.mkdirSync(themeDirectory, { recursive: true })
-    } else {
-        for (const file of fs.readdirSync(themeDirectory)) {
-            if (file.endsWith(".json")) {
-                fs.unlinkSync(path.join(themeDirectory, file))
-            }
-        }
-    }
-}
-
-function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
-    clearThemes(outputDirectory)
-    for (let colorScheme of colorSchemes) {
-        let styleTree = snakeCase(app(colorScheme))
-        let styleTreeJSON = JSON.stringify(styleTree, null, 2)
-        let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`)
-        let outPath = path.join(outputDirectory, `${colorScheme.name}.json`)
-        fs.writeFileSync(tempPath, styleTreeJSON)
-        fs.renameSync(tempPath, outPath)
-        console.log(`- ${outPath} created`)
-    }
-}
-
-const colorSchemes: ColorScheme[] = themes.map((theme) =>
-    createColorScheme(theme)
-)
-
-// Write new themes to theme directory
-writeThemes(colorSchemes, `${assetsDirectory}/themes`)

styles/src/buildTokens.ts πŸ”—

@@ -1,85 +0,0 @@
-import * as fs from "fs";
-import * as path from "path";
-import { ColorScheme, createColorScheme } from "./common";
-import { themes } from "./themes";
-import { slugify } from "./utils/slugify";
-import { colorSchemeTokens } from "./theme/tokens/colorScheme";
-
-const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens");
-const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json");
-const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json");
-
-function clearTokens(tokensDirectory: string) {
-    if (!fs.existsSync(tokensDirectory)) {
-        fs.mkdirSync(tokensDirectory, { recursive: true })
-    } else {
-        for (const file of fs.readdirSync(tokensDirectory)) {
-            if (file.endsWith(".json")) {
-                fs.unlinkSync(path.join(tokensDirectory, file))
-            }
-        }
-    }
-}
-
-type TokenSet = {
-    id: string;
-    name: string;
-    selectedTokenSets: { [key: string]: "enabled" };
-};
-
-function buildTokenSetOrder(colorSchemes: ColorScheme[]): { tokenSetOrder: string[] } {
-    const tokenSetOrder: string[] = colorSchemes.map(
-        (scheme) => scheme.name.toLowerCase().replace(/\s+/g, "_")
-    );
-    return { tokenSetOrder };
-}
-
-function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
-    const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
-        const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
-            .toLowerCase()
-            .replace(/\s+/g, "_")}_${index}`;
-        const selectedTokenSets: { [key: string]: "enabled" } = {};
-        const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_");
-        selectedTokenSets[tokenSet] = "enabled";
-
-        return {
-            id,
-            name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
-            selectedTokenSets,
-        };
-    });
-
-    return themesIndex;
-}
-
-function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
-    clearTokens(tokensDirectory);
-
-    for (const colorScheme of colorSchemes) {
-        const fileName = slugify(colorScheme.name) + ".json";
-        const tokens = colorSchemeTokens(colorScheme);
-        const tokensJSON = JSON.stringify(tokens, null, 2);
-        const outPath = path.join(tokensDirectory, fileName);
-        fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 });
-        console.log(`- ${outPath} created`);
-    }
-
-    const themeIndexData = buildThemesIndex(colorSchemes);
-
-    const themesJSON = JSON.stringify(themeIndexData, null, 2);
-    fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 });
-    console.log(`- ${TOKENS_FILE} created`);
-
-    const tokenSetOrderData = buildTokenSetOrder(colorSchemes);
-
-    const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2);
-    fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 });
-    console.log(`- ${METADATA_FILE} created`);
-}
-
-const colorSchemes: ColorScheme[] = themes.map((theme) =>
-    createColorScheme(theme)
-);
-
-writeTokens(colorSchemes, TOKENS_DIRECTORY);

styles/src/build_licenses.ts πŸ”—

@@ -0,0 +1,50 @@
+import * as fs from "fs"
+import toml from "toml"
+import { themes } from "./themes"
+import { ThemeConfig } from "./common"
+
+const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
+
+// Use the cargo-about configuration file as the source of truth for supported licenses.
+function parse_accepted_toml(file: string): string[] {
+    const buffer = fs.readFileSync(file).toString()
+
+    const obj = toml.parse(buffer)
+
+    if (!Array.isArray(obj.accepted)) {
+        throw Error("Accepted license source is malformed")
+    }
+
+    return obj.accepted
+}
+
+function check_licenses(themes: ThemeConfig[]) {
+    for (const theme of themes) {
+        if (!theme.license_file) {
+            throw Error(`Theme ${theme.name} should have a LICENSE file`)
+        }
+    }
+}
+
+function generate_license_file(themes: ThemeConfig[]) {
+    check_licenses(themes)
+    for (const theme of themes) {
+        const license_text = fs.readFileSync(theme.license_file).toString()
+        write_license(theme.name, license_text, theme.license_url)
+    }
+}
+
+function write_license(
+    theme_name: string,
+    license_text: string,
+    license_url?: string
+) {
+    process.stdout.write(
+        license_url
+            ? `## [${theme_name}](${license_url})\n\n${license_text}\n********************************************************************************\n\n`
+            : `## ${theme_name}\n\n${license_text}\n********************************************************************************\n\n`
+    )
+}
+
+const accepted_licenses = parse_accepted_toml(ACCEPTED_LICENSES_FILE)
+generate_license_file(themes)

styles/src/build_themes.ts πŸ”—

@@ -0,0 +1,47 @@
+import * as fs from "fs"
+import { tmpdir } from "os"
+import * as path from "path"
+import app from "./style_tree/app"
+import { Theme, create_theme } from "./theme/create_theme"
+import { themes } from "./themes"
+import { useThemeStore } from "./theme"
+
+const assets_directory = `${__dirname}/../../assets`
+const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
+
+function clear_themes(theme_directory: string) {
+    if (!fs.existsSync(theme_directory)) {
+        fs.mkdirSync(theme_directory, { recursive: true })
+    } else {
+        for (const file of fs.readdirSync(theme_directory)) {
+            if (file.endsWith(".json")) {
+                fs.unlinkSync(path.join(theme_directory, file))
+            }
+        }
+    }
+}
+
+const all_themes: Theme[] = themes.map((theme) =>
+    create_theme(theme)
+)
+
+function write_themes(themes: Theme[], output_directory: string) {
+    clear_themes(output_directory)
+    for (const theme of themes) {
+        const { setTheme } = useThemeStore.getState()
+        setTheme(theme)
+
+        const style_tree = app()
+        const style_tree_json = JSON.stringify(style_tree, null, 2)
+        const temp_path = path.join(temp_directory, `${theme.name}.json`)
+        const out_path = path.join(
+            output_directory,
+            `${theme.name}.json`
+        )
+        fs.writeFileSync(temp_path, style_tree_json)
+        fs.renameSync(temp_path, out_path)
+        console.log(`- ${out_path} created`)
+    }
+}
+
+write_themes(all_themes, `${assets_directory}/themes`)

styles/src/build_tokens.ts πŸ”—

@@ -0,0 +1,90 @@
+import * as fs from "fs"
+import * as path from "path"
+import { Theme, create_theme, useThemeStore } from "./common"
+import { themes } from "./themes"
+import { slugify } from "./utils/slugify"
+import { theme_tokens } from "./theme/tokens/theme"
+
+const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
+const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
+const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
+
+function clear_tokens(tokens_directory: string) {
+    if (!fs.existsSync(tokens_directory)) {
+        fs.mkdirSync(tokens_directory, { recursive: true })
+    } else {
+        for (const file of fs.readdirSync(tokens_directory)) {
+            if (file.endsWith(".json")) {
+                fs.unlinkSync(path.join(tokens_directory, file))
+            }
+        }
+    }
+}
+
+type TokenSet = {
+    id: string
+    name: string
+    selected_token_sets: { [key: string]: "enabled" }
+}
+
+function build_token_set_order(theme: Theme[]): {
+    token_set_order: string[]
+} {
+    const token_set_order: string[] = theme.map((scheme) =>
+        scheme.name.toLowerCase().replace(/\s+/g, "_")
+    )
+    return { token_set_order }
+}
+
+function build_themes_index(theme: Theme[]): TokenSet[] {
+    const themes_index: TokenSet[] = theme.map((scheme, index) => {
+        const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
+            .toLowerCase()
+            .replace(/\s+/g, "_")}_${index}`
+        const selected_token_sets: { [key: string]: "enabled" } = {}
+        const token_set = scheme.name.toLowerCase().replace(/\s+/g, "_")
+        selected_token_sets[token_set] = "enabled"
+
+        return {
+            id,
+            name: `${scheme.name} - ${scheme.is_light ? "Light" : "Dark"}`,
+            selected_token_sets,
+        }
+    })
+
+    return themes_index
+}
+
+function write_tokens(themes: Theme[], tokens_directory: string) {
+    clear_tokens(tokens_directory)
+
+    for (const theme of themes) {
+        const { setTheme } = useThemeStore.getState()
+        setTheme(theme)
+
+        const file_name = slugify(theme.name) + ".json"
+        const tokens = theme_tokens()
+        const tokens_json = JSON.stringify(tokens, null, 2)
+        const out_path = path.join(tokens_directory, file_name)
+        fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
+        console.log(`- ${out_path} created`)
+    }
+
+    const theme_index_data = build_themes_index(themes)
+
+    const themes_json = JSON.stringify(theme_index_data, null, 2)
+    fs.writeFileSync(TOKENS_FILE, themes_json, { mode: 0o644 })
+    console.log(`- ${TOKENS_FILE} created`)
+
+    const token_set_order_data = build_token_set_order(themes)
+
+    const metadata_json = JSON.stringify(token_set_order_data, null, 2)
+    fs.writeFileSync(METADATA_FILE, metadata_json, { mode: 0o644 })
+    console.log(`- ${METADATA_FILE} created`)
+}
+
+const all_themes: Theme[] = themes.map((theme) =>
+    create_theme(theme)
+)
+
+write_tokens(all_themes, TOKENS_DIRECTORY)

styles/src/build_types.ts πŸ”—

@@ -0,0 +1,62 @@
+import * as fs from "fs/promises"
+import * as fsSync from "fs"
+import * as path from "path"
+import { compile } from "json-schema-to-typescript"
+
+const BANNER = `/*
+* This file is autogenerated
+*/\n\n`
+const dirname = __dirname
+
+async function main() {
+    const schemas_path = path.join(dirname, "../../", "crates/theme/schemas")
+    const schema_files = (await fs.readdir(schemas_path)).filter((x) =>
+        x.endsWith(".json")
+    )
+
+    const compiled_types = new Set()
+
+    for (const filename of schema_files) {
+        const file_path = path.join(schemas_path, filename)
+        const file_contents = await fs.readFile(file_path)
+        const schema = JSON.parse(file_contents.toString())
+        const compiled = await compile(schema, schema.title, {
+            bannerComment: "",
+        })
+        const each_type = compiled.split("export")
+        for (const type of each_type) {
+            if (!type) {
+                continue
+            }
+            compiled_types.add("export " + type.trim())
+        }
+    }
+
+    const output = BANNER + Array.from(compiled_types).join("\n\n")
+    const output_path = path.join(dirname, "../../styles/src/types/zed.ts")
+
+    try {
+        const existing = await fs.readFile(output_path)
+        if (existing.toString() == output) {
+            // Skip writing if it hasn't changed
+            console.log("Schemas are up to date")
+            return
+        }
+    } catch (e) {
+        if (e.code !== "ENOENT") {
+            throw e
+        }
+    }
+
+    const types_dic = path.dirname(output_path)
+    if (!fsSync.existsSync(types_dic)) {
+        await fs.mkdir(types_dic)
+    }
+    await fs.writeFile(output_path, output)
+    console.log(`Wrote Typescript types to ${output_path}`)
+}
+
+main().catch((e) => {
+    console.error(e)
+    process.exit(1)
+})

styles/src/common.ts πŸ”—

@@ -2,42 +2,24 @@ import chroma from "chroma-js"
 export * from "./theme"
 export { chroma }
 
-export const fontFamilies = {
+export const font_families = {
     sans: "Zed Sans",
     mono: "Zed Mono",
 }
 
-export const fontSizes = {
-    "3xs": 8,
+export const font_sizes = {
     "2xs": 10,
     xs: 12,
     sm: 14,
     md: 16,
     lg: 18,
-    xl: 20,
 }
 
-export type FontWeight =
-    | "thin"
-    | "extra_light"
-    | "light"
-    | "normal"
-    | "medium"
-    | "semibold"
-    | "bold"
-    | "extra_bold"
-    | "black"
+export type FontWeight = "normal" | "bold"
 
-export const fontWeights: { [key: string]: FontWeight } = {
-    thin: "thin",
-    extra_light: "extra_light",
-    light: "light",
+export const font_weights: { [key: string]: FontWeight } = {
     normal: "normal",
-    medium: "medium",
-    semibold: "semibold",
     bold: "bold",
-    extra_bold: "extra_bold",
-    black: "black",
 }
 
 export const sizes = {

styles/src/component/icon_button.ts πŸ”—

@@ -0,0 +1,85 @@
+import { interactive, toggleable } from "../element"
+import { background, foreground } from "../style_tree/components"
+import { useTheme, Theme } from "../theme"
+
+export type Margin = {
+    top: number
+    bottom: number
+    left: number
+    right: number
+}
+
+interface IconButtonOptions {
+    layer?:
+    | Theme["lowest"]
+    | Theme["middle"]
+    | Theme["highest"]
+    color?: keyof Theme["lowest"]
+    margin?: Partial<Margin>
+}
+
+type ToggleableIconButtonOptions = IconButtonOptions & {
+    active_color?: keyof Theme["lowest"]
+}
+
+export function icon_button({ color, margin, layer }: IconButtonOptions) {
+    const theme = useTheme()
+
+    if (!color) color = "base"
+
+    const m = {
+        top: margin?.top ?? 0,
+        bottom: margin?.bottom ?? 0,
+        left: margin?.left ?? 0,
+        right: margin?.right ?? 0,
+    }
+
+    return interactive({
+        base: {
+            corner_radius: 6,
+            padding: {
+                top: 2,
+                bottom: 2,
+                left: 4,
+                right: 4,
+            },
+            margin: m,
+            icon_width: 14,
+            icon_height: 14,
+            button_width: 20,
+            button_height: 16,
+        },
+        state: {
+            default: {
+                background: background(layer ?? theme.lowest, color),
+                color: foreground(layer ?? theme.lowest, color),
+            },
+            hovered: {
+                background: background(layer ?? theme.lowest, color, "hovered"),
+                color: foreground(layer ?? theme.lowest, color, "hovered"),
+            },
+            clicked: {
+                background: background(layer ?? theme.lowest, color, "pressed"),
+                color: foreground(layer ?? theme.lowest, color, "pressed"),
+            },
+        },
+    })
+}
+
+export function toggleable_icon_button(
+    theme: Theme,
+    { color, active_color, margin }: ToggleableIconButtonOptions
+) {
+    if (!color) color = "base"
+
+    return toggleable({
+        state: {
+            inactive: icon_button({ color, margin }),
+            active: icon_button({
+                color: active_color ? active_color : color,
+                margin,
+                layer: theme.middle,
+            }),
+        },
+    })
+}

styles/src/component/text_button.ts πŸ”—

@@ -0,0 +1,93 @@
+import { interactive, toggleable } from "../element"
+import {
+    TextProperties,
+    background,
+    foreground,
+    text,
+} from "../style_tree/components"
+import { useTheme, Theme } from "../theme"
+import { Margin } from "./icon_button"
+
+interface TextButtonOptions {
+    layer?:
+    | Theme["lowest"]
+    | Theme["middle"]
+    | Theme["highest"]
+    color?: keyof Theme["lowest"]
+    margin?: Partial<Margin>
+    text_properties?: TextProperties
+}
+
+type ToggleableTextButtonOptions = TextButtonOptions & {
+    active_color?: keyof Theme["lowest"]
+}
+
+export function text_button({
+    color,
+    layer,
+    margin,
+    text_properties,
+}: TextButtonOptions) {
+    const theme = useTheme()
+    if (!color) color = "base"
+
+    const text_options: TextProperties = {
+        size: "xs",
+        weight: "normal",
+        ...text_properties,
+    }
+
+    const m = {
+        top: margin?.top ?? 0,
+        bottom: margin?.bottom ?? 0,
+        left: margin?.left ?? 0,
+        right: margin?.right ?? 0,
+    }
+
+    return interactive({
+        base: {
+            corner_radius: 6,
+            padding: {
+                top: 1,
+                bottom: 1,
+                left: 6,
+                right: 6,
+            },
+            margin: m,
+            button_height: 22,
+            ...text(layer ?? theme.lowest, "sans", color, text_options),
+        },
+        state: {
+            default: {
+                background: background(layer ?? theme.lowest, color),
+                color: foreground(layer ?? theme.lowest, color),
+            },
+            hovered: {
+                background: background(layer ?? theme.lowest, color, "hovered"),
+                color: foreground(layer ?? theme.lowest, color, "hovered"),
+            },
+            clicked: {
+                background: background(layer ?? theme.lowest, color, "pressed"),
+                color: foreground(layer ?? theme.lowest, color, "pressed"),
+            },
+        },
+    })
+}
+
+export function toggleable_text_button(
+    theme: Theme,
+    { color, active_color, margin }: ToggleableTextButtonOptions
+) {
+    if (!color) color = "base"
+
+    return toggleable({
+        state: {
+            inactive: text_button({ color, margin }),
+            active: text_button({
+                color: active_color ? active_color : color,
+                margin,
+                layer: theme.middle,
+            }),
+        },
+    })
+}

styles/src/element/index.ts πŸ”—

@@ -0,0 +1,4 @@
+import { interactive, Interactive } from "./interactive"
+import { toggleable } from "./toggle"
+
+export { interactive, Interactive, toggleable }

styles/src/element/interactive.test.ts πŸ”—

@@ -0,0 +1,56 @@
+import {
+    NOT_ENOUGH_STATES_ERROR,
+    NO_DEFAULT_OR_BASE_ERROR,
+    interactive,
+} from "./interactive"
+import { describe, it, expect } from "vitest"
+
+describe("interactive", () => {
+    it("creates an Interactive<Element> with base properties and states", () => {
+        const result = interactive({
+            base: { font_size: 10, color: "#FFFFFF" },
+            state: {
+                hovered: { color: "#EEEEEE" },
+                clicked: { color: "#CCCCCC" },
+            },
+        })
+
+        expect(result).toEqual({
+            default: { color: "#FFFFFF", font_size: 10 },
+            hovered: { color: "#EEEEEE", font_size: 10 },
+            clicked: { color: "#CCCCCC", font_size: 10 },
+        })
+    })
+
+    it("creates an Interactive<Element> with no base properties", () => {
+        const result = interactive({
+            state: {
+                default: { color: "#FFFFFF", font_size: 10 },
+                hovered: { color: "#EEEEEE" },
+                clicked: { color: "#CCCCCC" },
+            },
+        })
+
+        expect(result).toEqual({
+            default: { color: "#FFFFFF", font_size: 10 },
+            hovered: { color: "#EEEEEE", font_size: 10 },
+            clicked: { color: "#CCCCCC", font_size: 10 },
+        })
+    })
+
+    it("throws error when both default and base are missing", () => {
+        const state = {
+            hovered: { color: "blue" },
+        }
+
+        expect(() => interactive({ state })).toThrow(NO_DEFAULT_OR_BASE_ERROR)
+    })
+
+    it("throws error when no other state besides default is present", () => {
+        const state = {
+            default: { font_size: 10 },
+        }
+
+        expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR)
+    })
+})

styles/src/element/interactive.ts πŸ”—

@@ -0,0 +1,97 @@
+import merge from "ts-deepmerge"
+import { DeepPartial } from "utility-types"
+
+export type InteractiveState =
+    | "default"
+    | "hovered"
+    | "clicked"
+    | "selected"
+    | "disabled"
+
+export type Interactive<T> = {
+    default: T
+    hovered?: T
+    clicked?: T
+    selected?: T
+    disabled?: T
+}
+
+export const NO_DEFAULT_OR_BASE_ERROR =
+    "An interactive object must have a default state, or a base property."
+export const NOT_ENOUGH_STATES_ERROR =
+    "An interactive object must have a default and at least one other state."
+
+interface InteractiveProps<T> {
+    base?: T
+    state: Partial<Record<InteractiveState, DeepPartial<T>>>
+}
+
+/**
+ * Helper function for creating Interactive<T> objects that works with Toggle<T>-like behavior.
+ * It takes a default object to be used as the value for `default` field and fills out other fields
+ * with fields from either `base` or from the `state` object which contains values for specific states.
+ * Notably, it does not touch `hover`, `clicked`, `selected` and `disabled` states if there are no modifications for them.
+ *
+ * @param defaultObj Object to be used as the value for the `default` field.
+ * @param base Optional object containing base fields to be included in the resulting object.
+ * @param state Object containing optional modified fields to be included in the resulting object for each state.
+ * @returns Interactive<T> object with fields from `base` and `state`.
+ */
+export function interactive<T extends object>({
+    base,
+    state,
+}: InteractiveProps<T>): Interactive<T> {
+    if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
+
+    let default_state: T
+
+    if (state.default && base) {
+        default_state = merge(base, state.default) as T
+    } else {
+        default_state = base ? base : (state.default as T)
+    }
+
+    const interactive_obj: Interactive<T> = {
+        default: default_state,
+    }
+
+    let state_count = 0
+
+    if (state.hovered !== undefined) {
+        interactive_obj.hovered = merge(
+            interactive_obj.default,
+            state.hovered
+        ) as T
+        state_count++
+    }
+
+    if (state.clicked !== undefined) {
+        interactive_obj.clicked = merge(
+            interactive_obj.default,
+            state.clicked
+        ) as T
+        state_count++
+    }
+
+    if (state.selected !== undefined) {
+        interactive_obj.selected = merge(
+            interactive_obj.default,
+            state.selected
+        ) as T
+        state_count++
+    }
+
+    if (state.disabled !== undefined) {
+        interactive_obj.disabled = merge(
+            interactive_obj.default,
+            state.disabled
+        ) as T
+        state_count++
+    }
+
+    if (state_count < 1) {
+        throw new Error(NOT_ENOUGH_STATES_ERROR)
+    }
+
+    return interactive_obj
+}

styles/src/element/toggle.test.ts πŸ”—

@@ -0,0 +1,52 @@
+import {
+    NO_ACTIVE_ERROR,
+    NO_INACTIVE_OR_BASE_ERROR,
+    toggleable,
+} from "./toggle"
+import { describe, it, expect } from "vitest"
+
+describe("toggleable", () => {
+    it("creates a Toggleable<Element> with base properties and states", () => {
+        const result = toggleable({
+            base: { background: "#000000", color: "#CCCCCC" },
+            state: {
+                active: { color: "#FFFFFF" },
+            },
+        })
+
+        expect(result).toEqual({
+            inactive: { background: "#000000", color: "#CCCCCC" },
+            active: { background: "#000000", color: "#FFFFFF" },
+        })
+    })
+
+    it("creates a Toggleable<Element> with no base properties", () => {
+        const result = toggleable({
+            state: {
+                inactive: { background: "#000000", color: "#CCCCCC" },
+                active: { background: "#000000", color: "#FFFFFF" },
+            },
+        })
+
+        expect(result).toEqual({
+            inactive: { background: "#000000", color: "#CCCCCC" },
+            active: { background: "#000000", color: "#FFFFFF" },
+        })
+    })
+
+    it("throws error when both inactive and base are missing", () => {
+        const state = {
+            active: { background: "#000000", color: "#FFFFFF" },
+        }
+
+        expect(() => toggleable({ state })).toThrow(NO_INACTIVE_OR_BASE_ERROR)
+    })
+
+    it("throws error when no active state is present", () => {
+        const state = {
+            inactive: { background: "#000000", color: "#CCCCCC" },
+        }
+
+        expect(() => toggleable({ state })).toThrow(NO_ACTIVE_ERROR)
+    })
+})

styles/src/element/toggle.ts πŸ”—

@@ -0,0 +1,47 @@
+import merge from "ts-deepmerge"
+import { DeepPartial } from "utility-types"
+
+type ToggleState = "inactive" | "active"
+
+type Toggleable<T> = Record<ToggleState, T>
+
+export const NO_INACTIVE_OR_BASE_ERROR =
+    "A toggleable object must have an inactive state, or a base property."
+export const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
+
+interface ToggleableProps<T> {
+    base?: T
+    state: Partial<Record<ToggleState, DeepPartial<T>>>
+}
+
+/**
+ * Helper function for creating Toggleable objects.
+ * @template T The type of the object being toggled.
+ * @param props Object containing the base (inactive) state and state modifications to create the active state.
+ * @returns A Toggleable object containing both the inactive and active states.
+ * @example
+ * ```
+ * toggleable({
+ *   base: { background: "#000000", text: "#CCCCCC" },
+ *   state: { active: { text: "#CCCCCC" } },
+ * })
+ * ```
+ */
+export function toggleable<T extends object>(
+    props: ToggleableProps<T>
+): Toggleable<T> {
+    const { base, state } = props
+
+    if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
+    if (!state.active) throw new Error(NO_ACTIVE_ERROR)
+
+    const inactive_state = base
+        ? ((state.inactive ? merge(base, state.inactive) : base) as T)
+        : (state.inactive as T)
+
+    const toggle_obj: Toggleable<T> = {
+        inactive: inactive_state,
+        active: merge(base ?? {}, state.active) as T,
+    }
+    return toggle_obj
+}

styles/src/styleTree/app.ts πŸ”—

@@ -1,74 +0,0 @@
-import { text } from "./components"
-import contactFinder from "./contactFinder"
-import contactsPopover from "./contactsPopover"
-import commandPalette from "./commandPalette"
-import editor from "./editor"
-import projectPanel from "./projectPanel"
-import search from "./search"
-import picker from "./picker"
-import workspace from "./workspace"
-import contextMenu from "./contextMenu"
-import sharedScreen from "./sharedScreen"
-import projectDiagnostics from "./projectDiagnostics"
-import contactNotification from "./contactNotification"
-import updateNotification from "./updateNotification"
-import simpleMessageNotification from "./simpleMessageNotification"
-import projectSharedNotification from "./projectSharedNotification"
-import tooltip from "./tooltip"
-import terminal from "./terminal"
-import contactList from "./contactList"
-import toolbarDropdownMenu from "./toolbarDropdownMenu"
-import incomingCallNotification from "./incomingCallNotification"
-import { ColorScheme } from "../theme/colorScheme"
-import feedback from "./feedback"
-import welcome from "./welcome"
-import copilot from "./copilot"
-import assistant from "./assistant"
-
-export default function app(colorScheme: ColorScheme): Object {
-    return {
-        meta: {
-            name: colorScheme.name,
-            isLight: colorScheme.isLight,
-        },
-        commandPalette: commandPalette(colorScheme),
-        contactNotification: contactNotification(colorScheme),
-        projectSharedNotification: projectSharedNotification(colorScheme),
-        incomingCallNotification: incomingCallNotification(colorScheme),
-        picker: picker(colorScheme),
-        workspace: workspace(colorScheme),
-        copilot: copilot(colorScheme),
-        welcome: welcome(colorScheme),
-        contextMenu: contextMenu(colorScheme),
-        editor: editor(colorScheme),
-        projectDiagnostics: projectDiagnostics(colorScheme),
-        projectPanel: projectPanel(colorScheme),
-        contactsPopover: contactsPopover(colorScheme),
-        contactFinder: contactFinder(colorScheme),
-        contactList: contactList(colorScheme),
-        toolbarDropdownMenu: toolbarDropdownMenu(colorScheme),
-        search: search(colorScheme),
-        sharedScreen: sharedScreen(colorScheme),
-        updateNotification: updateNotification(colorScheme),
-        simpleMessageNotification: simpleMessageNotification(colorScheme),
-        tooltip: tooltip(colorScheme),
-        terminal: terminal(colorScheme),
-        assistant: assistant(colorScheme),
-        feedback: feedback(colorScheme),
-        colorScheme: {
-            ...colorScheme,
-            players: Object.values(colorScheme.players),
-            ramps: {
-                neutral: colorScheme.ramps.neutral.colors(100, "hex"),
-                red: colorScheme.ramps.red.colors(100, "hex"),
-                orange: colorScheme.ramps.orange.colors(100, "hex"),
-                yellow: colorScheme.ramps.yellow.colors(100, "hex"),
-                green: colorScheme.ramps.green.colors(100, "hex"),
-                cyan: colorScheme.ramps.cyan.colors(100, "hex"),
-                blue: colorScheme.ramps.blue.colors(100, "hex"),
-                violet: colorScheme.ramps.violet.colors(100, "hex"),
-                magenta: colorScheme.ramps.magenta.colors(100, "hex"),
-            },
-        },
-    }
-}

styles/src/styleTree/assistant.ts πŸ”—

@@ -1,85 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { text, border, background, foreground } from "./components"
-import editor from "./editor"
-
-export default function assistant(colorScheme: ColorScheme) {
-    const layer = colorScheme.highest
-    return {
-        container: {
-            background: editor(colorScheme).background,
-            padding: { left: 12 },
-        },
-        header: {
-            border: border(layer, "default", { bottom: true, top: true }),
-            margin: { bottom: 6, top: 6 },
-            background: editor(colorScheme).background,
-        },
-        userSender: {
-            ...text(layer, "sans", "default", { size: "sm", weight: "bold" }),
-        },
-        assistantSender: {
-            ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }),
-        },
-        systemSender: {
-            ...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
-        },
-        sentAt: {
-            margin: { top: 2, left: 8 },
-            ...text(layer, "sans", "default", { size: "2xs" }),
-        },
-        modelInfoContainer: {
-            margin: { right: 16, top: 4 },
-        },
-        model: {
-            background: background(layer, "on"),
-            border: border(layer, "on", { overlay: true }),
-            padding: 4,
-            cornerRadius: 4,
-            ...text(layer, "sans", "default", { size: "xs" }),
-            hover: {
-                background: background(layer, "on", "hovered"),
-            },
-        },
-        remainingTokens: {
-            background: background(layer, "on"),
-            border: border(layer, "on", { overlay: true }),
-            padding: 4,
-            margin: { left: 4 },
-            cornerRadius: 4,
-            ...text(layer, "sans", "positive", { size: "xs" }),
-        },
-        noRemainingTokens: {
-            background: background(layer, "on"),
-            border: border(layer, "on", { overlay: true }),
-            padding: 4,
-            margin: { left: 4 },
-            cornerRadius: 4,
-            ...text(layer, "sans", "negative", { size: "xs" }),
-        },
-        errorIcon: {
-            margin: { left: 8 },
-            color: foreground(layer, "negative"),
-            width: 12,
-        },
-        apiKeyEditor: {
-            background: background(layer, "on"),
-            cornerRadius: 6,
-            text: text(layer, "mono", "on"),
-            placeholderText: text(layer, "mono", "on", "disabled", {
-                size: "xs",
-            }),
-            selection: colorScheme.players[0],
-            border: border(layer, "on"),
-            padding: {
-                bottom: 4,
-                left: 8,
-                right: 8,
-                top: 4,
-            },
-        },
-        apiKeyPrompt: {
-            padding: 10,
-            ...text(layer, "sans", "default", { size: "xs" }),
-        },
-    }
-}

styles/src/styleTree/commandPalette.ts πŸ”—

@@ -1,30 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import { text, background } from "./components"
-
-export default function commandPalette(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-    return {
-        keystrokeSpacing: 8,
-        key: {
-            text: text(layer, "mono", "variant", "default", { size: "xs" }),
-            cornerRadius: 2,
-            background: background(layer, "on"),
-            padding: {
-                top: 1,
-                bottom: 1,
-                left: 6,
-                right: 6,
-            },
-            margin: {
-                top: 1,
-                bottom: 1,
-                left: 2,
-            },
-            active: {
-                text: text(layer, "mono", "on", "default", { size: "xs" }),
-                background: withOpacity(background(layer, "on"), 0.2),
-            },
-        },
-    }
-}

styles/src/styleTree/contactFinder.ts πŸ”—

@@ -1,70 +0,0 @@
-import picker from "./picker"
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-
-export default function contactFinder(colorScheme: ColorScheme): any {
-    let layer = colorScheme.middle
-
-    const sideMargin = 6
-    const contactButton = {
-        background: background(layer, "variant"),
-        color: foreground(layer, "variant"),
-        iconWidth: 8,
-        buttonWidth: 16,
-        cornerRadius: 8,
-    }
-
-    const pickerStyle = picker(colorScheme)
-    const pickerInput = {
-        background: background(layer, "on"),
-        cornerRadius: 6,
-        text: text(layer, "mono"),
-        placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
-        selection: colorScheme.players[0],
-        border: border(layer),
-        padding: {
-            bottom: 4,
-            left: 8,
-            right: 8,
-            top: 4,
-        },
-        margin: {
-            left: sideMargin,
-            right: sideMargin,
-        },
-    }
-
-    return {
-        picker: {
-            emptyContainer: {},
-            item: {
-                ...pickerStyle.item,
-                margin: { left: sideMargin, right: sideMargin },
-            },
-            noMatches: pickerStyle.noMatches,
-            inputEditor: pickerInput,
-            emptyInputEditor: pickerInput,
-        },
-        rowHeight: 28,
-        contactAvatar: {
-            cornerRadius: 10,
-            width: 18,
-        },
-        contactUsername: {
-            padding: {
-                left: 8,
-            },
-        },
-        contactButton: {
-            ...contactButton,
-            hover: {
-                background: background(layer, "variant", "hovered"),
-            },
-        },
-        disabledContactButton: {
-            ...contactButton,
-            background: background(layer, "disabled"),
-            color: foreground(layer, "disabled"),
-        },
-    }
-}

styles/src/styleTree/contactList.ts πŸ”—

@@ -1,182 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, borderColor, foreground, text } from "./components"
-
-export default function contactsPanel(colorScheme: ColorScheme) {
-    const nameMargin = 8
-    const sidePadding = 12
-
-    let layer = colorScheme.middle
-
-    const contactButton = {
-        background: background(layer, "on"),
-        color: foreground(layer, "on"),
-        iconWidth: 8,
-        buttonWidth: 16,
-        cornerRadius: 8,
-    }
-    const projectRow = {
-        guestAvatarSpacing: 4,
-        height: 24,
-        guestAvatar: {
-            cornerRadius: 8,
-            width: 14,
-        },
-        name: {
-            ...text(layer, "mono", { size: "sm" }),
-            margin: {
-                left: nameMargin,
-                right: 6,
-            },
-        },
-        guests: {
-            margin: {
-                left: nameMargin,
-                right: nameMargin,
-            },
-        },
-        padding: {
-            left: sidePadding,
-            right: sidePadding,
-        },
-    }
-
-    return {
-        background: background(layer),
-        padding: { top: 12 },
-        userQueryEditor: {
-            background: background(layer, "on"),
-            cornerRadius: 6,
-            text: text(layer, "mono", "on"),
-            placeholderText: text(layer, "mono", "on", "disabled", {
-                size: "xs",
-            }),
-            selection: colorScheme.players[0],
-            border: border(layer, "on"),
-            padding: {
-                bottom: 4,
-                left: 8,
-                right: 8,
-                top: 4,
-            },
-            margin: {
-                left: 6,
-            },
-        },
-        userQueryEditorHeight: 33,
-        addContactButton: {
-            margin: { left: 6, right: 12 },
-            color: foreground(layer, "on"),
-            buttonWidth: 28,
-            iconWidth: 16,
-        },
-        rowHeight: 28,
-        sectionIconSize: 8,
-        headerRow: {
-            ...text(layer, "mono", { size: "sm" }),
-            margin: { top: 14 },
-            padding: {
-                left: sidePadding,
-                right: sidePadding,
-            },
-            active: {
-                ...text(layer, "mono", "active", { size: "sm" }),
-                background: background(layer, "active"),
-            },
-        },
-        leaveCall: {
-            background: background(layer),
-            border: border(layer),
-            cornerRadius: 6,
-            margin: {
-                top: 1,
-            },
-            padding: {
-                top: 1,
-                bottom: 1,
-                left: 7,
-                right: 7,
-            },
-            ...text(layer, "sans", "variant", { size: "xs" }),
-            hover: {
-                ...text(layer, "sans", "hovered", { size: "xs" }),
-                background: background(layer, "hovered"),
-                border: border(layer, "hovered"),
-            },
-        },
-        contactRow: {
-            padding: {
-                left: sidePadding,
-                right: sidePadding,
-            },
-            active: {
-                background: background(layer, "active"),
-            },
-        },
-        contactAvatar: {
-            cornerRadius: 10,
-            width: 18,
-        },
-        contactStatusFree: {
-            cornerRadius: 4,
-            padding: 4,
-            margin: { top: 12, left: 12 },
-            background: foreground(layer, "positive"),
-        },
-        contactStatusBusy: {
-            cornerRadius: 4,
-            padding: 4,
-            margin: { top: 12, left: 12 },
-            background: foreground(layer, "negative"),
-        },
-        contactUsername: {
-            ...text(layer, "mono", { size: "sm" }),
-            margin: {
-                left: nameMargin,
-            },
-        },
-        contactButtonSpacing: nameMargin,
-        contactButton: {
-            ...contactButton,
-            hover: {
-                background: background(layer, "hovered"),
-            },
-        },
-        disabledButton: {
-            ...contactButton,
-            background: background(layer, "on"),
-            color: foreground(layer, "on"),
-        },
-        callingIndicator: {
-            ...text(layer, "mono", "variant", { size: "xs" }),
-        },
-        treeBranch: {
-            color: borderColor(layer),
-            width: 1,
-            hover: {
-                color: borderColor(layer),
-            },
-            active: {
-                color: borderColor(layer),
-            },
-        },
-        projectRow: {
-            ...projectRow,
-            background: background(layer),
-            icon: {
-                margin: { left: nameMargin },
-                color: foreground(layer, "variant"),
-                width: 12,
-            },
-            name: {
-                ...projectRow.name,
-                ...text(layer, "mono", { size: "sm" }),
-            },
-            hover: {
-                background: background(layer, "hovered"),
-            },
-            active: {
-                background: background(layer, "active"),
-            },
-        },
-    }
-}

styles/src/styleTree/contactNotification.ts πŸ”—

@@ -1,45 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, foreground, text } from "./components"
-
-const avatarSize = 12
-const headerPadding = 8
-
-export default function contactNotification(colorScheme: ColorScheme): Object {
-    let layer = colorScheme.lowest
-    return {
-        headerAvatar: {
-            height: avatarSize,
-            width: avatarSize,
-            cornerRadius: 6,
-        },
-        headerMessage: {
-            ...text(layer, "sans", { size: "xs" }),
-            margin: { left: headerPadding, right: headerPadding },
-        },
-        headerHeight: 18,
-        bodyMessage: {
-            ...text(layer, "sans", { size: "xs" }),
-            margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
-        },
-        button: {
-            ...text(layer, "sans", "on", { size: "xs" }),
-            background: background(layer, "on"),
-            padding: 4,
-            cornerRadius: 6,
-            margin: { left: 6 },
-            hover: {
-                background: background(layer, "on", "hovered"),
-            },
-        },
-        dismissButton: {
-            color: foreground(layer, "variant"),
-            iconWidth: 8,
-            iconHeight: 8,
-            buttonWidth: 8,
-            buttonHeight: 8,
-            hover: {
-                color: foreground(layer, "hovered"),
-            },
-        },
-    }
-}

styles/src/styleTree/contactsPopover.ts πŸ”—

@@ -1,16 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function contactsPopover(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle
-    const sidePadding = 12
-    return {
-        background: background(layer),
-        cornerRadius: 6,
-        padding: { top: 6, bottom: 6 },
-        shadow: colorScheme.popoverShadow,
-        border: border(layer),
-        width: 300,
-        height: 400,
-    }
-}

styles/src/styleTree/contextMenu.ts πŸ”—

@@ -1,49 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, borderColor, text } from "./components"
-
-export default function contextMenu(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle
-    return {
-        background: background(layer),
-        cornerRadius: 10,
-        padding: 4,
-        shadow: colorScheme.popoverShadow,
-        border: border(layer),
-        keystrokeMargin: 30,
-        item: {
-            iconSpacing: 8,
-            iconWidth: 14,
-            padding: { left: 6, right: 6, top: 2, bottom: 2 },
-            cornerRadius: 6,
-            label: text(layer, "sans", { size: "sm" }),
-            keystroke: {
-                ...text(layer, "sans", "variant", {
-                    size: "sm",
-                    weight: "bold",
-                }),
-                padding: { left: 3, right: 3 },
-            },
-            hover: {
-                background: background(layer, "hovered"),
-                label: text(layer, "sans", "hovered", { size: "sm" }),
-                keystroke: {
-                    ...text(layer, "sans", "hovered", {
-                        size: "sm",
-                        weight: "bold",
-                    }),
-                    padding: { left: 3, right: 3 },
-                },
-            },
-            active: {
-                background: background(layer, "active"),
-            },
-            activeHover: {
-                background: background(layer, "active"),
-            },
-        },
-        separator: {
-            background: borderColor(layer),
-            margin: { top: 2, bottom: 2 },
-        },
-    }
-}

styles/src/styleTree/copilot.ts πŸ”—

@@ -1,267 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, svg, text } from "./components"
-
-export default function copilot(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle
-
-    let content_width = 264
-
-    let ctaButton = {
-        // Copied from welcome screen. FIXME: Move this into a ZDS component
-        background: background(layer),
-        border: border(layer, "default"),
-        cornerRadius: 4,
-        margin: {
-            top: 4,
-            bottom: 4,
-            left: 8,
-            right: 8,
-        },
-        padding: {
-            top: 3,
-            bottom: 3,
-            left: 7,
-            right: 7,
-        },
-        ...text(layer, "sans", "default", { size: "sm" }),
-        hover: {
-            ...text(layer, "sans", "default", { size: "sm" }),
-            background: background(layer, "hovered"),
-            border: border(layer, "active"),
-        },
-    }
-
-    return {
-        outLinkIcon: {
-            icon: svg(
-                foreground(layer, "variant"),
-                "icons/link_out_12.svg",
-                12,
-                12
-            ),
-            container: {
-                cornerRadius: 6,
-                padding: { left: 6 },
-            },
-            hover: {
-                icon: svg(
-                    foreground(layer, "hovered"),
-                    "icons/link_out_12.svg",
-                    12,
-                    12
-                ),
-            },
-        },
-        modal: {
-            titleText: {
-                ...text(layer, "sans", { size: "xs", weight: "bold" }),
-            },
-            titlebar: {
-                background: background(colorScheme.lowest),
-                border: border(layer, "active"),
-                padding: {
-                    top: 4,
-                    bottom: 4,
-                    left: 8,
-                    right: 8,
-                },
-            },
-            container: {
-                background: background(colorScheme.lowest),
-                padding: {
-                    top: 0,
-                    left: 0,
-                    right: 0,
-                    bottom: 8,
-                },
-            },
-            closeIcon: {
-                icon: svg(
-                    foreground(layer, "variant"),
-                    "icons/x_mark_8.svg",
-                    8,
-                    8
-                ),
-                container: {
-                    cornerRadius: 2,
-                    padding: {
-                        top: 4,
-                        bottom: 4,
-                        left: 4,
-                        right: 4,
-                    },
-                    margin: {
-                        right: 0,
-                    },
-                },
-                hover: {
-                    icon: svg(
-                        foreground(layer, "on"),
-                        "icons/x_mark_8.svg",
-                        8,
-                        8
-                    ),
-                },
-                clicked: {
-                    icon: svg(
-                        foreground(layer, "base"),
-                        "icons/x_mark_8.svg",
-                        8,
-                        8
-                    ),
-                },
-            },
-            dimensions: {
-                width: 280,
-                height: 280,
-            },
-        },
-
-        auth: {
-            content_width,
-
-            ctaButton,
-
-            header: {
-                icon: svg(
-                    foreground(layer, "default"),
-                    "icons/zed_plus_copilot_32.svg",
-                    92,
-                    32
-                ),
-                container: {
-                    margin: {
-                        top: 35,
-                        bottom: 5,
-                        left: 0,
-                        right: 0,
-                    },
-                },
-            },
-
-            prompting: {
-                subheading: {
-                    ...text(layer, "sans", { size: "xs" }),
-                    margin: {
-                        top: 6,
-                        bottom: 12,
-                        left: 0,
-                        right: 0,
-                    },
-                },
-
-                hint: {
-                    ...text(layer, "sans", { size: "xs", color: "#838994" }),
-                    margin: {
-                        top: 6,
-                        bottom: 2,
-                    },
-                },
-
-                deviceCode: {
-                    text: text(layer, "mono", { size: "sm" }),
-                    cta: {
-                        ...ctaButton,
-                        background: background(colorScheme.lowest),
-                        border: border(colorScheme.lowest, "inverted"),
-                        padding: {
-                            top: 0,
-                            bottom: 0,
-                            left: 16,
-                            right: 16,
-                        },
-                        margin: {
-                            left: 16,
-                            right: 16,
-                        },
-                    },
-                    left: content_width / 2,
-                    leftContainer: {
-                        padding: {
-                            top: 3,
-                            bottom: 3,
-                            left: 0,
-                            right: 6,
-                        },
-                    },
-                    right: (content_width * 1) / 3,
-                    rightContainer: {
-                        border: border(colorScheme.lowest, "inverted", {
-                            bottom: false,
-                            right: false,
-                            top: false,
-                            left: true,
-                        }),
-                        padding: {
-                            top: 3,
-                            bottom: 5,
-                            left: 8,
-                            right: 0,
-                        },
-                        hover: {
-                            border: border(layer, "active", {
-                                bottom: false,
-                                right: false,
-                                top: false,
-                                left: true,
-                            }),
-                        },
-                    },
-                },
-            },
-
-            notAuthorized: {
-                subheading: {
-                    ...text(layer, "sans", { size: "xs" }),
-
-                    margin: {
-                        top: 16,
-                        bottom: 16,
-                        left: 0,
-                        right: 0,
-                    },
-                },
-
-                warning: {
-                    ...text(layer, "sans", {
-                        size: "xs",
-                        color: foreground(layer, "warning"),
-                    }),
-                    border: border(layer, "warning"),
-                    background: background(layer, "warning"),
-                    cornerRadius: 2,
-                    padding: {
-                        top: 4,
-                        left: 4,
-                        bottom: 4,
-                        right: 4,
-                    },
-                    margin: {
-                        bottom: 16,
-                        left: 8,
-                        right: 8,
-                    },
-                },
-            },
-
-            authorized: {
-                subheading: {
-                    ...text(layer, "sans", { size: "xs" }),
-
-                    margin: {
-                        top: 16,
-                        bottom: 16,
-                    },
-                },
-
-                hint: {
-                    ...text(layer, "sans", { size: "xs", color: "#838994" }),
-                    margin: {
-                        top: 24,
-                        bottom: 4,
-                    },
-                },
-            },
-        },
-    }
-}

styles/src/styleTree/editor.ts πŸ”—

@@ -1,281 +0,0 @@
-import { withOpacity } from "../theme/color"
-import { ColorScheme, Layer, StyleSets } from "../theme/colorScheme"
-import { background, border, borderColor, foreground, text } from "./components"
-import hoverPopover from "./hoverPopover"
-
-import { buildSyntax } from "../theme/syntax"
-
-export default function editor(colorScheme: ColorScheme) {
-  const { isLight } = colorScheme
-
-  let layer = colorScheme.highest
-
-  const autocompleteItem = {
-    cornerRadius: 6,
-    padding: {
-      bottom: 2,
-      left: 6,
-      right: 6,
-      top: 2,
-    },
-  }
-
-  function diagnostic(layer: Layer, styleSet: StyleSets) {
-    return {
-      textScaleFactor: 0.857,
-      header: {
-        border: border(layer, {
-          top: true,
-        }),
-      },
-      message: {
-        text: text(layer, "sans", styleSet, "default", { size: "sm" }),
-        highlightText: text(layer, "sans", styleSet, "default", {
-          size: "sm",
-          weight: "bold",
-        }),
-      },
-    }
-  }
-
-  const syntax = buildSyntax(colorScheme)
-
-  return {
-    textColor: syntax.primary.color,
-    background: background(layer),
-    activeLineBackground: withOpacity(background(layer, "on"), 0.75),
-    highlightedLineBackground: background(layer, "on"),
-    // Inline autocomplete suggestions, Co-pilot suggestions, etc.
-    suggestion: syntax.predictive,
-    codeActions: {
-      indicator: {
-        color: foreground(layer, "variant"),
-
-        clicked: {
-          color: foreground(layer, "base"),
-        },
-        hover: {
-          color: foreground(layer, "on"),
-        },
-        active: {
-          color: foreground(layer, "on"),
-        },
-      },
-      verticalScale: 0.55,
-    },
-    folds: {
-      iconMarginScale: 2.5,
-      foldedIcon: "icons/chevron_right_8.svg",
-      foldableIcon: "icons/chevron_down_8.svg",
-      indicator: {
-        color: foreground(layer, "variant"),
-
-        clicked: {
-          color: foreground(layer, "base"),
-        },
-        hover: {
-          color: foreground(layer, "on"),
-        },
-        active: {
-          color: foreground(layer, "on"),
-        },
-      },
-      ellipses: {
-        textColor: colorScheme.ramps.neutral(0.71).hex(),
-        cornerRadiusFactor: 0.15,
-        background: {
-          // Copied from hover_popover highlight
-          color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
-
-          hover: {
-            color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
-          },
-
-          clicked: {
-            color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(),
-          },
-        },
-      },
-      foldBackground: foreground(layer, "variant"),
-    },
-    diff: {
-      deleted: isLight
-        ? colorScheme.ramps.red(0.5).hex()
-        : colorScheme.ramps.red(0.4).hex(),
-      modified: isLight
-        ? colorScheme.ramps.yellow(0.5).hex()
-        : colorScheme.ramps.yellow(0.5).hex(),
-      inserted: isLight
-        ? colorScheme.ramps.green(0.4).hex()
-        : colorScheme.ramps.green(0.5).hex(),
-      removedWidthEm: 0.275,
-      widthEm: 0.15,
-      cornerRadius: 0.05,
-    },
-    /** Highlights matching occurrences of what is under the cursor
-     * as well as matched brackets
-     */
-    documentHighlightReadBackground: withOpacity(
-      foreground(layer, "accent"),
-      0.1
-    ),
-    documentHighlightWriteBackground: colorScheme.ramps
-      .neutral(0.5)
-      .alpha(0.4)
-      .hex(), // TODO: This was blend * 2
-    errorColor: background(layer, "negative"),
-    gutterBackground: background(layer),
-    gutterPaddingFactor: 3.5,
-    lineNumber: withOpacity(foreground(layer), 0.35),
-    lineNumberActive: foreground(layer),
-    renameFade: 0.6,
-    unnecessaryCodeFade: 0.5,
-    selection: colorScheme.players[0],
-    whitespace: colorScheme.ramps.neutral(0.5).hex(),
-    guestSelections: [
-      colorScheme.players[1],
-      colorScheme.players[2],
-      colorScheme.players[3],
-      colorScheme.players[4],
-      colorScheme.players[5],
-      colorScheme.players[6],
-      colorScheme.players[7],
-    ],
-    autocomplete: {
-      background: background(colorScheme.middle),
-      cornerRadius: 8,
-      padding: 4,
-      margin: {
-        left: -14,
-      },
-      border: border(colorScheme.middle),
-      shadow: colorScheme.popoverShadow,
-      matchHighlight: foreground(colorScheme.middle, "accent"),
-      item: autocompleteItem,
-      hoveredItem: {
-        ...autocompleteItem,
-        matchHighlight: foreground(
-          colorScheme.middle,
-          "accent",
-          "hovered"
-        ),
-        background: background(colorScheme.middle, "hovered"),
-      },
-      selectedItem: {
-        ...autocompleteItem,
-        matchHighlight: foreground(
-          colorScheme.middle,
-          "accent",
-          "active"
-        ),
-        background: background(colorScheme.middle, "active"),
-      },
-    },
-    diagnosticHeader: {
-      background: background(colorScheme.middle),
-      iconWidthFactor: 1.5,
-      textScaleFactor: 0.857,
-      border: border(colorScheme.middle, {
-        bottom: true,
-        top: true,
-      }),
-      code: {
-        ...text(colorScheme.middle, "mono", { size: "sm" }),
-        margin: {
-          left: 10,
-        },
-      },
-      source: {
-        text: text(colorScheme.middle, "sans", {
-          size: "sm",
-          weight: "bold",
-        }),
-      },
-      message: {
-        highlightText: text(colorScheme.middle, "sans", {
-          size: "sm",
-          weight: "bold",
-        }),
-        text: text(colorScheme.middle, "sans", { size: "sm" }),
-      },
-    },
-    diagnosticPathHeader: {
-      background: background(colorScheme.middle),
-      textScaleFactor: 0.857,
-      filename: text(colorScheme.middle, "mono", { size: "sm" }),
-      path: {
-        ...text(colorScheme.middle, "mono", { size: "sm" }),
-        margin: {
-          left: 12,
-        },
-      },
-    },
-    errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
-    warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
-    informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
-    hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
-    invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
-    invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
-    invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
-    invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
-    hoverPopover: hoverPopover(colorScheme),
-    linkDefinition: {
-      color: syntax.linkUri.color,
-      underline: syntax.linkUri.underline,
-    },
-    jumpIcon: {
-      color: foreground(layer, "on"),
-      iconWidth: 20,
-      buttonWidth: 20,
-      cornerRadius: 6,
-      padding: {
-        top: 6,
-        bottom: 6,
-        left: 6,
-        right: 6,
-      },
-      hover: {
-        background: background(layer, "on", "hovered"),
-      },
-    },
-    scrollbar: {
-      width: 12,
-      minHeightFactor: 1.0,
-      track: {
-        border: border(layer, "variant", { left: true }),
-      },
-      thumb: {
-        background: withOpacity(background(layer, "inverted"), 0.3),
-        border: {
-          width: 1,
-          color: borderColor(layer, "variant"),
-          top: false,
-          right: true,
-          left: true,
-          bottom: false,
-        },
-      },
-      git: {
-        deleted: isLight
-          ? withOpacity(colorScheme.ramps.red(0.5).hex(), 0.8)
-          : withOpacity(colorScheme.ramps.red(0.4).hex(), 0.8),
-        modified: isLight
-          ? withOpacity(colorScheme.ramps.yellow(0.5).hex(), 0.8)
-          : withOpacity(colorScheme.ramps.yellow(0.4).hex(), 0.8),
-        inserted: isLight
-          ? withOpacity(colorScheme.ramps.green(0.5).hex(), 0.8)
-          : withOpacity(colorScheme.ramps.green(0.4).hex(), 0.8),
-      },
-      selections: isLight
-        ? withOpacity(colorScheme.ramps.blue(0.5).hex(), 0.8)
-        : withOpacity(colorScheme.ramps.blue(0.4).hex(), 0.8)
-    },
-    compositionMark: {
-      underline: {
-        thickness: 1.0,
-        color: borderColor(layer),
-      },
-    },
-    syntax,
-  }
-}

styles/src/styleTree/feedback.ts πŸ”—

@@ -1,44 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function feedback(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-
-    return {
-        submit_button: {
-            ...text(layer, "mono", "on"),
-            background: background(layer, "on"),
-            cornerRadius: 6,
-            border: border(layer, "on"),
-            margin: {
-                right: 4,
-            },
-            padding: {
-                bottom: 2,
-                left: 10,
-                right: 10,
-                top: 2,
-            },
-            clicked: {
-                ...text(layer, "mono", "on", "pressed"),
-                background: background(layer, "on", "pressed"),
-                border: border(layer, "on", "pressed"),
-            },
-            hover: {
-                ...text(layer, "mono", "on", "hovered"),
-                background: background(layer, "on", "hovered"),
-                border: border(layer, "on", "hovered"),
-            },
-        },
-        button_margin: 8,
-        info_text_default: text(layer, "sans", "default", { size: "xs" }),
-        link_text_default: text(layer, "sans", "default", {
-            size: "xs",
-            underline: true,
-        }),
-        link_text_hover: text(layer, "sans", "hovered", {
-            size: "xs",
-            underline: true,
-        }),
-    }
-}

styles/src/styleTree/hoverPopover.ts πŸ”—

@@ -1,46 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-
-export default function HoverPopover(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle
-    let baseContainer = {
-        background: background(layer),
-        cornerRadius: 8,
-        padding: {
-            left: 8,
-            right: 8,
-            top: 4,
-            bottom: 4,
-        },
-        shadow: colorScheme.popoverShadow,
-        border: border(layer),
-        margin: {
-            left: -8,
-        },
-    }
-
-    return {
-        container: baseContainer,
-        infoContainer: {
-            ...baseContainer,
-            background: background(layer, "accent"),
-            border: border(layer, "accent"),
-        },
-        warningContainer: {
-            ...baseContainer,
-            background: background(layer, "warning"),
-            border: border(layer, "warning"),
-        },
-        errorContainer: {
-            ...baseContainer,
-            background: background(layer, "negative"),
-            border: border(layer, "negative"),
-        },
-        blockStyle: {
-            padding: { top: 4 },
-        },
-        prose: text(layer, "sans", { size: "sm" }),
-        diagnosticSourceHighlight: { color: foreground(layer, "accent") },
-        highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
-    }
-}

styles/src/styleTree/incomingCallNotification.ts πŸ”—

@@ -1,53 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function incomingCallNotification(
-    colorScheme: ColorScheme
-): Object {
-    let layer = colorScheme.middle
-    const avatarSize = 48
-    return {
-        windowHeight: 74,
-        windowWidth: 380,
-        background: background(layer),
-        callerContainer: {
-            padding: 12,
-        },
-        callerAvatar: {
-            height: avatarSize,
-            width: avatarSize,
-            cornerRadius: avatarSize / 2,
-        },
-        callerMetadata: {
-            margin: { left: 10 },
-        },
-        callerUsername: {
-            ...text(layer, "sans", { size: "sm", weight: "bold" }),
-            margin: { top: -3 },
-        },
-        callerMessage: {
-            ...text(layer, "sans", "variant", { size: "xs" }),
-            margin: { top: -3 },
-        },
-        worktreeRoots: {
-            ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
-            margin: { top: -3 },
-        },
-        buttonWidth: 96,
-        acceptButton: {
-            background: background(layer, "accent"),
-            border: border(layer, { left: true, bottom: true }),
-            ...text(layer, "sans", "positive", {
-                size: "xs",
-                weight: "extra_bold",
-            }),
-        },
-        declineButton: {
-            border: border(layer, { left: true }),
-            ...text(layer, "sans", "negative", {
-                size: "xs",
-                weight: "extra_bold",
-            }),
-        },
-    }
-}

styles/src/styleTree/picker.ts πŸ”—

@@ -1,82 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import { background, border, text } from "./components"
-
-export default function picker(colorScheme: ColorScheme): any {
-    let layer = colorScheme.lowest
-    const container = {
-        background: background(layer),
-        border: border(layer),
-        shadow: colorScheme.modalShadow,
-        cornerRadius: 12,
-        padding: {
-            bottom: 4,
-        },
-    }
-    const inputEditor = {
-        placeholderText: text(layer, "sans", "on", "disabled"),
-        selection: colorScheme.players[0],
-        text: text(layer, "mono", "on"),
-        border: border(layer, { bottom: true }),
-        padding: {
-            bottom: 8,
-            left: 16,
-            right: 16,
-            top: 8,
-        },
-        margin: {
-            bottom: 4,
-        },
-    }
-    const emptyInputEditor: any = { ...inputEditor }
-    delete emptyInputEditor.border
-    delete emptyInputEditor.margin
-
-    return {
-        ...container,
-        emptyContainer: {
-            ...container,
-            padding: {},
-        },
-        item: {
-            padding: {
-                bottom: 4,
-                left: 12,
-                right: 12,
-                top: 4,
-            },
-            margin: {
-                top: 1,
-                left: 4,
-                right: 4,
-            },
-            cornerRadius: 8,
-            text: text(layer, "sans", "variant"),
-            highlightText: text(layer, "sans", "accent", { weight: "bold" }),
-            active: {
-                background: withOpacity(
-                    background(layer, "base", "active"),
-                    0.5
-                ),
-                text: text(layer, "sans", "base", "active"),
-                highlightText: text(layer, "sans", "accent", {
-                    weight: "bold",
-                }),
-            },
-            hover: {
-                background: withOpacity(background(layer, "hovered"), 0.5),
-            },
-        },
-        inputEditor,
-        emptyInputEditor,
-        noMatches: {
-            text: text(layer, "sans", "variant"),
-            padding: {
-                bottom: 8,
-                left: 16,
-                right: 16,
-                top: 8,
-            },
-        },
-    }
-}

styles/src/styleTree/projectDiagnostics.ts πŸ”—

@@ -1,13 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, text } from "./components"
-
-export default function projectDiagnostics(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-    return {
-        background: background(layer),
-        tabIconSpacing: 4,
-        tabIconWidth: 13,
-        tabSummarySpacing: 10,
-        emptyMessage: text(layer, "sans", "variant", { size: "md" }),
-    }
-}

styles/src/styleTree/projectPanel.ts πŸ”—

@@ -1,107 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import { background, border, foreground, text } from "./components"
-
-export default function projectPanel(colorScheme: ColorScheme) {
-    const { isLight } = colorScheme
-
-    let layer = colorScheme.middle
-
-    let baseEntry = {
-        height: 22,
-        iconColor: foreground(layer, "variant"),
-        iconSize: 7,
-        iconSpacing: 5,
-    }
-
-    let status = {
-        git: {
-            modified: isLight
-                ? colorScheme.ramps.yellow(0.6).hex()
-                : colorScheme.ramps.yellow(0.5).hex(),
-            inserted: isLight
-                ? colorScheme.ramps.green(0.45).hex()
-                : colorScheme.ramps.green(0.5).hex(),
-            conflict: isLight
-                ? colorScheme.ramps.red(0.6).hex()
-                : colorScheme.ramps.red(0.5).hex(),
-        },
-    }
-
-    let entry = {
-        ...baseEntry,
-        text: text(layer, "mono", "variant", { size: "sm" }),
-        hover: {
-            background: background(layer, "variant", "hovered"),
-        },
-        active: {
-            background: colorScheme.isLight
-                ? withOpacity(background(layer, "active"), 0.5)
-                : background(layer, "active"),
-            text: text(layer, "mono", "active", { size: "sm" }),
-        },
-        activeHover: {
-            background: background(layer, "active"),
-            text: text(layer, "mono", "active", { size: "sm" }),
-        },
-        status,
-    }
-
-    return {
-        openProjectButton: {
-            background: background(layer),
-            border: border(layer, "active"),
-            cornerRadius: 4,
-            margin: {
-                top: 16,
-                left: 16,
-                right: 16,
-            },
-            padding: {
-                top: 3,
-                bottom: 3,
-                left: 7,
-                right: 7,
-            },
-            ...text(layer, "sans", "default", { size: "sm" }),
-            hover: {
-                ...text(layer, "sans", "default", { size: "sm" }),
-                background: background(layer, "hovered"),
-                border: border(layer, "active"),
-            },
-        },
-        background: background(layer),
-        padding: { left: 6, right: 6, top: 0, bottom: 6 },
-        indentWidth: 12,
-        entry,
-        draggedEntry: {
-            ...baseEntry,
-            text: text(layer, "mono", "on", { size: "sm" }),
-            background: withOpacity(background(layer, "on"), 0.9),
-            border: border(layer),
-            status,
-        },
-        ignoredEntry: {
-            ...entry,
-            iconColor: foreground(layer, "disabled"),
-            text: text(layer, "mono", "disabled"),
-            active: {
-                ...entry.active,
-                iconColor: foreground(layer, "variant"),
-            },
-        },
-        cutEntry: {
-            ...entry,
-            text: text(layer, "mono", "disabled"),
-            active: {
-                background: background(layer, "active"),
-                text: text(layer, "mono", "disabled", { size: "sm" }),
-            },
-        },
-        filenameEditor: {
-            background: background(layer, "on"),
-            text: text(layer, "mono", "on", { size: "sm" }),
-            selection: colorScheme.players[0],
-        },
-    }
-}

styles/src/styleTree/projectSharedNotification.ts πŸ”—

@@ -1,54 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function projectSharedNotification(
-    colorScheme: ColorScheme
-): Object {
-    let layer = colorScheme.middle
-
-    const avatarSize = 48
-    return {
-        windowHeight: 74,
-        windowWidth: 380,
-        background: background(layer),
-        ownerContainer: {
-            padding: 12,
-        },
-        ownerAvatar: {
-            height: avatarSize,
-            width: avatarSize,
-            cornerRadius: avatarSize / 2,
-        },
-        ownerMetadata: {
-            margin: { left: 10 },
-        },
-        ownerUsername: {
-            ...text(layer, "sans", { size: "sm", weight: "bold" }),
-            margin: { top: -3 },
-        },
-        message: {
-            ...text(layer, "sans", "variant", { size: "xs" }),
-            margin: { top: -3 },
-        },
-        worktreeRoots: {
-            ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
-            margin: { top: -3 },
-        },
-        buttonWidth: 96,
-        openButton: {
-            background: background(layer, "accent"),
-            border: border(layer, { left: true, bottom: true }),
-            ...text(layer, "sans", "accent", {
-                size: "xs",
-                weight: "extra_bold",
-            }),
-        },
-        dismissButton: {
-            border: border(layer, { left: true }),
-            ...text(layer, "sans", "variant", {
-                size: "xs",
-                weight: "extra_bold",
-            }),
-        },
-    }
-}

styles/src/styleTree/search.ts πŸ”—

@@ -1,113 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import { background, border, foreground, text } from "./components"
-
-export default function search(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-
-    // Search input
-    const editor = {
-        background: background(layer),
-        cornerRadius: 8,
-        minWidth: 200,
-        maxWidth: 500,
-        placeholderText: text(layer, "mono", "disabled"),
-        selection: colorScheme.players[0],
-        text: text(layer, "mono", "default"),
-        border: border(layer),
-        margin: {
-            right: 12,
-        },
-        padding: {
-            top: 3,
-            bottom: 3,
-            left: 12,
-            right: 8,
-        },
-    }
-
-    const includeExcludeEditor = {
-        ...editor,
-        minWidth: 100,
-        maxWidth: 250,
-    }
-
-    return {
-        // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
-        matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
-        optionButton: {
-            ...text(layer, "mono", "on"),
-            background: background(layer, "on"),
-            cornerRadius: 6,
-            border: border(layer, "on"),
-            margin: {
-                right: 4,
-            },
-            padding: {
-                bottom: 2,
-                left: 10,
-                right: 10,
-                top: 2,
-            },
-            active: {
-                ...text(layer, "mono", "on", "inverted"),
-                background: background(layer, "on", "inverted"),
-                border: border(layer, "on", "inverted"),
-            },
-            clicked: {
-                ...text(layer, "mono", "on", "pressed"),
-                background: background(layer, "on", "pressed"),
-                border: border(layer, "on", "pressed"),
-            },
-            hover: {
-                ...text(layer, "mono", "on", "hovered"),
-                background: background(layer, "on", "hovered"),
-                border: border(layer, "on", "hovered"),
-            },
-        },
-        editor,
-        invalidEditor: {
-            ...editor,
-            border: border(layer, "negative"),
-        },
-        includeExcludeEditor,
-        invalidIncludeExcludeEditor: {
-            ...includeExcludeEditor,
-            border: border(layer, "negative"),
-        },
-        matchIndex: {
-            ...text(layer, "mono", "variant"),
-            padding: {
-                left: 6,
-            },
-        },
-        optionButtonGroup: {
-            padding: {
-                left: 12,
-                right: 12,
-            },
-        },
-        includeExcludeInputs: {
-            ...text(layer, "mono", "variant"),
-            padding: {
-                right: 6,
-            },
-        },
-        resultsStatus: {
-            ...text(layer, "mono", "on"),
-            size: 18,
-        },
-        dismissButton: {
-            color: foreground(layer, "variant"),
-            iconWidth: 12,
-            buttonWidth: 14,
-            padding: {
-                left: 10,
-                right: 10,
-            },
-            hover: {
-                color: foreground(layer, "hovered"),
-            },
-        },
-    }
-}

styles/src/styleTree/sharedScreen.ts πŸ”—

@@ -1,9 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background } from "./components"
-
-export default function sharedScreen(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-    return {
-        background: background(layer),
-    }
-}

styles/src/styleTree/simpleMessageNotification.ts πŸ”—

@@ -1,44 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-
-const headerPadding = 8
-
-export default function simpleMessageNotification(
-    colorScheme: ColorScheme
-): Object {
-    let layer = colorScheme.middle
-    return {
-        message: {
-            ...text(layer, "sans", { size: "xs" }),
-            margin: { left: headerPadding, right: headerPadding },
-        },
-        actionMessage: {
-            ...text(layer, "sans", { size: "xs" }),
-            border: border(layer, "active"),
-            cornerRadius: 4,
-            padding: {
-                top: 3,
-                bottom: 3,
-                left: 7,
-                right: 7,
-            },
-
-            margin: { left: headerPadding, top: 6, bottom: 6 },
-            hover: {
-                ...text(layer, "sans", "default", { size: "xs" }),
-                background: background(layer, "hovered"),
-                border: border(layer, "active"),
-            },
-        },
-        dismissButton: {
-            color: foreground(layer),
-            iconWidth: 8,
-            iconHeight: 8,
-            buttonWidth: 8,
-            buttonHeight: 8,
-            hover: {
-                color: foreground(layer, "hovered"),
-            },
-        },
-    }
-}

styles/src/styleTree/statusBar.ts πŸ”—

@@ -1,126 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, foreground, text } from "./components"
-
-export default function statusBar(colorScheme: ColorScheme) {
-    let layer = colorScheme.lowest
-
-    const statusContainer = {
-        cornerRadius: 6,
-        padding: { top: 3, bottom: 3, left: 6, right: 6 },
-    }
-
-    const diagnosticStatusContainer = {
-        cornerRadius: 6,
-        padding: { top: 1, bottom: 1, left: 6, right: 6 },
-    }
-
-    return {
-        height: 30,
-        itemSpacing: 8,
-        padding: {
-            top: 1,
-            bottom: 1,
-            left: 6,
-            right: 6,
-        },
-        border: border(layer, { top: true, overlay: true }),
-        cursorPosition: text(layer, "sans", "variant"),
-        activeLanguage: {
-            padding: { left: 6, right: 6 },
-            ...text(layer, "sans", "variant"),
-            hover: {
-                ...text(layer, "sans", "on"),
-            },
-        },
-        autoUpdateProgressMessage: text(layer, "sans", "variant"),
-        autoUpdateDoneMessage: text(layer, "sans", "variant"),
-        lspStatus: {
-            ...diagnosticStatusContainer,
-            iconSpacing: 4,
-            iconWidth: 14,
-            height: 18,
-            message: text(layer, "sans"),
-            iconColor: foreground(layer),
-            hover: {
-                message: text(layer, "sans"),
-                iconColor: foreground(layer),
-                background: background(layer, "hovered"),
-            },
-        },
-        diagnosticMessage: {
-            ...text(layer, "sans"),
-            hover: text(layer, "sans", "hovered"),
-        },
-        diagnosticSummary: {
-            height: 20,
-            iconWidth: 16,
-            iconSpacing: 2,
-            summarySpacing: 6,
-            text: text(layer, "sans", { size: "sm" }),
-            iconColorOk: foreground(layer, "variant"),
-            iconColorWarning: foreground(layer, "warning"),
-            iconColorError: foreground(layer, "negative"),
-            containerOk: {
-                cornerRadius: 6,
-                padding: { top: 3, bottom: 3, left: 7, right: 7 },
-            },
-            containerWarning: {
-                ...diagnosticStatusContainer,
-                background: background(layer, "warning"),
-                border: border(layer, "warning"),
-            },
-            containerError: {
-                ...diagnosticStatusContainer,
-                background: background(layer, "negative"),
-                border: border(layer, "negative"),
-            },
-            hover: {
-                iconColorOk: foreground(layer, "on"),
-                containerOk: {
-                    cornerRadius: 6,
-                    padding: { top: 3, bottom: 3, left: 7, right: 7 },
-                    background: background(layer, "on", "hovered"),
-                },
-                containerWarning: {
-                    ...diagnosticStatusContainer,
-                    background: background(layer, "warning", "hovered"),
-                    border: border(layer, "warning", "hovered"),
-                },
-                containerError: {
-                    ...diagnosticStatusContainer,
-                    background: background(layer, "negative", "hovered"),
-                    border: border(layer, "negative", "hovered"),
-                },
-            },
-        },
-        panelButtons: {
-            groupLeft: {},
-            groupBottom: {},
-            groupRight: {},
-            button: {
-                ...statusContainer,
-                iconSize: 16,
-                iconColor: foreground(layer, "variant"),
-                label: {
-                    margin: { left: 6 },
-                    ...text(layer, "sans", { size: "sm" }),
-                },
-                hover: {
-                    iconColor: foreground(layer, "hovered"),
-                    background: background(layer, "variant"),
-                },
-                active: {
-                    iconColor: foreground(layer, "active"),
-                    background: background(layer, "active"),
-                },
-            },
-            badge: {
-                cornerRadius: 3,
-                padding: 2,
-                margin: { bottom: -1, right: -1 },
-                border: border(layer),
-                background: background(layer, "accent"),
-            },
-        },
-    }
-}

styles/src/styleTree/tabBar.ts πŸ”—

@@ -1,109 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import { text, border, background, foreground } from "./components"
-
-export default function tabBar(colorScheme: ColorScheme) {
-    const height = 32
-
-    let activeLayer = colorScheme.highest
-    let layer = colorScheme.middle
-
-    const tab = {
-        height,
-        text: text(layer, "sans", "variant", { size: "sm" }),
-        background: background(layer),
-        border: border(layer, {
-            right: true,
-            bottom: true,
-            overlay: true,
-        }),
-        padding: {
-            left: 8,
-            right: 12,
-        },
-        spacing: 8,
-
-        // Tab type icons (e.g. Project Search)
-        typeIconWidth: 14,
-
-        // Close icons
-        closeIconWidth: 8,
-        iconClose: foreground(layer, "variant"),
-        iconCloseActive: foreground(layer, "hovered"),
-
-        // Indicators
-        iconConflict: foreground(layer, "warning"),
-        iconDirty: foreground(layer, "accent"),
-
-        // When two tabs of the same name are open, a label appears next to them
-        description: {
-            margin: { left: 8 },
-            ...text(layer, "sans", "disabled", { size: "2xs" }),
-        },
-    }
-
-    const activePaneActiveTab = {
-        ...tab,
-        background: background(activeLayer),
-        text: text(activeLayer, "sans", "active", { size: "sm" }),
-        border: {
-            ...tab.border,
-            bottom: false,
-        },
-    }
-
-    const inactivePaneInactiveTab = {
-        ...tab,
-        background: background(layer),
-        text: text(layer, "sans", "variant", { size: "sm" }),
-    }
-
-    const inactivePaneActiveTab = {
-        ...tab,
-        background: background(activeLayer),
-        text: text(layer, "sans", "variant", { size: "sm" }),
-        border: {
-            ...tab.border,
-            bottom: false,
-        },
-    }
-
-    const draggedTab = {
-        ...activePaneActiveTab,
-        background: withOpacity(tab.background, 0.9),
-        border: undefined as any,
-        shadow: colorScheme.popoverShadow,
-    }
-
-    return {
-        height,
-        background: background(layer),
-        activePane: {
-            activeTab: activePaneActiveTab,
-            inactiveTab: tab,
-        },
-        inactivePane: {
-            activeTab: inactivePaneActiveTab,
-            inactiveTab: inactivePaneInactiveTab,
-        },
-        draggedTab,
-        paneButton: {
-            color: foreground(layer, "variant"),
-            iconWidth: 12,
-            buttonWidth: activePaneActiveTab.height,
-            hover: {
-                color: foreground(layer, "hovered"),
-            },
-            active: {
-                color: foreground(layer, "accent"),
-            },
-        },
-        paneButtonContainer: {
-            background: tab.background,
-            border: {
-                ...tab.border,
-                right: false,
-            },
-        },
-    }
-}

styles/src/styleTree/terminal.ts πŸ”—

@@ -1,52 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-
-export default function terminal(colorScheme: ColorScheme) {
-    /**
-     * Colors are controlled per-cell in the terminal grid.
-     * Cells can be set to any of these more 'theme-capable' colors
-     * or can be set directly with RGB values.
-     * Here are the common interpretations of these names:
-     * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
-     */
-    return {
-        black: colorScheme.ramps.neutral(0).hex(),
-        red: colorScheme.ramps.red(0.5).hex(),
-        green: colorScheme.ramps.green(0.5).hex(),
-        yellow: colorScheme.ramps.yellow(0.5).hex(),
-        blue: colorScheme.ramps.blue(0.5).hex(),
-        magenta: colorScheme.ramps.magenta(0.5).hex(),
-        cyan: colorScheme.ramps.cyan(0.5).hex(),
-        white: colorScheme.ramps.neutral(1).hex(),
-        brightBlack: colorScheme.ramps.neutral(0.4).hex(),
-        brightRed: colorScheme.ramps.red(0.25).hex(),
-        brightGreen: colorScheme.ramps.green(0.25).hex(),
-        brightYellow: colorScheme.ramps.yellow(0.25).hex(),
-        brightBlue: colorScheme.ramps.blue(0.25).hex(),
-        brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
-        brightCyan: colorScheme.ramps.cyan(0.25).hex(),
-        brightWhite: colorScheme.ramps.neutral(1).hex(),
-        /**
-         * Default color for characters
-         */
-        foreground: colorScheme.ramps.neutral(1).hex(),
-        /**
-         * Default color for the rectangle background of a cell
-         */
-        background: colorScheme.ramps.neutral(0).hex(),
-        modalBackground: colorScheme.ramps.neutral(0.1).hex(),
-        /**
-         * Default color for the cursor
-         */
-        cursor: colorScheme.players[0].cursor,
-        dimBlack: colorScheme.ramps.neutral(1).hex(),
-        dimRed: colorScheme.ramps.red(0.75).hex(),
-        dimGreen: colorScheme.ramps.green(0.75).hex(),
-        dimYellow: colorScheme.ramps.yellow(0.75).hex(),
-        dimBlue: colorScheme.ramps.blue(0.75).hex(),
-        dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
-        dimCyan: colorScheme.ramps.cyan(0.75).hex(),
-        dimWhite: colorScheme.ramps.neutral(0.6).hex(),
-        brightForeground: colorScheme.ramps.neutral(1).hex(),
-        dimForeground: colorScheme.ramps.neutral(0).hex(),
-    }
-}

styles/src/styleTree/toolbarDropdownMenu.ts πŸ”—

@@ -1,46 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function dropdownMenu(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle
-
-    return {
-        rowHeight: 30,
-        background: background(layer),
-        border: border(layer),
-        shadow: colorScheme.popoverShadow,
-        header: {
-            ...text(layer, "sans", { size: "sm" }),
-            secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }),
-            secondaryTextSpacing: 10,
-            padding: { left: 8, right: 8, top: 2, bottom: 2 },
-            cornerRadius: 6,
-            background: background(layer, "on"),
-            border: border(layer, "on", { overlay: true }),
-            hover: {
-                background: background(layer, "hovered"),
-                ...text(layer, "sans", "hovered", { size: "sm" }),
-            }
-        },
-        sectionHeader: {
-            ...text(layer, "sans", { size: "sm" }),
-            padding: { left: 8, right: 8, top: 8, bottom: 8 },
-        },
-        item: {
-            ...text(layer, "sans", { size: "sm" }),
-            secondaryTextSpacing: 10,
-            secondaryText: text(layer, "sans", { size: "sm" }),
-            padding: { left: 18, right: 18, top: 2, bottom: 2 },
-            hover: {
-                background: background(layer, "hovered"),
-                ...text(layer, "sans", "hovered", { size: "sm" }),
-            },
-            active: {
-                background: background(layer, "active"),
-            },
-            activeHover: {
-                background: background(layer, "active"),
-            },
-        },
-    }
-}

styles/src/styleTree/tooltip.ts πŸ”—

@@ -1,23 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { background, border, text } from "./components"
-
-export default function tooltip(colorScheme: ColorScheme) {
-    let layer = colorScheme.middle
-    return {
-        background: background(layer),
-        border: border(layer),
-        padding: { top: 4, bottom: 4, left: 8, right: 8 },
-        margin: { top: 6, left: 6 },
-        shadow: colorScheme.popoverShadow,
-        cornerRadius: 6,
-        text: text(layer, "sans", { size: "xs" }),
-        keystroke: {
-            background: background(layer, "on"),
-            cornerRadius: 4,
-            margin: { left: 6 },
-            padding: { left: 4, right: 4 },
-            ...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
-        },
-        maxTextWidth: 200,
-    }
-}

styles/src/styleTree/updateNotification.ts πŸ”—

@@ -1,31 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { foreground, text } from "./components"
-
-const headerPadding = 8
-
-export default function updateNotification(colorScheme: ColorScheme): Object {
-    let layer = colorScheme.middle
-    return {
-        message: {
-            ...text(layer, "sans", { size: "xs" }),
-            margin: { left: headerPadding, right: headerPadding },
-        },
-        actionMessage: {
-            ...text(layer, "sans", { size: "xs" }),
-            margin: { left: headerPadding, top: 6, bottom: 6 },
-            hover: {
-                color: foreground(layer, "hovered"),
-            },
-        },
-        dismissButton: {
-            color: foreground(layer),
-            iconWidth: 8,
-            iconHeight: 8,
-            buttonWidth: 8,
-            buttonHeight: 8,
-            hover: {
-                color: foreground(layer, "hovered"),
-            },
-        },
-    }
-}

styles/src/styleTree/welcome.ts πŸ”—

@@ -1,129 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import {
-    border,
-    background,
-    foreground,
-    text,
-    TextProperties,
-    svg,
-} from "./components"
-
-export default function welcome(colorScheme: ColorScheme) {
-    let layer = colorScheme.highest
-
-    let checkboxBase = {
-        cornerRadius: 4,
-        padding: {
-            left: 3,
-            right: 3,
-            top: 3,
-            bottom: 3,
-        },
-        // shadow: colorScheme.popoverShadow,
-        border: border(layer),
-        margin: {
-            right: 8,
-            top: 5,
-            bottom: 5,
-        },
-    }
-
-    let interactive_text_size: TextProperties = { size: "sm" }
-
-    return {
-        pageWidth: 320,
-        logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64),
-        logoSubheading: {
-            ...text(layer, "sans", "variant", { size: "md" }),
-            margin: {
-                top: 10,
-                bottom: 7,
-            },
-        },
-        buttonGroup: {
-            margin: {
-                top: 8,
-                bottom: 16,
-            },
-        },
-        headingGroup: {
-            margin: {
-                top: 8,
-                bottom: 12,
-            },
-        },
-        checkboxGroup: {
-            border: border(layer, "variant"),
-            background: withOpacity(background(layer, "hovered"), 0.25),
-            cornerRadius: 4,
-            padding: {
-                left: 12,
-                top: 2,
-                bottom: 2,
-            },
-        },
-        button: {
-            background: background(layer),
-            border: border(layer, "active"),
-            cornerRadius: 4,
-            margin: {
-                top: 4,
-                bottom: 4,
-            },
-            padding: {
-                top: 3,
-                bottom: 3,
-                left: 7,
-                right: 7,
-            },
-            ...text(layer, "sans", "default", interactive_text_size),
-            hover: {
-                ...text(layer, "sans", "default", interactive_text_size),
-                background: background(layer, "hovered"),
-                border: border(layer, "active"),
-            },
-        },
-        usageNote: {
-            ...text(layer, "sans", "variant", { size: "2xs" }),
-            padding: {
-                top: -4,
-            },
-        },
-        checkboxContainer: {
-            margin: {
-                top: 4,
-            },
-            padding: {
-                bottom: 8,
-            },
-        },
-        checkbox: {
-            label: {
-                ...text(layer, "sans", interactive_text_size),
-                // Also supports margin, container, border, etc.
-            },
-            icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12),
-            default: {
-                ...checkboxBase,
-                background: background(layer, "default"),
-                border: border(layer, "active"),
-            },
-            checked: {
-                ...checkboxBase,
-                background: background(layer, "hovered"),
-                border: border(layer, "active"),
-            },
-            hovered: {
-                ...checkboxBase,
-                background: background(layer, "hovered"),
-                border: border(layer, "active"),
-            },
-            hoveredAndChecked: {
-                ...checkboxBase,
-                background: background(layer, "hovered"),
-                border: border(layer, "active"),
-            },
-        },
-    }
-}

styles/src/styleTree/workspace.ts πŸ”—

@@ -1,338 +0,0 @@
-import { ColorScheme } from "../theme/colorScheme"
-import { withOpacity } from "../theme/color"
-import {
-    background,
-    border,
-    borderColor,
-    foreground,
-    svg,
-    text,
-} from "./components"
-import statusBar from "./statusBar"
-import tabBar from "./tabBar"
-
-export default function workspace(colorScheme: ColorScheme) {
-    const layer = colorScheme.lowest
-    const isLight = colorScheme.isLight
-    const itemSpacing = 8
-    const titlebarButton = {
-        cornerRadius: 6,
-        padding: {
-            top: 1,
-            bottom: 1,
-            left: 8,
-            right: 8,
-        },
-        ...text(layer, "sans", "variant", { size: "xs" }),
-        background: background(layer, "variant"),
-        border: border(layer),
-        hover: {
-            ...text(layer, "sans", "variant", "hovered", { size: "xs" }),
-            background: background(layer, "variant", "hovered"),
-            border: border(layer, "variant", "hovered"),
-        },
-        clicked: {
-            ...text(layer, "sans", "variant", "pressed", { size: "xs" }),
-            background: background(layer, "variant", "pressed"),
-            border: border(layer, "variant", "pressed"),
-        },
-        active: {
-            ...text(layer, "sans", "variant", "active", { size: "xs" }),
-            background: background(layer, "variant", "active"),
-            border: border(layer, "variant", "active"),
-        },
-    }
-    const avatarWidth = 18
-    const avatarOuterWidth = avatarWidth + 4
-    const followerAvatarWidth = 14
-    const followerAvatarOuterWidth = followerAvatarWidth + 4
-
-    return {
-        background: background(colorScheme.lowest),
-        blankPane: {
-            logoContainer: {
-                width: 256,
-                height: 256,
-            },
-            logo: svg(
-                withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8),
-                "icons/logo_96.svg",
-                256,
-                256
-            ),
-
-            logoShadow: svg(
-                withOpacity(
-                    colorScheme.isLight
-                        ? "#FFFFFF"
-                        : colorScheme.lowest.base.default.background,
-                    colorScheme.isLight ? 1 : 0.6
-                ),
-                "icons/logo_96.svg",
-                256,
-                256
-            ),
-            keyboardHints: {
-                margin: {
-                    top: 96,
-                },
-                cornerRadius: 4,
-            },
-            keyboardHint: {
-                ...text(layer, "sans", "variant", { size: "sm" }),
-                padding: {
-                    top: 3,
-                    left: 8,
-                    right: 8,
-                    bottom: 3,
-                },
-                cornerRadius: 8,
-                hover: {
-                    ...text(layer, "sans", "active", { size: "sm" }),
-                },
-            },
-            keyboardHintWidth: 320,
-        },
-        joiningProjectAvatar: {
-            cornerRadius: 40,
-            width: 80,
-        },
-        joiningProjectMessage: {
-            padding: 12,
-            ...text(layer, "sans", { size: "lg" }),
-        },
-        externalLocationMessage: {
-            background: background(colorScheme.middle, "accent"),
-            border: border(colorScheme.middle, "accent"),
-            cornerRadius: 6,
-            padding: 12,
-            margin: { bottom: 8, right: 8 },
-            ...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
-        },
-        leaderBorderOpacity: 0.7,
-        leaderBorderWidth: 2.0,
-        tabBar: tabBar(colorScheme),
-        modal: {
-            margin: {
-                bottom: 52,
-                top: 52,
-            },
-            cursor: "Arrow",
-        },
-        zoomedBackground: {
-            cursor: "Arrow",
-            background: isLight
-                ? withOpacity(background(colorScheme.lowest), 0.8)
-                : withOpacity(background(colorScheme.highest), 0.6),
-        },
-        zoomedPaneForeground: {
-            margin: 16,
-            shadow: colorScheme.modalShadow,
-            border: border(colorScheme.lowest, { overlay: true }),
-        },
-        zoomedPanelForeground: {
-            margin: 16,
-            border: border(colorScheme.lowest, { overlay: true }),
-        },
-        dock: {
-            left: {
-                border: border(layer, { right: true }),
-            },
-            bottom: {
-                border: border(layer, { top: true }),
-            },
-            right: {
-                border: border(layer, { left: true }),
-            },
-        },
-        paneDivider: {
-            color: borderColor(layer),
-            width: 1,
-        },
-        statusBar: statusBar(colorScheme),
-        titlebar: {
-            itemSpacing,
-            facePileSpacing: 2,
-            height: 33, // 32px + 1px border. It's important the content area of the titlebar is evenly sized to vertically center avatar images.
-            background: background(layer),
-            border: border(layer, { bottom: true }),
-            padding: {
-                left: 80,
-                right: itemSpacing,
-            },
-
-            // Project
-            title: text(layer, "sans", "variant"),
-            highlight_color: text(layer, "sans", "active").color,
-
-            // Collaborators
-            leaderAvatar: {
-                width: avatarWidth,
-                outerWidth: avatarOuterWidth,
-                cornerRadius: avatarWidth / 2,
-                outerCornerRadius: avatarOuterWidth / 2,
-            },
-            followerAvatar: {
-                width: followerAvatarWidth,
-                outerWidth: followerAvatarOuterWidth,
-                cornerRadius: followerAvatarWidth / 2,
-                outerCornerRadius: followerAvatarOuterWidth / 2,
-            },
-            inactiveAvatarGrayscale: true,
-            followerAvatarOverlap: 8,
-            leaderSelection: {
-                margin: {
-                    top: 4,
-                    bottom: 4,
-                },
-                padding: {
-                    left: 2,
-                    right: 2,
-                    top: 2,
-                    bottom: 2,
-                },
-                cornerRadius: 6,
-            },
-            avatarRibbon: {
-                height: 3,
-                width: 12,
-                // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
-            },
-
-            // Sign in buttom
-            // FlatButton, Variant
-            signInPrompt: {
-                margin: {
-                    left: itemSpacing,
-                },
-                ...titlebarButton,
-            },
-
-            // Offline Indicator
-            offlineIcon: {
-                color: foreground(layer, "variant"),
-                width: 16,
-                margin: {
-                    left: itemSpacing,
-                },
-                padding: {
-                    right: 4,
-                },
-            },
-
-            // Notice that the collaboration server is out of date
-            outdatedWarning: {
-                ...text(layer, "sans", "warning", { size: "xs" }),
-                background: withOpacity(background(layer, "warning"), 0.3),
-                border: border(layer, "warning"),
-                margin: {
-                    left: itemSpacing,
-                },
-                padding: {
-                    left: 8,
-                    right: 8,
-                },
-                cornerRadius: 6,
-            },
-            callControl: {
-                cornerRadius: 6,
-                color: foreground(layer, "variant"),
-                iconWidth: 12,
-                buttonWidth: 20,
-                hover: {
-                    background: background(layer, "variant", "hovered"),
-                    color: foreground(layer, "variant", "hovered"),
-                },
-            },
-            toggleContactsButton: {
-                margin: { left: itemSpacing },
-                cornerRadius: 6,
-                color: foreground(layer, "variant"),
-                iconWidth: 14,
-                buttonWidth: 20,
-                active: {
-                    background: background(layer, "variant", "active"),
-                    color: foreground(layer, "variant", "active"),
-                },
-                clicked: {
-                    background: background(layer, "variant", "pressed"),
-                    color: foreground(layer, "variant", "pressed"),
-                },
-                hover: {
-                    background: background(layer, "variant", "hovered"),
-                    color: foreground(layer, "variant", "hovered"),
-                },
-            },
-            userMenuButton: {
-                buttonWidth: 20,
-                iconWidth: 12,
-                ...titlebarButton,
-            },
-            toggleContactsBadge: {
-                cornerRadius: 3,
-                padding: 2,
-                margin: { top: 3, left: 3 },
-                border: border(layer),
-                background: foreground(layer, "accent"),
-            },
-            shareButton: {
-                ...titlebarButton,
-            },
-        },
-
-        toolbar: {
-            height: 34,
-            background: background(colorScheme.highest),
-            border: border(colorScheme.highest, { bottom: true }),
-            itemSpacing: 8,
-            navButton: {
-                color: foreground(colorScheme.highest, "on"),
-                iconWidth: 12,
-                buttonWidth: 24,
-                cornerRadius: 6,
-                hover: {
-                    color: foreground(colorScheme.highest, "on", "hovered"),
-                    background: background(
-                        colorScheme.highest,
-                        "on",
-                        "hovered"
-                    ),
-                },
-                disabled: {
-                    color: foreground(colorScheme.highest, "on", "disabled"),
-                },
-            },
-            padding: { left: 8, right: 8, top: 4, bottom: 4 },
-        },
-        breadcrumbHeight: 24,
-        breadcrumbs: {
-            ...text(colorScheme.highest, "sans", "variant"),
-            cornerRadius: 6,
-            padding: {
-                left: 6,
-                right: 6,
-            },
-            hover: {
-                color: foreground(colorScheme.highest, "on", "hovered"),
-                background: background(colorScheme.highest, "on", "hovered"),
-            },
-        },
-        disconnectedOverlay: {
-            ...text(layer, "sans"),
-            background: withOpacity(background(layer), 0.8),
-        },
-        notification: {
-            margin: { top: 10 },
-            background: background(colorScheme.middle),
-            cornerRadius: 6,
-            padding: 12,
-            border: border(colorScheme.middle),
-            shadow: colorScheme.popoverShadow,
-        },
-        notifications: {
-            width: 400,
-            margin: { right: 10, bottom: 10 },
-        },
-        dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
-    }
-}

styles/src/style_tree/app.ts πŸ”—

@@ -0,0 +1,62 @@
+import contact_finder from "./contact_finder"
+import contacts_popover from "./contacts_popover"
+import command_palette from "./command_palette"
+import project_panel from "./project_panel"
+import search from "./search"
+import picker from "./picker"
+import workspace from "./workspace"
+import context_menu from "./context_menu"
+import shared_screen from "./shared_screen"
+import project_diagnostics from "./project_diagnostics"
+import contact_notification from "./contact_notification"
+import update_notification from "./update_notification"
+import simple_message_notification from "./simple_message_notification"
+import project_shared_notification from "./project_shared_notification"
+import tooltip from "./tooltip"
+import terminal from "./terminal"
+import contact_list from "./contact_list"
+import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
+import incoming_call_notification from "./incoming_call_notification"
+import welcome from "./welcome"
+import copilot from "./copilot"
+import assistant from "./assistant"
+import { titlebar } from "./titlebar"
+import editor from "./editor"
+import feedback from "./feedback"
+import { useTheme } from "../common"
+
+export default function app(): any {
+    const theme = useTheme()
+
+    return {
+        meta: {
+            name: theme.name,
+            is_light: theme.is_light,
+        },
+        command_palette: command_palette(),
+        contact_notification: contact_notification(),
+        project_shared_notification: project_shared_notification(),
+        incoming_call_notification: incoming_call_notification(),
+        picker: picker(),
+        workspace: workspace(),
+        titlebar: titlebar(),
+        copilot: copilot(),
+        welcome: welcome(),
+        context_menu: context_menu(),
+        editor: editor(),
+        project_diagnostics: project_diagnostics(),
+        project_panel: project_panel(),
+        contacts_popover: contacts_popover(),
+        contact_finder: contact_finder(),
+        contact_list: contact_list(),
+        toolbar_dropdown_menu: toolbar_dropdown_menu(),
+        search: search(),
+        shared_screen: shared_screen(),
+        update_notification: update_notification(),
+        simple_message_notification: simple_message_notification(),
+        tooltip: tooltip(),
+        terminal: terminal(),
+        assistant: assistant(),
+        feedback: feedback()
+    }
+}

styles/src/style_tree/assistant.ts πŸ”—

@@ -0,0 +1,281 @@
+import { text, border, background, foreground } from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function assistant(): any {
+    const theme = useTheme()
+
+    return {
+        container: {
+            background: background(theme.highest),
+            padding: { left: 12 },
+        },
+        message_header: {
+            margin: { bottom: 6, top: 6 },
+            background: background(theme.highest),
+        },
+        hamburger_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/hamburger_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    padding: { left: 12, right: 8.5 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        split_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/split_message_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    padding: { left: 8.5, right: 8.5 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        quote_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/quote_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    padding: { left: 8.5, right: 8.5 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        assist_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/assist_15.svg",
+                    dimensions: {
+                        width: 15,
+                        height: 15,
+                    },
+                },
+                container: {
+                    padding: { left: 8.5, right: 8.5 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        zoom_in_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/maximize_8.svg",
+                    dimensions: {
+                        width: 12,
+                        height: 12,
+                    },
+                },
+                container: {
+                    padding: { left: 10, right: 10 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        zoom_out_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/minimize_8.svg",
+                    dimensions: {
+                        width: 12,
+                        height: 12,
+                    },
+                },
+                container: {
+                    padding: { left: 10, right: 10 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        plus_button: interactive({
+            base: {
+                icon: {
+                    color: foreground(theme.highest, "variant"),
+                    asset: "icons/plus_12.svg",
+                    dimensions: {
+                        width: 12,
+                        height: 12,
+                    },
+                },
+                container: {
+                    padding: { left: 10, right: 10 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.highest, "hovered"),
+                    },
+                },
+            },
+        }),
+        title: {
+            ...text(theme.highest, "sans", "default", { size: "sm" }),
+        },
+        saved_conversation: {
+            container: interactive({
+                base: {
+                    background: background(theme.highest, "on"),
+                    padding: { top: 4, bottom: 4 },
+                },
+                state: {
+                    hovered: {
+                        background: background(theme.highest, "on", "hovered"),
+                    },
+                },
+            }),
+            saved_at: {
+                margin: { left: 8 },
+                ...text(theme.highest, "sans", "default", { size: "xs" }),
+            },
+            title: {
+                margin: { left: 16 },
+                ...text(theme.highest, "sans", "default", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        },
+        user_sender: {
+            default: {
+                ...text(theme.highest, "sans", "default", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        },
+        assistant_sender: {
+            default: {
+                ...text(theme.highest, "sans", "accent", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        },
+        system_sender: {
+            default: {
+                ...text(theme.highest, "sans", "variant", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        },
+        sent_at: {
+            margin: { top: 2, left: 8 },
+            ...text(theme.highest, "sans", "default", { size: "2xs" }),
+        },
+        model: interactive({
+            base: {
+                background: background(theme.highest, "on"),
+                margin: { left: 12, right: 12, top: 12 },
+                padding: 4,
+                corner_radius: 4,
+                ...text(theme.highest, "sans", "default", { size: "xs" }),
+            },
+            state: {
+                hovered: {
+                    background: background(theme.highest, "on", "hovered"),
+                    border: border(theme.highest, "on", { overlay: true }),
+                },
+            },
+        }),
+        remaining_tokens: {
+            background: background(theme.highest, "on"),
+            margin: { top: 12, right: 24 },
+            padding: 4,
+            corner_radius: 4,
+            ...text(theme.highest, "sans", "positive", { size: "xs" }),
+        },
+        no_remaining_tokens: {
+            background: background(theme.highest, "on"),
+            margin: { top: 12, right: 24 },
+            padding: 4,
+            corner_radius: 4,
+            ...text(theme.highest, "sans", "negative", { size: "xs" }),
+        },
+        error_icon: {
+            margin: { left: 8 },
+            color: foreground(theme.highest, "negative"),
+            width: 12,
+        },
+        api_key_editor: {
+            background: background(theme.highest, "on"),
+            corner_radius: 6,
+            text: text(theme.highest, "mono", "on"),
+            placeholder_text: text(theme.highest, "mono", "on", "disabled", {
+                size: "xs",
+            }),
+            selection: theme.players[0],
+            border: border(theme.highest, "on"),
+            padding: {
+                bottom: 4,
+                left: 8,
+                right: 8,
+                top: 4,
+            },
+        },
+        api_key_prompt: {
+            padding: 10,
+            ...text(theme.highest, "sans", "default", { size: "xs" }),
+        },
+    }
+}

styles/src/style_tree/command_palette.ts πŸ”—

@@ -0,0 +1,46 @@
+import { with_opacity } from "../theme/color"
+import { text, background } from "./components"
+import { toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function command_palette(): any {
+    const theme = useTheme()
+
+    const key = toggleable({
+        base: {
+            text: text(theme.highest, "mono", "variant", "default", {
+                size: "xs",
+            }),
+            corner_radius: 2,
+            background: background(theme.highest, "on"),
+            padding: {
+                top: 1,
+                bottom: 1,
+                left: 6,
+                right: 6,
+            },
+            margin: {
+                top: 1,
+                bottom: 1,
+                left: 2,
+            },
+        },
+        state: {
+            active: {
+                text: text(theme.highest, "mono", "on", "default", {
+                    size: "xs",
+                }),
+                background: with_opacity(background(theme.highest, "on"), 0.2),
+            },
+        },
+    })
+
+    return {
+        keystroke_spacing: 8,
+        // TODO: This should be a Toggle<ContainedText> on the rust side so we don't have to do this
+        key: {
+            inactive: { ...key.inactive },
+            active: key.active,
+        },
+    }
+}

styles/src/styleTree/components.ts β†’ styles/src/style_tree/components.ts πŸ”—

@@ -1,7 +1,7 @@
-import { fontFamilies, fontSizes, FontWeight } from "../common"
-import { Layer, Styles, StyleSets, Style } from "../theme/colorScheme"
+import { font_families, font_sizes, FontWeight } from "../common"
+import { Layer, Styles, StyleSets, Style } from "../theme/create_theme"
 
-function isStyleSet(key: any): key is StyleSets {
+function is_style_set(key: any): key is StyleSets {
     return [
         "base",
         "variant",
@@ -13,7 +13,7 @@ function isStyleSet(key: any): key is StyleSets {
     ].includes(key)
 }
 
-function isStyle(key: any): key is Styles {
+function is_style(key: any): key is Styles {
     return [
         "default",
         "active",
@@ -23,70 +23,70 @@ function isStyle(key: any): key is Styles {
         "inverted",
     ].includes(key)
 }
-function getStyle(
+function get_style(
     layer: Layer,
-    possibleStyleSetOrStyle?: any,
-    possibleStyle?: any
+    possible_style_set_or_style?: any,
+    possible_style?: any
 ): Style {
-    let styleSet: StyleSets = "base"
+    let style_set: StyleSets = "base"
     let style: Styles = "default"
-    if (isStyleSet(possibleStyleSetOrStyle)) {
-        styleSet = possibleStyleSetOrStyle
-    } else if (isStyle(possibleStyleSetOrStyle)) {
-        style = possibleStyleSetOrStyle
+    if (is_style_set(possible_style_set_or_style)) {
+        style_set = possible_style_set_or_style
+    } else if (is_style(possible_style_set_or_style)) {
+        style = possible_style_set_or_style
     }
 
-    if (isStyle(possibleStyle)) {
-        style = possibleStyle
+    if (is_style(possible_style)) {
+        style = possible_style
     }
 
-    return layer[styleSet][style]
+    return layer[style_set][style]
 }
 
 export function background(layer: Layer, style?: Styles): string
 export function background(
     layer: Layer,
-    styleSet?: StyleSets,
+    style_set?: StyleSets,
     style?: Styles
 ): string
 export function background(
     layer: Layer,
-    styleSetOrStyles?: StyleSets | Styles,
+    style_set_or_styles?: StyleSets | Styles,
     style?: Styles
 ): string {
-    return getStyle(layer, styleSetOrStyles, style).background
+    return get_style(layer, style_set_or_styles, style).background
 }
 
-export function borderColor(layer: Layer, style?: Styles): string
-export function borderColor(
+export function border_color(layer: Layer, style?: Styles): string
+export function border_color(
     layer: Layer,
-    styleSet?: StyleSets,
+    style_set?: StyleSets,
     style?: Styles
 ): string
-export function borderColor(
+export function border_color(
     layer: Layer,
-    styleSetOrStyles?: StyleSets | Styles,
+    style_set_or_styles?: StyleSets | Styles,
     style?: Styles
 ): string {
-    return getStyle(layer, styleSetOrStyles, style).border
+    return get_style(layer, style_set_or_styles, style).border
 }
 
 export function foreground(layer: Layer, style?: Styles): string
 export function foreground(
     layer: Layer,
-    styleSet?: StyleSets,
+    style_set?: StyleSets,
     style?: Styles
 ): string
 export function foreground(
     layer: Layer,
-    styleSetOrStyles?: StyleSets | Styles,
+    style_set_or_styles?: StyleSets | Styles,
     style?: Styles
 ): string {
-    return getStyle(layer, styleSetOrStyles, style).foreground
+    return get_style(layer, style_set_or_styles, style).foreground
 }
 
-interface Text {
-    family: keyof typeof fontFamilies
+export interface TextStyle extends Object {
+    family: keyof typeof font_families
     color: string
     size: number
     weight?: FontWeight
@@ -94,7 +94,7 @@ interface Text {
 }
 
 export interface TextProperties {
-    size?: keyof typeof fontSizes
+    size?: keyof typeof font_sizes
     weight?: FontWeight
     underline?: boolean
     color?: string
@@ -174,49 +174,53 @@ interface FontFeatures {
 
 export function text(
     layer: Layer,
-    fontFamily: keyof typeof fontFamilies,
-    styleSet: StyleSets,
+    font_family: keyof typeof font_families,
+    style_set: StyleSets,
     style: Styles,
     properties?: TextProperties
-): Text
+): TextStyle
 export function text(
     layer: Layer,
-    fontFamily: keyof typeof fontFamilies,
-    styleSet: StyleSets,
+    font_family: keyof typeof font_families,
+    style_set: StyleSets,
     properties?: TextProperties
-): Text
+): TextStyle
 export function text(
     layer: Layer,
-    fontFamily: keyof typeof fontFamilies,
+    font_family: keyof typeof font_families,
     style: Styles,
     properties?: TextProperties
-): Text
+): TextStyle
 export function text(
     layer: Layer,
-    fontFamily: keyof typeof fontFamilies,
+    font_family: keyof typeof font_families,
     properties?: TextProperties
-): Text
+): TextStyle
 export function text(
     layer: Layer,
-    fontFamily: keyof typeof fontFamilies,
-    styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
-    styleOrProperties?: Styles | TextProperties,
+    font_family: keyof typeof font_families,
+    style_set_style_or_properties?: StyleSets | Styles | TextProperties,
+    style_or_properties?: Styles | TextProperties,
     properties?: TextProperties
 ) {
-    let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+    const style = get_style(
+        layer,
+        style_set_style_or_properties,
+        style_or_properties
+    )
 
-    if (typeof styleSetStyleOrProperties === "object") {
-        properties = styleSetStyleOrProperties
+    if (typeof style_set_style_or_properties === "object") {
+        properties = style_set_style_or_properties
     }
-    if (typeof styleOrProperties === "object") {
-        properties = styleOrProperties
+    if (typeof style_or_properties === "object") {
+        properties = style_or_properties
     }
 
-    let size = fontSizes[properties?.size || "sm"]
-    let color = properties?.color || style.foreground
+    const size = font_sizes[properties?.size || "sm"]
+    const color = properties?.color || style.foreground
 
     return {
-        family: fontFamilies[fontFamily],
+        family: font_families[font_family],
         ...properties,
         color,
         size,
@@ -244,13 +248,13 @@ export interface BorderProperties {
 
 export function border(
     layer: Layer,
-    styleSet: StyleSets,
+    style_set: StyleSets,
     style: Styles,
     properties?: BorderProperties
 ): Border
 export function border(
     layer: Layer,
-    styleSet: StyleSets,
+    style_set: StyleSets,
     properties?: BorderProperties
 ): Border
 export function border(
@@ -261,17 +265,17 @@ export function border(
 export function border(layer: Layer, properties?: BorderProperties): Border
 export function border(
     layer: Layer,
-    styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
-    styleOrProperties?: Styles | BorderProperties,
+    style_set_or_properties?: StyleSets | Styles | BorderProperties,
+    style_or_properties?: Styles | BorderProperties,
     properties?: BorderProperties
 ): Border {
-    let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
+    const style = get_style(layer, style_set_or_properties, style_or_properties)
 
-    if (typeof styleSetStyleOrProperties === "object") {
-        properties = styleSetStyleOrProperties
+    if (typeof style_set_or_properties === "object") {
+        properties = style_set_or_properties
     }
-    if (typeof styleOrProperties === "object") {
-        properties = styleOrProperties
+    if (typeof style_or_properties === "object") {
+        properties = style_or_properties
     }
 
     return {
@@ -283,9 +287,9 @@ export function border(
 
 export function svg(
     color: string,
-    asset: String,
-    width: Number,
-    height: Number
+    asset: string,
+    width: number,
+    height: number
 ) {
     return {
         color,

styles/src/style_tree/contact_finder.ts πŸ”—

@@ -0,0 +1,74 @@
+import picker from "./picker"
+import { background, border, foreground, text } from "./components"
+import { useTheme } from "../theme"
+
+export default function contact_finder(): any {
+    const theme = useTheme()
+
+    const side_margin = 6
+    const contact_button = {
+        background: background(theme.middle, "variant"),
+        color: foreground(theme.middle, "variant"),
+        icon_width: 8,
+        button_width: 16,
+        corner_radius: 8,
+    }
+
+    const picker_style = picker()
+    const picker_input = {
+        background: background(theme.middle, "on"),
+        corner_radius: 6,
+        text: text(theme.middle, "mono"),
+        placeholder_text: text(theme.middle, "mono", "on", "disabled", {
+            size: "xs",
+        }),
+        selection: theme.players[0],
+        border: border(theme.middle),
+        padding: {
+            bottom: 4,
+            left: 8,
+            right: 8,
+            top: 4,
+        },
+        margin: {
+            left: side_margin,
+            right: side_margin,
+        },
+    }
+
+    return {
+        picker: {
+            empty_container: {},
+            item: {
+                ...picker_style.item,
+                margin: { left: side_margin, right: side_margin },
+            },
+            no_matches: picker_style.no_matches,
+            input_editor: picker_input,
+            empty_input_editor: picker_input,
+            header: picker_style.header,
+            footer: picker_style.footer,
+        },
+        row_height: 28,
+        contact_avatar: {
+            corner_radius: 10,
+            width: 18,
+        },
+        contact_username: {
+            padding: {
+                left: 8,
+            },
+        },
+        contact_button: {
+            ...contact_button,
+            hover: {
+                background: background(theme.middle, "variant", "hovered"),
+            },
+        },
+        disabled_contact_button: {
+            ...contact_button,
+            background: background(theme.middle, "disabled"),
+            color: foreground(theme.middle, "disabled"),
+        },
+    }
+}

styles/src/style_tree/contact_list.ts πŸ”—

@@ -0,0 +1,247 @@
+import {
+    background,
+    border,
+    border_color,
+    foreground,
+    text,
+} from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+export default function contacts_panel(): any {
+    const theme = useTheme()
+
+    const name_margin = 8
+    const side_padding = 12
+
+    const layer = theme.middle
+
+    const contact_button = {
+        background: background(layer, "on"),
+        color: foreground(layer, "on"),
+        icon_width: 8,
+        button_width: 16,
+        corner_radius: 8,
+    }
+    const project_row = {
+        guest_avatar_spacing: 4,
+        height: 24,
+        guest_avatar: {
+            corner_radius: 8,
+            width: 14,
+        },
+        name: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: {
+                left: name_margin,
+                right: 6,
+            },
+        },
+        guests: {
+            margin: {
+                left: name_margin,
+                right: name_margin,
+            },
+        },
+        padding: {
+            left: side_padding,
+            right: side_padding,
+        },
+    }
+
+    return {
+        background: background(layer),
+        padding: { top: 12 },
+        user_query_editor: {
+            background: background(layer, "on"),
+            corner_radius: 6,
+            text: text(layer, "mono", "on"),
+            placeholder_text: text(layer, "mono", "on", "disabled", {
+                size: "xs",
+            }),
+            selection: theme.players[0],
+            border: border(layer, "on"),
+            padding: {
+                bottom: 4,
+                left: 8,
+                right: 8,
+                top: 4,
+            },
+            margin: {
+                left: 6,
+            },
+        },
+        user_query_editor_height: 33,
+        add_contact_button: {
+            margin: { left: 6, right: 12 },
+            color: foreground(layer, "on"),
+            button_width: 28,
+            icon_width: 16,
+        },
+        row_height: 28,
+        section_icon_size: 8,
+        header_row: toggleable({
+            base: interactive({
+                base: {
+                    ...text(layer, "mono", { size: "sm" }),
+                    margin: { top: 14 },
+                    padding: {
+                        left: side_padding,
+                        right: side_padding,
+                    },
+                    background: background(layer, "default"), // posiewic: breaking change
+                },
+                state: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                    clicked: {
+                        background: background(layer, "pressed"),
+                    },
+                }, // hack, we want headerRow to be interactive for whatever reason. It probably shouldn't be interactive in the first place.
+            }),
+            state: {
+                active: {
+                    default: {
+                        ...text(layer, "mono", "active", { size: "sm" }),
+                        background: background(layer, "active"),
+                    },
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                    clicked: {
+                        background: background(layer, "pressed"),
+                    },
+                },
+            },
+        }),
+        leave_call: interactive({
+            base: {
+                background: background(layer),
+                border: border(layer),
+                corner_radius: 6,
+                margin: {
+                    top: 1,
+                },
+                padding: {
+                    top: 1,
+                    bottom: 1,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(layer, "sans", "variant", { size: "xs" }),
+            },
+            state: {
+                hovered: {
+                    ...text(layer, "sans", "hovered", { size: "xs" }),
+                    background: background(layer, "hovered"),
+                    border: border(layer, "hovered"),
+                },
+            },
+        }),
+        contact_row: {
+            inactive: {
+                default: {
+                    padding: {
+                        left: side_padding,
+                        right: side_padding,
+                    },
+                },
+            },
+            active: {
+                default: {
+                    background: background(layer, "active"),
+                    padding: {
+                        left: side_padding,
+                        right: side_padding,
+                    },
+                },
+            },
+        },
+        contact_avatar: {
+            corner_radius: 10,
+            width: 18,
+        },
+        contact_status_free: {
+            corner_radius: 4,
+            padding: 4,
+            margin: { top: 12, left: 12 },
+            background: foreground(layer, "positive"),
+        },
+        contact_status_busy: {
+            corner_radius: 4,
+            padding: 4,
+            margin: { top: 12, left: 12 },
+            background: foreground(layer, "negative"),
+        },
+        contact_username: {
+            ...text(layer, "mono", { size: "sm" }),
+            margin: {
+                left: name_margin,
+            },
+        },
+        contact_button_spacing: name_margin,
+        contact_button: interactive({
+            base: { ...contact_button },
+            state: {
+                hovered: {
+                    background: background(layer, "hovered"),
+                },
+            },
+        }),
+        disabled_button: {
+            ...contact_button,
+            background: background(layer, "on"),
+            color: foreground(layer, "on"),
+        },
+        calling_indicator: {
+            ...text(layer, "mono", "variant", { size: "xs" }),
+        },
+        tree_branch: toggleable({
+            base: interactive({
+                base: {
+                    color: border_color(layer),
+                    width: 1,
+                },
+                state: {
+                    hovered: {
+                        color: border_color(layer),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        color: border_color(layer),
+                    },
+                },
+            },
+        }),
+        project_row: toggleable({
+            base: interactive({
+                base: {
+                    ...project_row,
+                    background: background(layer),
+                    icon: {
+                        margin: { left: name_margin },
+                        color: foreground(layer, "variant"),
+                        width: 12,
+                    },
+                    name: {
+                        ...project_row.name,
+                        ...text(layer, "mono", { size: "sm" }),
+                    },
+                },
+                state: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: { background: background(layer, "active") },
+                },
+            },
+        }),
+    }
+}

styles/src/style_tree/contact_notification.ts πŸ”—

@@ -0,0 +1,55 @@
+import { background, foreground, text } from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function contact_notification(): any {
+    const theme = useTheme()
+
+    const avatar_size = 12
+    const header_padding = 8
+
+    return {
+        header_avatar: {
+            height: avatar_size,
+            width: avatar_size,
+            corner_radius: 6,
+        },
+        header_message: {
+            ...text(theme.lowest, "sans", { size: "xs" }),
+            margin: { left: header_padding, right: header_padding },
+        },
+        header_height: 18,
+        body_message: {
+            ...text(theme.lowest, "sans", { size: "xs" }),
+            margin: { left: avatar_size + header_padding, top: 6, bottom: 6 },
+        },
+        button: interactive({
+            base: {
+                ...text(theme.lowest, "sans", "on", { size: "xs" }),
+                background: background(theme.lowest, "on"),
+                padding: 4,
+                corner_radius: 6,
+                margin: { left: 6 },
+            },
+
+            state: {
+                hovered: {
+                    background: background(theme.lowest, "on", "hovered"),
+                },
+            },
+        }),
+
+        dismiss_button: {
+            default: {
+                color: foreground(theme.lowest, "variant"),
+                icon_width: 8,
+                icon_height: 8,
+                button_width: 8,
+                button_height: 8,
+                hover: {
+                    color: foreground(theme.lowest, "hovered"),
+                },
+            },
+        },
+    }
+}

styles/src/style_tree/contacts_popover.ts πŸ”—

@@ -0,0 +1,16 @@
+import { useTheme } from "../theme"
+import { background, border } from "./components"
+
+export default function contacts_popover(): any {
+    const theme = useTheme()
+
+    return {
+        background: background(theme.middle),
+        corner_radius: 6,
+        padding: { top: 6, bottom: 6 },
+        shadow: theme.popover_shadow,
+        border: border(theme.middle),
+        width: 300,
+        height: 400,
+    }
+}

styles/src/style_tree/context_menu.ts πŸ”—

@@ -0,0 +1,70 @@
+import { background, border, border_color, text } from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function context_menu(): any {
+    const theme = useTheme()
+
+    return {
+        background: background(theme.middle),
+        corner_radius: 10,
+        padding: 4,
+        shadow: theme.popover_shadow,
+        border: border(theme.middle),
+        keystroke_margin: 30,
+        item: toggleable({
+            base: interactive({
+                base: {
+                    icon_spacing: 8,
+                    icon_width: 14,
+                    padding: { left: 6, right: 6, top: 2, bottom: 2 },
+                    corner_radius: 6,
+                    label: text(theme.middle, "sans", { size: "sm" }),
+                    keystroke: {
+                        ...text(theme.middle, "sans", "variant", {
+                            size: "sm",
+                            weight: "bold",
+                        }),
+                        padding: { left: 3, right: 3 },
+                    },
+                },
+                state: {
+                    hovered: {
+                        background: background(theme.middle, "hovered"),
+                        label: text(theme.middle, "sans", "hovered", {
+                            size: "sm",
+                        }),
+                        keystroke: {
+                            ...text(theme.middle, "sans", "hovered", {
+                                size: "sm",
+                                weight: "bold",
+                            }),
+                            padding: { left: 3, right: 3 },
+                        },
+                    },
+                    clicked: {
+                        background: background(theme.middle, "pressed"),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        background: background(theme.middle, "active"),
+                    },
+                    hovered: {
+                        background: background(theme.middle, "hovered"),
+                    },
+                    clicked: {
+                        background: background(theme.middle, "pressed"),
+                    },
+                },
+            },
+        }),
+
+        separator: {
+            background: border_color(theme.middle),
+            margin: { top: 2, bottom: 2 },
+        },
+    }
+}

styles/src/style_tree/copilot.ts πŸ”—

@@ -0,0 +1,293 @@
+import { background, border, foreground, svg, text } from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+export default function copilot(): any {
+    const theme = useTheme()
+
+    const content_width = 264
+
+    const cta_button =
+        // Copied from welcome screen. FIXME: Move this into a ZDS component
+        interactive({
+            base: {
+                background: background(theme.middle),
+                border: border(theme.middle, "default"),
+                corner_radius: 4,
+                margin: {
+                    top: 4,
+                    bottom: 4,
+                    left: 8,
+                    right: 8,
+                },
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(theme.middle, "sans", "default", { size: "sm" }),
+            },
+            state: {
+                hovered: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "hovered"),
+                    border: border(theme.middle, "active"),
+                },
+            },
+        })
+
+    return {
+        out_link_icon: interactive({
+            base: {
+                icon: svg(
+                    foreground(theme.middle, "variant"),
+                    "icons/link_out_12.svg",
+                    12,
+                    12
+                ),
+                container: {
+                    corner_radius: 6,
+                    padding: { left: 6 },
+                },
+            },
+            state: {
+                hovered: {
+                    icon: {
+                        color: foreground(theme.middle, "hovered"),
+                    },
+                },
+            },
+        }),
+
+        modal: {
+            title_text: {
+                default: {
+                    ...text(theme.middle, "sans", {
+                        size: "xs",
+                        weight: "bold",
+                    }),
+                },
+            },
+            titlebar: {
+                background: background(theme.lowest),
+                border: border(theme.middle, "active"),
+                padding: {
+                    top: 4,
+                    bottom: 4,
+                    left: 8,
+                    right: 8,
+                },
+            },
+            container: {
+                background: background(theme.lowest),
+                padding: {
+                    top: 0,
+                    left: 0,
+                    right: 0,
+                    bottom: 8,
+                },
+            },
+            close_icon: interactive({
+                base: {
+                    icon: svg(
+                        foreground(theme.middle, "variant"),
+                        "icons/x_mark_8.svg",
+                        8,
+                        8
+                    ),
+                    container: {
+                        corner_radius: 2,
+                        padding: {
+                            top: 4,
+                            bottom: 4,
+                            left: 4,
+                            right: 4,
+                        },
+                        margin: {
+                            right: 0,
+                        },
+                    },
+                },
+                state: {
+                    hovered: {
+                        icon: svg(
+                            foreground(theme.middle, "on"),
+                            "icons/x_mark_8.svg",
+                            8,
+                            8
+                        ),
+                    },
+                    clicked: {
+                        icon: svg(
+                            foreground(theme.middle, "base"),
+                            "icons/x_mark_8.svg",
+                            8,
+                            8
+                        ),
+                    },
+                },
+            }),
+            dimensions: {
+                width: 280,
+                height: 280,
+            },
+        },
+
+        auth: {
+            content_width,
+
+            cta_button,
+
+            header: {
+                icon: svg(
+                    foreground(theme.middle, "default"),
+                    "icons/zed_plus_copilot_32.svg",
+                    92,
+                    32
+                ),
+                container: {
+                    margin: {
+                        top: 35,
+                        bottom: 5,
+                        left: 0,
+                        right: 0,
+                    },
+                },
+            },
+
+            prompting: {
+                subheading: {
+                    ...text(theme.middle, "sans", { size: "xs" }),
+                    margin: {
+                        top: 6,
+                        bottom: 12,
+                        left: 0,
+                        right: 0,
+                    },
+                },
+
+                hint: {
+                    ...text(theme.middle, "sans", {
+                        size: "xs",
+                        color: "#838994",
+                    }),
+                    margin: {
+                        top: 6,
+                        bottom: 2,
+                    },
+                },
+
+                device_code: {
+                    text: text(theme.middle, "mono", { size: "sm" }),
+                    cta: {
+                        ...cta_button,
+                        background: background(theme.lowest),
+                        border: border(theme.lowest, "inverted"),
+                        padding: {
+                            top: 0,
+                            bottom: 0,
+                            left: 16,
+                            right: 16,
+                        },
+                        margin: {
+                            left: 16,
+                            right: 16,
+                        },
+                    },
+                    left: content_width / 2,
+                    left_container: {
+                        padding: {
+                            top: 3,
+                            bottom: 3,
+                            left: 0,
+                            right: 6,
+                        },
+                    },
+                    right: (content_width * 1) / 3,
+                    right_container: interactive({
+                        base: {
+                            border: border(theme.lowest, "inverted", {
+                                bottom: false,
+                                right: false,
+                                top: false,
+                                left: true,
+                            }),
+                            padding: {
+                                top: 3,
+                                bottom: 5,
+                                left: 8,
+                                right: 0,
+                            },
+                        },
+                        state: {
+                            hovered: {
+                                border: border(theme.middle, "active", {
+                                    bottom: false,
+                                    right: false,
+                                    top: false,
+                                    left: true,
+                                }),
+                            },
+                        },
+                    }),
+                },
+            },
+
+            not_authorized: {
+                subheading: {
+                    ...text(theme.middle, "sans", { size: "xs" }),
+
+                    margin: {
+                        top: 16,
+                        bottom: 16,
+                        left: 0,
+                        right: 0,
+                    },
+                },
+
+                warning: {
+                    ...text(theme.middle, "sans", {
+                        size: "xs",
+                        color: foreground(theme.middle, "warning"),
+                    }),
+                    border: border(theme.middle, "warning"),
+                    background: background(theme.middle, "warning"),
+                    corner_radius: 2,
+                    padding: {
+                        top: 4,
+                        left: 4,
+                        bottom: 4,
+                        right: 4,
+                    },
+                    margin: {
+                        bottom: 16,
+                        left: 8,
+                        right: 8,
+                    },
+                },
+            },
+
+            authorized: {
+                subheading: {
+                    ...text(theme.middle, "sans", { size: "xs" }),
+
+                    margin: {
+                        top: 16,
+                        bottom: 16,
+                    },
+                },
+
+                hint: {
+                    ...text(theme.middle, "sans", {
+                        size: "xs",
+                        color: "#838994",
+                    }),
+                    margin: {
+                        top: 24,
+                        bottom: 4,
+                    },
+                },
+            },
+        },
+    }
+}

styles/src/style_tree/editor.ts πŸ”—

@@ -0,0 +1,319 @@
+import { with_opacity } from "../theme/color"
+import { Layer, StyleSets } from "../theme/create_theme"
+import {
+    background,
+    border,
+    border_color,
+    foreground,
+    text,
+} from "./components"
+import hover_popover from "./hover_popover"
+
+import { build_syntax } from "../theme/syntax"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function editor(): any {
+    const theme = useTheme()
+
+    const { is_light } = theme
+
+    const layer = theme.highest
+
+    const autocomplete_item = {
+        corner_radius: 6,
+        padding: {
+            bottom: 2,
+            left: 6,
+            right: 6,
+            top: 2,
+        },
+    }
+
+    function diagnostic(layer: Layer, style_set: StyleSets) {
+        return {
+            text_scale_factor: 0.857,
+            header: {
+                border: border(layer, {
+                    top: true,
+                }),
+            },
+            message: {
+                text: text(layer, "sans", style_set, "default", { size: "sm" }),
+                highlight_text: text(layer, "sans", style_set, "default", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+        }
+    }
+
+    const syntax = build_syntax()
+
+    return {
+        text_color: syntax.primary.color,
+        background: background(layer),
+        active_line_background: with_opacity(background(layer, "on"), 0.75),
+        highlighted_line_background: background(layer, "on"),
+        // Inline autocomplete suggestions, Co-pilot suggestions, etc.
+        hint: syntax.hint,
+        suggestion: syntax.predictive,
+        code_actions: {
+            indicator: toggleable({
+                base: interactive({
+                    base: {
+                        color: foreground(layer, "variant"),
+                    },
+                    state: {
+                        hovered: {
+                            color: foreground(layer, "variant", "hovered"),
+                        },
+                        clicked: {
+                            color: foreground(layer, "variant", "pressed"),
+                        },
+                    },
+                }),
+                state: {
+                    active: {
+                        default: {
+                            color: foreground(layer, "accent"),
+                        },
+                        hovered: {
+                            color: foreground(layer, "accent", "hovered"),
+                        },
+                        clicked: {
+                            color: foreground(layer, "accent", "pressed"),
+                        },
+                    },
+                },
+            }),
+
+            vertical_scale: 0.55,
+        },
+        folds: {
+            icon_margin_scale: 2.5,
+            folded_icon: "icons/chevron_right_8.svg",
+            foldable_icon: "icons/chevron_down_8.svg",
+            indicator: toggleable({
+                base: interactive({
+                    base: {
+                        color: foreground(layer, "variant"),
+                    },
+                    state: {
+                        hovered: {
+                            color: foreground(layer, "on"),
+                        },
+                        clicked: {
+                            color: foreground(layer, "base"),
+                        },
+                    },
+                }),
+                state: {
+                    active: {
+                        default: {
+                            color: foreground(layer, "default"),
+                        },
+                        hovered: {
+                            color: foreground(layer, "variant"),
+                        },
+                    },
+                },
+            }),
+            ellipses: {
+                text_color: theme.ramps.neutral(0.71).hex(),
+                corner_radius_factor: 0.15,
+                background: {
+                    // Copied from hover_popover highlight
+                    default: {
+                        color: theme.ramps.neutral(0.5).alpha(0.0).hex(),
+                    },
+
+                    hovered: {
+                        color: theme.ramps.neutral(0.5).alpha(0.5).hex(),
+                    },
+
+                    clicked: {
+                        color: theme.ramps.neutral(0.5).alpha(0.7).hex(),
+                    },
+                },
+            },
+            fold_background: foreground(layer, "variant"),
+        },
+        diff: {
+            deleted: is_light
+                ? theme.ramps.red(0.5).hex()
+                : theme.ramps.red(0.4).hex(),
+            modified: is_light
+                ? theme.ramps.yellow(0.5).hex()
+                : theme.ramps.yellow(0.5).hex(),
+            inserted: is_light
+                ? theme.ramps.green(0.4).hex()
+                : theme.ramps.green(0.5).hex(),
+            removed_width_em: 0.275,
+            width_em: 0.15,
+            corner_radius: 0.05,
+        },
+        /** Highlights matching occurrences of what is under the cursor
+         * as well as matched brackets
+         */
+        document_highlight_read_background: with_opacity(
+            foreground(layer, "accent"),
+            0.1
+        ),
+        document_highlight_write_background: theme.ramps
+            .neutral(0.5)
+            .alpha(0.4)
+            .hex(), // TODO: This was blend * 2
+        error_color: background(layer, "negative"),
+        gutter_background: background(layer),
+        gutter_padding_factor: 3.5,
+        line_number: with_opacity(foreground(layer), 0.35),
+        line_number_active: foreground(layer),
+        rename_fade: 0.6,
+        unnecessary_code_fade: 0.5,
+        selection: theme.players[0],
+        whitespace: theme.ramps.neutral(0.5).hex(),
+        guest_selections: [
+            theme.players[1],
+            theme.players[2],
+            theme.players[3],
+            theme.players[4],
+            theme.players[5],
+            theme.players[6],
+            theme.players[7],
+        ],
+        autocomplete: {
+            background: background(theme.middle),
+            corner_radius: 8,
+            padding: 4,
+            margin: {
+                left: -14,
+            },
+            border: border(theme.middle),
+            shadow: theme.popover_shadow,
+            match_highlight: foreground(theme.middle, "accent"),
+            item: autocomplete_item,
+            hovered_item: {
+                ...autocomplete_item,
+                match_highlight: foreground(theme.middle, "accent", "hovered"),
+                background: background(theme.middle, "hovered"),
+            },
+            selected_item: {
+                ...autocomplete_item,
+                match_highlight: foreground(theme.middle, "accent", "active"),
+                background: background(theme.middle, "active"),
+            },
+        },
+        diagnostic_header: {
+            background: background(theme.middle),
+            icon_width_factor: 1.5,
+            text_scale_factor: 0.857,
+            border: border(theme.middle, {
+                bottom: true,
+                top: true,
+            }),
+            code: {
+                ...text(theme.middle, "mono", { size: "sm" }),
+                margin: {
+                    left: 10,
+                },
+            },
+            source: {
+                text: text(theme.middle, "sans", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+            },
+            message: {
+                highlight_text: text(theme.middle, "sans", {
+                    size: "sm",
+                    weight: "bold",
+                }),
+                text: text(theme.middle, "sans", { size: "sm" }),
+            },
+        },
+        diagnostic_path_header: {
+            background: background(theme.middle),
+            text_scale_factor: 0.857,
+            filename: text(theme.middle, "mono", { size: "sm" }),
+            path: {
+                ...text(theme.middle, "mono", { size: "sm" }),
+                margin: {
+                    left: 12,
+                },
+            },
+        },
+        error_diagnostic: diagnostic(theme.middle, "negative"),
+        warning_diagnostic: diagnostic(theme.middle, "warning"),
+        information_diagnostic: diagnostic(theme.middle, "accent"),
+        hint_diagnostic: diagnostic(theme.middle, "warning"),
+        invalid_error_diagnostic: diagnostic(theme.middle, "base"),
+        invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
+        invalid_information_diagnostic: diagnostic(theme.middle, "base"),
+        invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
+        hover_popover: hover_popover(),
+        link_definition: {
+            color: syntax.link_uri.color,
+            underline: syntax.link_uri.underline,
+        },
+        jump_icon: interactive({
+            base: {
+                color: foreground(layer, "on"),
+                icon_width: 20,
+                button_width: 20,
+                corner_radius: 6,
+                padding: {
+                    top: 6,
+                    bottom: 6,
+                    left: 6,
+                    right: 6,
+                },
+            },
+            state: {
+                hovered: {
+                    background: background(layer, "on", "hovered"),
+                },
+            },
+        }),
+
+        scrollbar: {
+            width: 12,
+            min_height_factor: 1.0,
+            track: {
+                border: border(layer, "variant", { left: true }),
+            },
+            thumb: {
+                background: with_opacity(background(layer, "inverted"), 0.3),
+                border: {
+                    width: 1,
+                    color: border_color(layer, "variant"),
+                    top: false,
+                    right: true,
+                    left: true,
+                    bottom: false,
+                },
+            },
+            git: {
+                deleted: is_light
+                    ? with_opacity(theme.ramps.red(0.5).hex(), 0.8)
+                    : with_opacity(theme.ramps.red(0.4).hex(), 0.8),
+                modified: is_light
+                    ? with_opacity(theme.ramps.yellow(0.5).hex(), 0.8)
+                    : with_opacity(theme.ramps.yellow(0.4).hex(), 0.8),
+                inserted: is_light
+                    ? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
+                    : with_opacity(theme.ramps.green(0.4).hex(), 0.8),
+            },
+            selections: is_light
+                ? with_opacity(theme.ramps.blue(0.5).hex(), 0.8)
+                : with_opacity(theme.ramps.blue(0.4).hex(), 0.8)
+        },
+        composition_mark: {
+            underline: {
+                thickness: 1.0,
+                color: border_color(layer),
+            },
+        },
+        syntax,
+    }
+}

styles/src/style_tree/feedback.ts πŸ”—

@@ -0,0 +1,51 @@
+import { background, border, text } from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function feedback(): any {
+    const theme = useTheme()
+
+    return {
+        submit_button: interactive({
+            base: {
+                ...text(theme.highest, "mono", "on"),
+                background: background(theme.highest, "on"),
+                corner_radius: 6,
+                border: border(theme.highest, "on"),
+                margin: {
+                    right: 4,
+                },
+                padding: {
+                    bottom: 2,
+                    left: 10,
+                    right: 10,
+                    top: 2,
+                },
+            },
+            state: {
+                clicked: {
+                    ...text(theme.highest, "mono", "on", "pressed"),
+                    background: background(theme.highest, "on", "pressed"),
+                    border: border(theme.highest, "on", "pressed"),
+                },
+                hovered: {
+                    ...text(theme.highest, "mono", "on", "hovered"),
+                    background: background(theme.highest, "on", "hovered"),
+                    border: border(theme.highest, "on", "hovered"),
+                },
+            },
+        }),
+        button_margin: 8,
+        info_text_default: text(theme.highest, "sans", "default", {
+            size: "xs",
+        }),
+        link_text_default: text(theme.highest, "sans", "default", {
+            size: "xs",
+            underline: true,
+        }),
+        link_text_hover: text(theme.highest, "sans", "hovered", {
+            size: "xs",
+            underline: true,
+        }),
+    }
+}

styles/src/style_tree/hover_popover.ts πŸ”—

@@ -0,0 +1,49 @@
+import { useTheme } from "../theme"
+import { background, border, foreground, text } from "./components"
+
+export default function hover_popover(): any {
+    const theme = useTheme()
+
+    const base_container = {
+        background: background(theme.middle),
+        corner_radius: 8,
+        padding: {
+            left: 8,
+            right: 8,
+            top: 4,
+            bottom: 4,
+        },
+        shadow: theme.popover_shadow,
+        border: border(theme.middle),
+        margin: {
+            left: -8,
+        },
+    }
+
+    return {
+        container: base_container,
+        info_container: {
+            ...base_container,
+            background: background(theme.middle, "accent"),
+            border: border(theme.middle, "accent"),
+        },
+        warning_container: {
+            ...base_container,
+            background: background(theme.middle, "warning"),
+            border: border(theme.middle, "warning"),
+        },
+        error_container: {
+            ...base_container,
+            background: background(theme.middle, "negative"),
+            border: border(theme.middle, "negative"),
+        },
+        block_style: {
+            padding: { top: 4 },
+        },
+        prose: text(theme.middle, "sans", { size: "sm" }),
+        diagnostic_source_highlight: {
+            color: foreground(theme.middle, "accent"),
+        },
+        highlight: theme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
+    }
+}

styles/src/style_tree/incoming_call_notification.ts πŸ”—

@@ -0,0 +1,55 @@
+import { useTheme } from "../theme"
+import { background, border, text } from "./components"
+
+export default function incoming_call_notification(): unknown {
+    const theme = useTheme()
+
+    const avatar_size = 48
+    return {
+        window_height: 74,
+        window_width: 380,
+        background: background(theme.middle),
+        caller_container: {
+            padding: 12,
+        },
+        caller_avatar: {
+            height: avatar_size,
+            width: avatar_size,
+            corner_radius: avatar_size / 2,
+        },
+        caller_metadata: {
+            margin: { left: 10 },
+        },
+        caller_username: {
+            ...text(theme.middle, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        caller_message: {
+            ...text(theme.middle, "sans", "variant", { size: "xs" }),
+            margin: { top: -3 },
+        },
+        worktree_roots: {
+            ...text(theme.middle, "sans", "variant", {
+                size: "xs",
+                weight: "bold",
+            }),
+            margin: { top: -3 },
+        },
+        button_width: 96,
+        accept_button: {
+            background: background(theme.middle, "accent"),
+            border: border(theme.middle, { left: true, bottom: true }),
+            ...text(theme.middle, "sans", "positive", {
+                size: "xs",
+                weight: "bold",
+            }),
+        },
+        decline_button: {
+            border: border(theme.middle, { left: true }),
+            ...text(theme.middle, "sans", "negative", {
+                size: "xs",
+                weight: "bold",
+            }),
+        },
+    }
+}

styles/src/style_tree/picker.ts πŸ”—

@@ -0,0 +1,132 @@
+import { with_opacity } from "../theme/color"
+import { background, border, text } from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function picker(): any {
+    const theme = useTheme()
+
+    const container = {
+        background: background(theme.lowest),
+        border: border(theme.lowest),
+        shadow: theme.modal_shadow,
+        corner_radius: 12,
+        padding: {
+            bottom: 4,
+        },
+    }
+    const input_editor = {
+        placeholder_text: text(theme.lowest, "sans", "on", "disabled"),
+        selection: theme.players[0],
+        text: text(theme.lowest, "mono", "on"),
+        border: border(theme.lowest, { bottom: true }),
+        padding: {
+            bottom: 8,
+            left: 16,
+            right: 16,
+            top: 8,
+        },
+        margin: {
+            bottom: 4,
+        },
+    }
+    const empty_input_editor: any = { ...input_editor }
+    delete empty_input_editor.border
+    delete empty_input_editor.margin
+
+    return {
+        ...container,
+        empty_container: {
+            ...container,
+            padding: {},
+        },
+        item: toggleable({
+            base: interactive({
+                base: {
+                    padding: {
+                        bottom: 4,
+                        left: 12,
+                        right: 12,
+                        top: 4,
+                    },
+                    margin: {
+                        top: 1,
+                        left: 4,
+                        right: 4,
+                    },
+                    corner_radius: 8,
+                    text: text(theme.lowest, "sans", "variant"),
+                    highlight_text: text(theme.lowest, "sans", "accent", {
+                        weight: "bold",
+                    }),
+                },
+                state: {
+                    hovered: {
+                        background: with_opacity(
+                            background(theme.lowest, "hovered"),
+                            0.5
+                        ),
+                    },
+                    clicked: {
+                        background: with_opacity(
+                            background(theme.lowest, "pressed"),
+                            0.5
+                        ),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        background: with_opacity(
+                            background(theme.lowest, "base", "active"),
+                            0.5
+                        ),
+                    },
+                    hovered: {
+                        background: with_opacity(
+                            background(theme.lowest, "hovered"),
+                            0.5
+                        ),
+                    },
+                    clicked: {
+                        background: with_opacity(
+                            background(theme.lowest, "pressed"),
+                            0.5
+                        ),
+                    },
+                },
+            },
+        }),
+
+        input_editor,
+        empty_input_editor,
+        no_matches: {
+            text: text(theme.lowest, "sans", "variant"),
+            padding: {
+                bottom: 8,
+                left: 16,
+                right: 16,
+                top: 8,
+            },
+        },
+        header: {
+            text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+
+            margin: {
+                top: 1,
+                left: 8,
+                right: 8,
+            },
+        },
+        footer: {
+            text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+            margin: {
+                top: 1,
+                left: 8,
+                right: 8,
+            },
+
+        }
+    }
+}

styles/src/style_tree/project_diagnostics.ts πŸ”—

@@ -0,0 +1,14 @@
+import { useTheme } from "../theme"
+import { background, text } from "./components"
+
+export default function project_diagnostics(): any {
+    const theme = useTheme()
+
+    return {
+        background: background(theme.highest),
+        tab_icon_spacing: 4,
+        tab_icon_width: 13,
+        tab_summary_spacing: 10,
+        empty_message: text(theme.highest, "sans", "variant", { size: "md" }),
+    }
+}

styles/src/style_tree/project_panel.ts πŸ”—

@@ -0,0 +1,199 @@
+import { with_opacity } from "../theme/color"
+import {
+    Border,
+    TextStyle,
+    background,
+    border,
+    foreground,
+    text,
+} from "./components"
+import { interactive, toggleable } from "../element"
+import merge from "ts-deepmerge"
+import { useTheme } from "../theme"
+export default function project_panel(): any {
+    const theme = useTheme()
+
+    const { is_light } = theme
+
+    type EntryStateProps = {
+        background?: string
+        border?: Border
+        text?: TextStyle
+        icon_color?: string
+    }
+
+    type EntryState = {
+        default: EntryStateProps
+        hovered?: EntryStateProps
+        clicked?: EntryStateProps
+    }
+
+    const entry = (unselected?: EntryState, selected?: EntryState) => {
+        const git_status = {
+            git: {
+                modified: is_light
+                    ? theme.ramps.yellow(0.6).hex()
+                    : theme.ramps.yellow(0.5).hex(),
+                inserted: is_light
+                    ? theme.ramps.green(0.45).hex()
+                    : theme.ramps.green(0.5).hex(),
+                conflict: is_light
+                    ? theme.ramps.red(0.6).hex()
+                    : theme.ramps.red(0.5).hex(),
+            },
+        }
+
+        const base_properties = {
+            height: 22,
+            background: background(theme.middle),
+            icon_color: foreground(theme.middle, "variant"),
+            icon_size: 7,
+            icon_spacing: 5,
+            text: text(theme.middle, "sans", "variant", { size: "sm" }),
+            status: {
+                ...git_status,
+            },
+        }
+
+        const selected_style: EntryState | undefined = selected
+            ? selected
+            : unselected
+
+        const unselected_default_style = merge(
+            base_properties,
+            unselected?.default ?? {},
+            {}
+        )
+        const unselected_hovered_style = merge(
+            base_properties,
+            { background: background(theme.middle, "hovered") },
+            unselected?.hovered ?? {}
+        )
+        const unselected_clicked_style = merge(
+            base_properties,
+            { background: background(theme.middle, "pressed") },
+            unselected?.clicked ?? {}
+        )
+        const selected_default_style = merge(
+            base_properties,
+            {
+                background: background(theme.lowest),
+                text: text(theme.lowest, "sans", { size: "sm" }),
+            },
+            selected_style?.default ?? {}
+        )
+        const selected_hovered_style = merge(
+            base_properties,
+            {
+                background: background(theme.lowest, "hovered"),
+                text: text(theme.lowest, "sans", { size: "sm" }),
+            },
+            selected_style?.hovered ?? {}
+        )
+        const selected_clicked_style = merge(
+            base_properties,
+            {
+                background: background(theme.lowest, "pressed"),
+                text: text(theme.lowest, "sans", { size: "sm" }),
+            },
+            selected_style?.clicked ?? {}
+        )
+
+        return toggleable({
+            state: {
+                inactive: interactive({
+                    state: {
+                        default: unselected_default_style,
+                        hovered: unselected_hovered_style,
+                        clicked: unselected_clicked_style,
+                    },
+                }),
+                active: interactive({
+                    state: {
+                        default: selected_default_style,
+                        hovered: selected_hovered_style,
+                        clicked: selected_clicked_style,
+                    },
+                }),
+            },
+        })
+    }
+
+    const default_entry = entry()
+
+    return {
+        open_project_button: interactive({
+            base: {
+                background: background(theme.middle),
+                border: border(theme.middle, "active"),
+                corner_radius: 4,
+                margin: {
+                    top: 16,
+                    left: 16,
+                    right: 16,
+                },
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(theme.middle, "sans", "default", { size: "sm" }),
+            },
+            state: {
+                hovered: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "hovered"),
+                    border: border(theme.middle, "active"),
+                },
+                clicked: {
+                    ...text(theme.middle, "sans", "default", { size: "sm" }),
+                    background: background(theme.middle, "pressed"),
+                    border: border(theme.middle, "active"),
+                },
+            },
+        }),
+        background: background(theme.middle),
+        padding: { left: 6, right: 6, top: 0, bottom: 6 },
+        indent_width: 12,
+        entry: default_entry,
+        dragged_entry: {
+            ...default_entry.inactive.default,
+            text: text(theme.middle, "sans", "on", { size: "sm" }),
+            background: with_opacity(background(theme.middle, "on"), 0.9),
+            border: border(theme.middle),
+        },
+        ignored_entry: entry(
+            {
+                default: {
+                    text: text(theme.middle, "sans", "disabled"),
+                },
+            },
+            {
+                default: {
+                    icon_color: foreground(theme.middle, "variant"),
+                },
+            }
+        ),
+        cut_entry: entry(
+            {
+                default: {
+                    text: text(theme.middle, "sans", "disabled"),
+                },
+            },
+            {
+                default: {
+                    background: background(theme.middle, "active"),
+                    text: text(theme.middle, "sans", "disabled", {
+                        size: "sm",
+                    }),
+                },
+            }
+        ),
+        filename_editor: {
+            background: background(theme.middle, "on"),
+            text: text(theme.middle, "sans", "on", { size: "sm" }),
+            selection: theme.players[0],
+        },
+    }
+}

styles/src/style_tree/project_shared_notification.ts πŸ”—

@@ -0,0 +1,55 @@
+import { useTheme } from "../theme"
+import { background, border, text } from "./components"
+
+export default function project_shared_notification(): unknown {
+    const theme = useTheme()
+
+    const avatar_size = 48
+    return {
+        window_height: 74,
+        window_width: 380,
+        background: background(theme.middle),
+        owner_container: {
+            padding: 12,
+        },
+        owner_avatar: {
+            height: avatar_size,
+            width: avatar_size,
+            corner_radius: avatar_size / 2,
+        },
+        owner_metadata: {
+            margin: { left: 10 },
+        },
+        owner_username: {
+            ...text(theme.middle, "sans", { size: "sm", weight: "bold" }),
+            margin: { top: -3 },
+        },
+        message: {
+            ...text(theme.middle, "sans", "variant", { size: "xs" }),
+            margin: { top: -3 },
+        },
+        worktree_roots: {
+            ...text(theme.middle, "sans", "variant", {
+                size: "xs",
+                weight: "bold",
+            }),
+            margin: { top: -3 },
+        },
+        button_width: 96,
+        open_button: {
+            background: background(theme.middle, "accent"),
+            border: border(theme.middle, { left: true, bottom: true }),
+            ...text(theme.middle, "sans", "accent", {
+                size: "xs",
+                weight: "bold",
+            }),
+        },
+        dismiss_button: {
+            border: border(theme.middle, { left: true }),
+            ...text(theme.middle, "sans", "variant", {
+                size: "xs",
+                weight: "bold",
+            }),
+        },
+    }
+}

styles/src/style_tree/search.ts πŸ”—

@@ -0,0 +1,138 @@
+import { with_opacity } from "../theme/color"
+import { background, border, foreground, text } from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function search(): any {
+    const theme = useTheme()
+
+    // Search input
+    const editor = {
+        background: background(theme.highest),
+        corner_radius: 8,
+        min_width: 200,
+        max_width: 500,
+        placeholder_text: text(theme.highest, "mono", "disabled"),
+        selection: theme.players[0],
+        text: text(theme.highest, "mono", "default"),
+        border: border(theme.highest),
+        margin: {
+            right: 12,
+        },
+        padding: {
+            top: 3,
+            bottom: 3,
+            left: 12,
+            right: 8,
+        },
+    }
+
+    const include_exclude_editor = {
+        ...editor,
+        min_width: 100,
+        max_width: 250,
+    }
+
+    return {
+        // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
+        match_background: with_opacity(
+            foreground(theme.highest, "accent"),
+            0.4
+        ),
+        option_button: toggleable({
+            base: interactive({
+                base: {
+                    ...text(theme.highest, "mono", "on"),
+                    background: background(theme.highest, "on"),
+                    corner_radius: 6,
+                    border: border(theme.highest, "on"),
+                    margin: {
+                        right: 4,
+                    },
+                    padding: {
+                        bottom: 2,
+                        left: 10,
+                        right: 10,
+                        top: 2,
+                    },
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.highest, "mono", "on", "hovered"),
+                        background: background(theme.highest, "on", "hovered"),
+                        border: border(theme.highest, "on", "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "mono", "on", "pressed"),
+                        background: background(theme.highest, "on", "pressed"),
+                        border: border(theme.highest, "on", "pressed"),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        ...text(theme.highest, "mono", "accent"),
+                    },
+                    hovered: {
+                        ...text(theme.highest, "mono", "accent", "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.highest, "mono", "accent", "pressed"),
+                    },
+                },
+            },
+        }),
+        editor,
+        invalid_editor: {
+            ...editor,
+            border: border(theme.highest, "negative"),
+        },
+        include_exclude_editor,
+        invalid_include_exclude_editor: {
+            ...include_exclude_editor,
+            border: border(theme.highest, "negative"),
+        },
+        match_index: {
+            ...text(theme.highest, "mono", "variant"),
+            padding: {
+                left: 6,
+            },
+        },
+        option_button_group: {
+            padding: {
+                left: 12,
+                right: 12,
+            },
+        },
+        include_exclude_inputs: {
+            ...text(theme.highest, "mono", "variant"),
+            padding: {
+                right: 6,
+            },
+        },
+        results_status: {
+            ...text(theme.highest, "mono", "on"),
+            size: 18,
+        },
+        dismiss_button: interactive({
+            base: {
+                color: foreground(theme.highest, "variant"),
+                icon_width: 12,
+                button_width: 14,
+                padding: {
+                    left: 10,
+                    right: 10,
+                },
+            },
+            state: {
+                hovered: {
+                    color: foreground(theme.highest, "hovered"),
+                },
+                clicked: {
+                    color: foreground(theme.highest, "pressed"),
+                },
+            },
+        }),
+    }
+}

styles/src/style_tree/shared_screen.ts πŸ”—

@@ -0,0 +1,10 @@
+import { useTheme } from "../theme"
+import { background } from "./components"
+
+export default function sharedScreen() {
+    const theme = useTheme()
+
+    return {
+        background: background(theme.highest),
+    }
+}

styles/src/style_tree/simple_message_notification.ts πŸ”—

@@ -0,0 +1,52 @@
+import { background, border, foreground, text } from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function simple_message_notification(): any {
+    const theme = useTheme()
+
+    const header_padding = 8
+
+    return {
+        message: {
+            ...text(theme.middle, "sans", { size: "xs" }),
+            margin: { left: header_padding, right: header_padding },
+        },
+        action_message: interactive({
+            base: {
+                ...text(theme.middle, "sans", { size: "xs" }),
+                border: border(theme.middle, "active"),
+                corner_radius: 4,
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 7,
+                    right: 7,
+                },
+
+                margin: { left: header_padding, top: 6, bottom: 6 },
+            },
+            state: {
+                hovered: {
+                    ...text(theme.middle, "sans", "default", { size: "xs" }),
+                    background: background(theme.middle, "hovered"),
+                    border: border(theme.middle, "active"),
+                },
+            },
+        }),
+        dismiss_button: interactive({
+            base: {
+                color: foreground(theme.middle),
+                icon_width: 8,
+                icon_height: 8,
+                button_width: 8,
+                button_height: 8,
+            },
+            state: {
+                hovered: {
+                    color: foreground(theme.middle, "hovered"),
+                },
+            },
+        }),
+    }
+}

styles/src/style_tree/status_bar.ts πŸ”—

@@ -0,0 +1,156 @@
+import { background, border, foreground, text } from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../common"
+export default function status_bar(): any {
+    const theme = useTheme()
+
+    const layer = theme.lowest
+
+    const status_container = {
+        corner_radius: 6,
+        padding: { top: 3, bottom: 3, left: 6, right: 6 },
+    }
+
+    const diagnostic_status_container = {
+        corner_radius: 6,
+        padding: { top: 1, bottom: 1, left: 6, right: 6 },
+    }
+
+    return {
+        height: 30,
+        item_spacing: 8,
+        padding: {
+            top: 1,
+            bottom: 1,
+            left: 6,
+            right: 6,
+        },
+        border: border(layer, { top: true, overlay: true }),
+        cursor_position: text(layer, "sans", "variant"),
+        active_language: interactive({
+            base: {
+                padding: { left: 6, right: 6 },
+                ...text(layer, "sans", "variant"),
+            },
+            state: {
+                hovered: {
+                    ...text(layer, "sans", "on"),
+                },
+            },
+        }),
+        auto_update_progress_message: text(layer, "sans", "variant"),
+        auto_update_done_message: text(layer, "sans", "variant"),
+        lsp_status: interactive({
+            base: {
+                ...diagnostic_status_container,
+                icon_spacing: 4,
+                icon_width: 14,
+                height: 18,
+                message: text(layer, "sans"),
+                icon_color: foreground(layer),
+            },
+            state: {
+                hovered: {
+                    message: text(layer, "sans"),
+                    icon_color: foreground(layer),
+                    background: background(layer, "hovered"),
+                },
+            },
+        }),
+        diagnostic_message: interactive({
+            base: {
+                ...text(layer, "sans"),
+            },
+            state: { hovered: text(layer, "sans", "hovered") },
+        }),
+        diagnostic_summary: interactive({
+            base: {
+                height: 20,
+                icon_width: 16,
+                icon_spacing: 2,
+                summary_spacing: 6,
+                text: text(layer, "sans", { size: "sm" }),
+                icon_color_ok: foreground(layer, "variant"),
+                icon_color_warning: foreground(layer, "warning"),
+                icon_color_error: foreground(layer, "negative"),
+                container_ok: {
+                    corner_radius: 6,
+                    padding: { top: 3, bottom: 3, left: 7, right: 7 },
+                },
+                container_warning: {
+                    ...diagnostic_status_container,
+                    background: background(layer, "warning"),
+                    border: border(layer, "warning"),
+                },
+                container_error: {
+                    ...diagnostic_status_container,
+                    background: background(layer, "negative"),
+                    border: border(layer, "negative"),
+                },
+            },
+            state: {
+                hovered: {
+                    icon_color_ok: foreground(layer, "on"),
+                    container_ok: {
+                        background: background(layer, "on", "hovered"),
+                    },
+                    container_warning: {
+                        background: background(layer, "warning", "hovered"),
+                        border: border(layer, "warning", "hovered"),
+                    },
+                    container_error: {
+                        background: background(layer, "negative", "hovered"),
+                        border: border(layer, "negative", "hovered"),
+                    },
+                },
+            },
+        }),
+        panel_buttons: {
+            group_left: {},
+            group_bottom: {},
+            group_right: {},
+            button: toggleable({
+                base: interactive({
+                    base: {
+                        ...status_container,
+                        icon_size: 16,
+                        icon_color: foreground(layer, "variant"),
+                        label: {
+                            margin: { left: 6 },
+                            ...text(layer, "sans", { size: "sm" }),
+                        },
+                    },
+                    state: {
+                        hovered: {
+                            icon_color: foreground(layer, "hovered"),
+                            background: background(layer, "variant"),
+                        },
+                    },
+                }),
+                state: {
+                    active: {
+                        default: {
+                            icon_color: foreground(layer, "active"),
+                            background: background(layer, "active"),
+                        },
+                        hovered: {
+                            icon_color: foreground(layer, "hovered"),
+                            background: background(layer, "hovered"),
+                        },
+                        clicked: {
+                            icon_color: foreground(layer, "pressed"),
+                            background: background(layer, "pressed"),
+                        },
+                    },
+                },
+            }),
+            badge: {
+                corner_radius: 3,
+                padding: 2,
+                margin: { bottom: -1, right: -1 },
+                border: border(layer),
+                background: background(layer, "accent"),
+            },
+        },
+    }
+}

styles/src/style_tree/tab_bar.ts πŸ”—

@@ -0,0 +1,131 @@
+import { with_opacity } from "../theme/color"
+import { text, border, background, foreground } from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../common"
+
+export default function tab_bar(): any {
+    const theme = useTheme()
+
+    const height = 32
+
+    const active_layer = theme.highest
+    const layer = theme.middle
+
+    const tab = {
+        height,
+        text: text(layer, "sans", "variant", { size: "sm" }),
+        background: background(layer),
+        border: border(layer, {
+            right: true,
+            bottom: true,
+            overlay: true,
+        }),
+        padding: {
+            left: 8,
+            right: 12,
+        },
+        spacing: 8,
+
+        // Tab type icons (e.g. Project Search)
+        type_icon_width: 14,
+
+        // Close icons
+        close_icon_width: 8,
+        icon_close: foreground(layer, "variant"),
+        icon_close_active: foreground(layer, "hovered"),
+
+        // Indicators
+        icon_conflict: foreground(layer, "warning"),
+        icon_dirty: foreground(layer, "accent"),
+
+        // When two tabs of the same name are open, a label appears next to them
+        description: {
+            margin: { left: 8 },
+            ...text(layer, "sans", "disabled", { size: "2xs" }),
+        },
+    }
+
+    const active_pane_active_tab = {
+        ...tab,
+        background: background(active_layer),
+        text: text(active_layer, "sans", "active", { size: "sm" }),
+        border: {
+            ...tab.border,
+            bottom: false,
+        },
+    }
+
+    const inactive_pane_inactive_tab = {
+        ...tab,
+        background: background(layer),
+        text: text(layer, "sans", "variant", { size: "sm" }),
+    }
+
+    const inactive_pane_active_tab = {
+        ...tab,
+        background: background(active_layer),
+        text: text(layer, "sans", "variant", { size: "sm" }),
+        border: {
+            ...tab.border,
+            bottom: false,
+        },
+    }
+
+    const dragged_tab = {
+        ...active_pane_active_tab,
+        background: with_opacity(tab.background, 0.9),
+        border: undefined as any,
+        shadow: theme.popover_shadow,
+    }
+
+    return {
+        height,
+        background: background(layer),
+        active_pane: {
+            active_tab: active_pane_active_tab,
+            inactive_tab: tab,
+        },
+        inactive_pane: {
+            active_tab: inactive_pane_active_tab,
+            inactive_tab: inactive_pane_inactive_tab,
+        },
+        dragged_tab,
+        pane_button: toggleable({
+            base: interactive({
+                base: {
+                    color: foreground(layer, "variant"),
+                    icon_width: 12,
+                    button_width: active_pane_active_tab.height,
+                },
+                state: {
+                    hovered: {
+                        color: foreground(layer, "hovered"),
+                    },
+                    clicked: {
+                        color: foreground(layer, "pressed"),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        color: foreground(layer, "accent"),
+                    },
+                    hovered: {
+                        color: foreground(layer, "hovered"),
+                    },
+                    clicked: {
+                        color: foreground(layer, "pressed"),
+                    },
+                },
+            },
+        }),
+        pane_button_container: {
+            background: tab.background,
+            border: {
+                ...tab.border,
+                right: false,
+            },
+        },
+    }
+}

styles/src/style_tree/terminal.ts πŸ”—

@@ -0,0 +1,54 @@
+import { useTheme } from "../theme"
+
+export default function terminal() {
+    const theme = useTheme()
+
+    /**
+     * Colors are controlled per-cell in the terminal grid.
+     * Cells can be set to any of these more 'theme-capable' colors
+     * or can be set directly with RGB values.
+     * Here are the common interpretations of these names:
+     * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+     */
+    return {
+        black: theme.ramps.neutral(0).hex(),
+        red: theme.ramps.red(0.5).hex(),
+        green: theme.ramps.green(0.5).hex(),
+        yellow: theme.ramps.yellow(0.5).hex(),
+        blue: theme.ramps.blue(0.5).hex(),
+        magenta: theme.ramps.magenta(0.5).hex(),
+        cyan: theme.ramps.cyan(0.5).hex(),
+        white: theme.ramps.neutral(1).hex(),
+        bright_black: theme.ramps.neutral(0.4).hex(),
+        bright_red: theme.ramps.red(0.25).hex(),
+        bright_green: theme.ramps.green(0.25).hex(),
+        bright_yellow: theme.ramps.yellow(0.25).hex(),
+        bright_blue: theme.ramps.blue(0.25).hex(),
+        bright_magenta: theme.ramps.magenta(0.25).hex(),
+        bright_cyan: theme.ramps.cyan(0.25).hex(),
+        bright_white: theme.ramps.neutral(1).hex(),
+        /**
+         * Default color for characters
+         */
+        foreground: theme.ramps.neutral(1).hex(),
+        /**
+         * Default color for the rectangle background of a cell
+         */
+        background: theme.ramps.neutral(0).hex(),
+        modal_background: theme.ramps.neutral(0.1).hex(),
+        /**
+         * Default color for the cursor
+         */
+        cursor: theme.players[0].cursor,
+        dim_black: theme.ramps.neutral(1).hex(),
+        dim_red: theme.ramps.red(0.75).hex(),
+        dim_green: theme.ramps.green(0.75).hex(),
+        dim_yellow: theme.ramps.yellow(0.75).hex(),
+        dim_blue: theme.ramps.blue(0.75).hex(),
+        dim_magenta: theme.ramps.magenta(0.75).hex(),
+        dim_cyan: theme.ramps.cyan(0.75).hex(),
+        dim_white: theme.ramps.neutral(0.6).hex(),
+        bright_foreground: theme.ramps.neutral(1).hex(),
+        dim_foreground: theme.ramps.neutral(0).hex(),
+    }
+}

styles/src/style_tree/titlebar.ts πŸ”—

@@ -0,0 +1,278 @@
+import { icon_button, toggleable_icon_button } from "../component/icon_button"
+import { toggleable_text_button } from "../component/text_button"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+import { with_opacity } from "../theme/color"
+import { background, border, foreground, text } from "./components"
+
+const ITEM_SPACING = 8
+const TITLEBAR_HEIGHT = 32
+
+function build_spacing(
+    container_height: number,
+    element_height: number,
+    spacing: number
+) {
+    return {
+        group: spacing,
+        item: spacing / 2,
+        half_item: spacing / 4,
+        margin_y: (container_height - element_height) / 2,
+        margin_x: (container_height - element_height) / 2,
+    }
+}
+
+function call_controls() {
+    const theme = useTheme()
+
+    const button_height = 18
+
+    const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
+    const margin_y = {
+        top: space.margin_y,
+        bottom: space.margin_y,
+    }
+
+    return {
+        toggle_microphone_button: toggleable_icon_button(theme, {
+            margin: {
+                ...margin_y,
+                left: space.group,
+                right: space.half_item,
+            },
+            active_color: "negative",
+        }),
+
+        toggle_speakers_button: toggleable_icon_button(theme, {
+            margin: {
+                ...margin_y,
+                left: space.half_item,
+                right: space.half_item,
+            },
+        }),
+
+        screen_share_button: toggleable_icon_button(theme, {
+            margin: {
+                ...margin_y,
+                left: space.half_item,
+                right: space.group,
+            },
+            active_color: "accent",
+        }),
+
+        muted: foreground(theme.lowest, "negative"),
+        speaking: foreground(theme.lowest, "accent"),
+    }
+}
+
+/**
+ * Opens the User Menu when toggled
+ *
+ * When logged in shows the user's avatar and a chevron,
+ * When logged out only shows a chevron.
+ */
+function user_menu() {
+    const theme = useTheme()
+
+    const button_height = 18
+
+    const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
+
+    const build_button = ({ online }: { online: boolean }) => {
+        const button = toggleable({
+            base: interactive({
+                base: {
+                    corner_radius: 6,
+                    height: button_height,
+                    width: online ? 37 : 24,
+                    padding: {
+                        top: 2,
+                        bottom: 2,
+                        left: 6,
+                        right: 6,
+                    },
+                    margin: {
+                        left: space.item,
+                        right: space.item,
+                    },
+                    ...text(theme.lowest, "sans", { size: "xs" }),
+                    background: background(theme.lowest),
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.lowest, "sans", "hovered", {
+                            size: "xs",
+                        }),
+                        background: background(theme.lowest, "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.lowest, "sans", "pressed", {
+                            size: "xs",
+                        }),
+                        background: background(theme.lowest, "pressed"),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        ...text(theme.lowest, "sans", "active", { size: "xs" }),
+                        background: background(theme.middle),
+                    },
+                    hovered: {
+                        ...text(theme.lowest, "sans", "active", { size: "xs" }),
+                        background: background(theme.middle, "hovered"),
+                    },
+                    clicked: {
+                        ...text(theme.lowest, "sans", "active", { size: "xs" }),
+                        background: background(theme.middle, "pressed"),
+                    },
+                },
+            },
+        })
+
+        return {
+            user_menu: button,
+            avatar: {
+                icon_width: 16,
+                icon_height: 16,
+                corner_radius: 4,
+                outer_width: 16,
+                outer_corner_radius: 16,
+            },
+            icon: {
+                margin: {
+                    top: 2,
+                    left: online ? space.item : 0,
+                    right: space.group,
+                    bottom: 2,
+                },
+                width: 11,
+                height: 11,
+                color: foreground(theme.lowest),
+            },
+        }
+    }
+    return {
+        user_menu_button_online: build_button({ online: true }),
+        user_menu_button_offline: build_button({ online: false }),
+    }
+}
+
+export function titlebar(): any {
+    const theme = useTheme()
+
+    const avatar_width = 15
+    const avatar_outer_width = avatar_width + 4
+    const follower_avatar_width = 14
+    const follower_avatar_outer_width = follower_avatar_width + 4
+
+    return {
+        item_spacing: ITEM_SPACING,
+        face_pile_spacing: 2,
+        height: TITLEBAR_HEIGHT,
+        background: background(theme.lowest),
+        border: border(theme.lowest, { bottom: true }),
+        padding: {
+            left: 80,
+            right: 0,
+        },
+
+        // Project
+        project_name_divider: text(theme.lowest, "sans", "variant"),
+
+        project_menu_button: toggleable_text_button(theme, {
+            color: 'base',
+        }),
+        git_menu_button: toggleable_text_button(theme, {
+            color: 'variant',
+        }),
+
+        // Collaborators
+        leader_avatar: {
+            width: avatar_width,
+            outer_width: avatar_outer_width,
+            corner_radius: avatar_width / 2,
+            outer_corner_radius: avatar_outer_width / 2,
+        },
+        follower_avatar: {
+            width: follower_avatar_width,
+            outer_width: follower_avatar_outer_width,
+            corner_radius: follower_avatar_width / 2,
+            outer_corner_radius: follower_avatar_outer_width / 2,
+        },
+        inactive_avatar_grayscale: true,
+        follower_avatar_overlap: 8,
+        leader_selection: {
+            margin: {
+                top: 4,
+                bottom: 4,
+            },
+            padding: {
+                left: 2,
+                right: 2,
+                top: 2,
+                bottom: 2,
+            },
+            corner_radius: 6,
+        },
+        avatar_ribbon: {
+            height: 3,
+            width: 14,
+            // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded.
+        },
+
+        sign_in_button: toggleable_text_button(theme, {}),
+        offline_icon: {
+            color: foreground(theme.lowest, "variant"),
+            width: 16,
+            margin: {
+                left: ITEM_SPACING,
+            },
+            padding: {
+                right: 4,
+            },
+        },
+
+        // When the collaboration server is out of date, show a warning
+        outdated_warning: {
+            ...text(theme.lowest, "sans", "warning", { size: "xs" }),
+            background: with_opacity(background(theme.lowest, "warning"), 0.3),
+            border: border(theme.lowest, "warning"),
+            margin: {
+                left: ITEM_SPACING,
+            },
+            padding: {
+                left: 8,
+                right: 8,
+            },
+            corner_radius: 6,
+        },
+
+        leave_call_button: icon_button({
+            margin: {
+                left: ITEM_SPACING / 2,
+                right: ITEM_SPACING,
+            },
+        }),
+
+        ...call_controls(),
+
+        toggle_contacts_button: toggleable_icon_button(theme, {
+            margin: {
+                left: ITEM_SPACING,
+            },
+        }),
+
+        // Jewel that notifies you that there are new contact requests
+        toggle_contacts_badge: {
+            corner_radius: 3,
+            padding: 2,
+            margin: { top: 3, left: 3 },
+            border: border(theme.lowest),
+            background: foreground(theme.lowest, "accent"),
+        },
+        share_button: toggleable_text_button(theme, {}),
+        user_menu: user_menu(),
+    }
+}

styles/src/style_tree/toolbar_dropdown_menu.ts πŸ”—

@@ -0,0 +1,66 @@
+import { background, border, text } from "./components"
+import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+export default function dropdown_menu(): any {
+    const theme = useTheme()
+
+    return {
+        row_height: 30,
+        background: background(theme.middle),
+        border: border(theme.middle),
+        shadow: theme.popover_shadow,
+        header: interactive({
+            base: {
+                ...text(theme.middle, "sans", { size: "sm" }),
+                secondary_text: text(theme.middle, "sans", {
+                    size: "sm",
+                    color: "#aaaaaa",
+                }),
+                secondary_text_spacing: 10,
+                padding: { left: 8, right: 8, top: 2, bottom: 2 },
+                corner_radius: 6,
+                background: background(theme.middle, "on"),
+            },
+            state: {
+                hovered: {
+                    background: background(theme.middle, "hovered"),
+                },
+                clicked: {
+                    background: background(theme.middle, "pressed"),
+                },
+            },
+        }),
+        section_header: {
+            ...text(theme.middle, "sans", { size: "sm" }),
+            padding: { left: 8, right: 8, top: 8, bottom: 8 },
+        },
+        item: toggleable({
+            base: interactive({
+                base: {
+                    ...text(theme.middle, "sans", { size: "sm" }),
+                    secondary_text_spacing: 10,
+                    secondary_text: text(theme.middle, "sans", { size: "sm" }),
+                    padding: { left: 18, right: 18, top: 2, bottom: 2 },
+                },
+                state: {
+                    hovered: {
+                        background: background(theme.middle, "hovered"),
+                        ...text(theme.middle, "sans", "hovered", {
+                            size: "sm",
+                        }),
+                    },
+                },
+            }),
+            state: {
+                active: {
+                    default: {
+                        background: background(theme.middle, "active"),
+                    },
+                    hovered: {
+                        background: background(theme.middle, "hovered"),
+                    },
+                },
+            },
+        }),
+    }
+}

styles/src/style_tree/tooltip.ts πŸ”—

@@ -0,0 +1,24 @@
+import { useTheme } from "../theme"
+import { background, border, text } from "./components"
+
+export default function tooltip(): any {
+    const theme = useTheme()
+
+    return {
+        background: background(theme.middle),
+        border: border(theme.middle),
+        padding: { top: 4, bottom: 4, left: 8, right: 8 },
+        margin: { top: 6, left: 6 },
+        shadow: theme.popover_shadow,
+        corner_radius: 6,
+        text: text(theme.middle, "sans", { size: "xs" }),
+        keystroke: {
+            background: background(theme.middle, "on"),
+            corner_radius: 4,
+            margin: { left: 6 },
+            padding: { left: 4, right: 4 },
+            ...text(theme.middle, "mono", "on", { size: "xs", weight: "bold" }),
+        },
+        max_text_width: 200,
+    }
+}

styles/src/style_tree/update_notification.ts πŸ”—

@@ -0,0 +1,41 @@
+import { foreground, text } from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function update_notification(): any {
+    const theme = useTheme()
+
+    const header_padding = 8
+
+    return {
+        message: {
+            ...text(theme.middle, "sans", { size: "xs" }),
+            margin: { left: header_padding, right: header_padding },
+        },
+        action_message: interactive({
+            base: {
+                ...text(theme.middle, "sans", { size: "xs" }),
+                margin: { left: header_padding, top: 6, bottom: 6 },
+            },
+            state: {
+                hovered: {
+                    color: foreground(theme.middle, "hovered"),
+                },
+            },
+        }),
+        dismiss_button: interactive({
+            base: {
+                color: foreground(theme.middle),
+                icon_width: 8,
+                icon_height: 8,
+                button_width: 8,
+                button_height: 8,
+            },
+            state: {
+                hovered: {
+                    color: foreground(theme.middle, "hovered"),
+                },
+            },
+        }),
+    }
+}

styles/src/style_tree/welcome.ts πŸ”—

@@ -0,0 +1,157 @@
+import { with_opacity } from "../theme/color"
+import {
+    border,
+    background,
+    foreground,
+    text,
+    TextProperties,
+    svg,
+} from "./components"
+import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function welcome(): any {
+    const theme = useTheme()
+
+    const checkbox_base = {
+        corner_radius: 4,
+        padding: {
+            left: 3,
+            right: 3,
+            top: 3,
+            bottom: 3,
+        },
+        // shadow: theme.popover_shadow,
+        border: border(theme.highest),
+        margin: {
+            right: 8,
+            top: 5,
+            bottom: 5,
+        },
+    }
+
+    const interactive_text_size: TextProperties = { size: "sm" }
+
+    return {
+        page_width: 320,
+        logo: svg(
+            foreground(theme.highest, "default"),
+            "icons/logo_96.svg",
+            64,
+            64
+        ),
+        logo_subheading: {
+            ...text(theme.highest, "sans", "variant", { size: "md" }),
+            margin: {
+                top: 10,
+                bottom: 7,
+            },
+        },
+        button_group: {
+            margin: {
+                top: 8,
+                bottom: 16,
+            },
+        },
+        heading_group: {
+            margin: {
+                top: 8,
+                bottom: 12,
+            },
+        },
+        checkbox_group: {
+            border: border(theme.highest, "variant"),
+            background: with_opacity(
+                background(theme.highest, "hovered"),
+                0.25
+            ),
+            corner_radius: 4,
+            padding: {
+                left: 12,
+                top: 2,
+                bottom: 2,
+            },
+        },
+        button: interactive({
+            base: {
+                background: background(theme.highest),
+                border: border(theme.highest, "active"),
+                corner_radius: 4,
+                margin: {
+                    top: 4,
+                    bottom: 4,
+                },
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 7,
+                    right: 7,
+                },
+                ...text(
+                    theme.highest,
+                    "sans",
+                    "default",
+                    interactive_text_size
+                ),
+            },
+            state: {
+                hovered: {
+                    ...text(
+                        theme.highest,
+                        "sans",
+                        "default",
+                        interactive_text_size
+                    ),
+                    background: background(theme.highest, "hovered"),
+                },
+            },
+        }),
+
+        usage_note: {
+            ...text(theme.highest, "sans", "variant", { size: "2xs" }),
+            padding: {
+                top: -4,
+            },
+        },
+        checkbox_container: {
+            margin: {
+                top: 4,
+            },
+            padding: {
+                bottom: 8,
+            },
+        },
+        checkbox: {
+            label: {
+                ...text(theme.highest, "sans", interactive_text_size),
+                // Also supports margin, container, border, etc.
+            },
+            icon: svg(
+                foreground(theme.highest, "on"),
+                "icons/check_12.svg",
+                12,
+                12
+            ),
+            default: {
+                ...checkbox_base,
+                background: background(theme.highest, "default"),
+                border: border(theme.highest, "active"),
+            },
+            checked: {
+                ...checkbox_base,
+                background: background(theme.highest, "hovered"),
+                border: border(theme.highest, "active"),
+            },
+            hovered: {
+                ...checkbox_base,
+                background: background(theme.highest, "hovered"),
+                border: border(theme.highest, "active"),
+            },
+            hovered_and_checked: {
+                ...checkbox_base,
+                background: background(theme.highest, "hovered"),
+                border: border(theme.highest, "active"),
+            },
+        },
+    }
+}

styles/src/style_tree/workspace.ts πŸ”—

@@ -0,0 +1,192 @@
+import { with_opacity } from "../theme/color"
+import {
+    background,
+    border,
+    border_color,
+    foreground,
+    svg,
+    text,
+} from "./components"
+import statusBar from "./status_bar"
+import tabBar from "./tab_bar"
+import { interactive } from "../element"
+import { titlebar } from "./titlebar"
+import { useTheme } from "../theme"
+
+export default function workspace(): any {
+    const theme = useTheme()
+
+    const { is_light } = theme
+
+    return {
+        background: background(theme.lowest),
+        blank_pane: {
+            logo_container: {
+                width: 256,
+                height: 256,
+            },
+            logo: svg(
+                with_opacity("#000000", theme.is_light ? 0.6 : 0.8),
+                "icons/logo_96.svg",
+                256,
+                256
+            ),
+
+            logo_shadow: svg(
+                with_opacity(
+                    theme.is_light
+                        ? "#FFFFFF"
+                        : theme.lowest.base.default.background,
+                    theme.is_light ? 1 : 0.6
+                ),
+                "icons/logo_96.svg",
+                256,
+                256
+            ),
+            keyboard_hints: {
+                margin: {
+                    top: 96,
+                },
+                corner_radius: 4,
+            },
+            keyboard_hint: interactive({
+                base: {
+                    ...text(theme.lowest, "sans", "variant", { size: "sm" }),
+                    padding: {
+                        top: 3,
+                        left: 8,
+                        right: 8,
+                        bottom: 3,
+                    },
+                    corner_radius: 8,
+                },
+                state: {
+                    hovered: {
+                        ...text(theme.lowest, "sans", "active", { size: "sm" }),
+                    },
+                },
+            }),
+
+            keyboard_hint_width: 320,
+        },
+        joining_project_avatar: {
+            corner_radius: 40,
+            width: 80,
+        },
+        joining_project_message: {
+            padding: 12,
+            ...text(theme.lowest, "sans", { size: "lg" }),
+        },
+        external_location_message: {
+            background: background(theme.middle, "accent"),
+            border: border(theme.middle, "accent"),
+            corner_radius: 6,
+            padding: 12,
+            margin: { bottom: 8, right: 8 },
+            ...text(theme.middle, "sans", "accent", { size: "xs" }),
+        },
+        leader_border_opacity: 0.7,
+        leader_border_width: 2.0,
+        tab_bar: tabBar(),
+        modal: {
+            margin: {
+                bottom: 52,
+                top: 52,
+            },
+            cursor: "Arrow",
+        },
+        zoomed_background: {
+            cursor: "Arrow",
+            background: is_light
+                ? with_opacity(background(theme.lowest), 0.8)
+                : with_opacity(background(theme.highest), 0.6),
+        },
+        zoomed_pane_foreground: {
+            margin: 16,
+            shadow: theme.modal_shadow,
+            border: border(theme.lowest, { overlay: true }),
+        },
+        zoomed_panel_foreground: {
+            margin: 16,
+            border: border(theme.lowest, { overlay: true }),
+        },
+        dock: {
+            left: {
+                border: border(theme.lowest, { right: true }),
+            },
+            bottom: {
+                border: border(theme.lowest, { top: true }),
+            },
+            right: {
+                border: border(theme.lowest, { left: true }),
+            },
+        },
+        pane_divider: {
+            color: border_color(theme.lowest),
+            width: 1,
+        },
+        status_bar: statusBar(),
+        titlebar: titlebar(),
+        toolbar: {
+            height: 34,
+            background: background(theme.highest),
+            border: border(theme.highest, { bottom: true }),
+            item_spacing: 8,
+            nav_button: interactive({
+                base: {
+                    color: foreground(theme.highest, "on"),
+                    icon_width: 12,
+                    button_width: 24,
+                    corner_radius: 6,
+                },
+                state: {
+                    hovered: {
+                        color: foreground(theme.highest, "on", "hovered"),
+                        background: background(theme.highest, "on", "hovered"),
+                    },
+                    disabled: {
+                        color: foreground(theme.highest, "on", "disabled"),
+                    },
+                },
+            }),
+            padding: { left: 8, right: 8, top: 4, bottom: 4 },
+        },
+        breadcrumb_height: 24,
+        breadcrumbs: interactive({
+            base: {
+                ...text(theme.highest, "sans", "variant"),
+                corner_radius: 6,
+                padding: {
+                    left: 6,
+                    right: 6,
+                },
+            },
+            state: {
+                hovered: {
+                    color: foreground(theme.highest, "on", "hovered"),
+                    background: background(theme.highest, "on", "hovered"),
+                },
+            },
+        }),
+        disconnected_overlay: {
+            ...text(theme.lowest, "sans"),
+            background: with_opacity(background(theme.lowest), 0.8),
+        },
+        notification: {
+            margin: { top: 10 },
+            background: background(theme.middle),
+            corner_radius: 6,
+            padding: 12,
+            border: border(theme.middle),
+            shadow: theme.popover_shadow,
+        },
+        notifications: {
+            width: 400,
+            margin: { right: 10, bottom: 10 },
+        },
+        drop_target_overlay_color: with_opacity(
+            foreground(theme.lowest, "variant"),
+            0.5
+        ),
+    }
+}

styles/src/system/lib/convert.ts πŸ”—

@@ -1,11 +0,0 @@
-/** Converts a percentage scale value (0-100) to normalized scale (0-1) value. */
-export function percentageToNormalized(value: number) {
-    const normalized = value / 100
-    return normalized
-}
-
-/** Converts a normalized scale (0-1) value to a percentage scale (0-100) value. */
-export function normalizedToPercetage(value: number) {
-    const percentage = value * 100
-    return percentage
-}

styles/src/system/lib/curve.ts πŸ”—

@@ -1,26 +0,0 @@
-import bezier from "bezier-easing"
-import { Curve } from "../ref/curves"
-
-/**
- * Formats our Curve data structure into a bezier easing function.
- * @param {Curve} curve - The curve to format.
- * @param {Boolean} inverted - Whether or not to invert the curve.
- * @returns {EasingFunction} The formatted easing function.
- */
-export function curve(curve: Curve, inverted?: Boolean) {
-    if (inverted) {
-        return bezier(
-            curve.value[3],
-            curve.value[2],
-            curve.value[1],
-            curve.value[0]
-        )
-    }
-
-    return bezier(
-        curve.value[0],
-        curve.value[1],
-        curve.value[2],
-        curve.value[3]
-    )
-}

styles/src/system/lib/generate.ts πŸ”—

@@ -1,159 +0,0 @@
-import bezier from "bezier-easing"
-import chroma from "chroma-js"
-import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types"
-import { percentageToNormalized } from "./convert"
-import { curve } from "./curve"
-
-// Re-export interface in a more standard format
-export type EasingFunction = bezier.EasingFunction
-
-/**
- * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata.
- *
- * @param {EasingFunction} hueEasing - An easing function for the hue component of the color.
- * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color.
- * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color.
- * @param {ColorFamilyConfig} family - Configuration for the color family.
- * @param {number} step - The current step.
- * @param {number} steps - The total number of steps in the color scale.
- *
- * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark.
- */
-function generateColor(
-    hueEasing: EasingFunction,
-    saturationEasing: EasingFunction,
-    lightnessEasing: EasingFunction,
-    family: ColorFamilyConfig,
-    step: number,
-    steps: number
-) {
-    const { hue, saturation, lightness } = family.color
-
-    const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start
-    const stepSaturation =
-        saturationEasing(step / steps) * (saturation.end - saturation.start) +
-        saturation.start
-    const stepLightness =
-        lightnessEasing(step / steps) * (lightness.end - lightness.start) +
-        lightness.start
-
-    const color = chroma.hsl(
-        stepHue,
-        percentageToNormalized(stepSaturation),
-        percentageToNormalized(stepLightness)
-    )
-
-    const contrast = {
-        black: {
-            value: chroma.contrast(color, "black"),
-            aaPass: chroma.contrast(color, "black") >= 4.5,
-            aaaPass: chroma.contrast(color, "black") >= 7,
-        },
-        white: {
-            value: chroma.contrast(color, "white"),
-            aaPass: chroma.contrast(color, "white") >= 4.5,
-            aaaPass: chroma.contrast(color, "white") >= 7,
-        },
-    }
-
-    const lch = color.lch()
-    const rgba = color.rgba()
-    const hex = color.hex()
-
-    // 55 is a magic number. It's the lightness value at which we consider a color to be "light".
-    // It was picked by eye with some testing. We might want to use a more scientific approach in the future.
-    const isLight = lch[0] > 55
-
-    const result: Color = {
-        step,
-        lch,
-        hex,
-        rgba,
-        contrast,
-        isLight,
-    }
-
-    return result
-}
-
-/**
- * Generates a color scale based on a color family configuration.
- *
- * @param {ColorFamilyConfig} config - The configuration for the color family.
- * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
- *
- * @returns {ColorScale} The generated color scale.
- *
- * @example
- * ```ts
- * const colorScale = generateColorScale({
- *   name: "blue",
- *   color: {
- *     hue: {
- *       start: 210,
- *       end: 240,
- *       curve: "easeInOut"
- *     },
- *     saturation: {
- *       start: 100,
- *       end: 100,
- *       curve: "easeInOut"
- *     },
- *     lightness: {
- *       start: 50,
- *       end: 50,
- *       curve: "easeInOut"
- *     }
- *   }
- * });
- * ```
- */
-
-export function generateColorScale(
-    config: ColorFamilyConfig,
-    inverted: Boolean = false
-) {
-    const { hue, saturation, lightness } = config.color
-
-    // 101 steps means we get values from 0-100
-    const NUM_STEPS = 101
-
-    const hueEasing = curve(hue.curve, inverted)
-    const saturationEasing = curve(saturation.curve, inverted)
-    const lightnessEasing = curve(lightness.curve, inverted)
-
-    let scale: ColorScale = {
-        colors: [],
-        values: [],
-    }
-
-    for (let i = 0; i < NUM_STEPS; i++) {
-        const color = generateColor(
-            hueEasing,
-            saturationEasing,
-            lightnessEasing,
-            config,
-            i,
-            NUM_STEPS
-        )
-
-        scale.colors.push(color)
-        scale.values.push(color.hex)
-    }
-
-    return scale
-}
-
-/** Generates a color family with a scale and an inverted scale. */
-export function generateColorFamily(config: ColorFamilyConfig) {
-    const scale = generateColorScale(config, false)
-    const invertedScale = generateColorScale(config, true)
-
-    const family: ColorFamily = {
-        name: config.name,
-        scale,
-        invertedScale,
-    }
-
-    return family
-}

styles/src/system/ref/color.ts πŸ”—

@@ -1,445 +0,0 @@
-import { generateColorFamily } from "../lib/generate"
-import { curve } from "./curves"
-
-// These are the source colors for the color scales in the system.
-// These should never directly be used directly in components or themes as they generate thousands of lines of code.
-// Instead, use the outputs from the reference palette which exports a smaller subset of colors.
-
-// Token or user-facing colors should use short, clear names and a 100-900 scale to match the font weight scale.
-
-// Light Gray ======================================== //
-
-export const lightgray = generateColorFamily({
-    name: "lightgray",
-    color: {
-        hue: {
-            start: 210,
-            end: 210,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 10,
-            end: 15,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 50,
-            curve: curve.linear,
-        },
-    },
-})
-
-// Light Dark ======================================== //
-
-export const darkgray = generateColorFamily({
-    name: "darkgray",
-    color: {
-        hue: {
-            start: 210,
-            end: 210,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 15,
-            end: 20,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 55,
-            end: 8,
-            curve: curve.linear,
-        },
-    },
-})
-
-// Red ======================================== //
-
-export const red = generateColorFamily({
-    name: "red",
-    color: {
-        hue: {
-            start: 0,
-            end: 0,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 95,
-            end: 75,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 25,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Sunset ======================================== //
-
-export const sunset = generateColorFamily({
-    name: "sunset",
-    color: {
-        hue: {
-            start: 15,
-            end: 15,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 100,
-            end: 90,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 25,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Orange ======================================== //
-
-export const orange = generateColorFamily({
-    name: "orange",
-    color: {
-        hue: {
-            start: 25,
-            end: 25,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 100,
-            end: 95,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 20,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Amber ======================================== //
-
-export const amber = generateColorFamily({
-    name: "amber",
-    color: {
-        hue: {
-            start: 38,
-            end: 38,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 100,
-            end: 100,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 18,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Yellow ======================================== //
-
-export const yellow = generateColorFamily({
-    name: "yellow",
-    color: {
-        hue: {
-            start: 48,
-            end: 48,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 90,
-            end: 100,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 15,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Lemon ======================================== //
-
-export const lemon = generateColorFamily({
-    name: "lemon",
-    color: {
-        hue: {
-            start: 55,
-            end: 55,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 85,
-            end: 95,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 15,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Citron ======================================== //
-
-export const citron = generateColorFamily({
-    name: "citron",
-    color: {
-        hue: {
-            start: 70,
-            end: 70,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 85,
-            end: 90,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 15,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Lime ======================================== //
-
-export const lime = generateColorFamily({
-    name: "lime",
-    color: {
-        hue: {
-            start: 85,
-            end: 85,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 85,
-            end: 80,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 18,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Green ======================================== //
-
-export const green = generateColorFamily({
-    name: "green",
-    color: {
-        hue: {
-            start: 108,
-            end: 108,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 60,
-            end: 70,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 18,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Mint ======================================== //
-
-export const mint = generateColorFamily({
-    name: "mint",
-    color: {
-        hue: {
-            start: 142,
-            end: 142,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 60,
-            end: 75,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 20,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Cyan ======================================== //
-
-export const cyan = generateColorFamily({
-    name: "cyan",
-    color: {
-        hue: {
-            start: 179,
-            end: 179,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 70,
-            end: 80,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 20,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Sky ======================================== //
-
-export const sky = generateColorFamily({
-    name: "sky",
-    color: {
-        hue: {
-            start: 195,
-            end: 205,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 85,
-            end: 90,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 15,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Blue ======================================== //
-
-export const blue = generateColorFamily({
-    name: "blue",
-    color: {
-        hue: {
-            start: 218,
-            end: 218,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 85,
-            end: 70,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 15,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Indigo ======================================== //
-
-export const indigo = generateColorFamily({
-    name: "indigo",
-    color: {
-        hue: {
-            start: 245,
-            end: 245,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 60,
-            end: 50,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 22,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Purple ======================================== //
-
-export const purple = generateColorFamily({
-    name: "purple",
-    color: {
-        hue: {
-            start: 260,
-            end: 270,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 65,
-            end: 55,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 20,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Pink ======================================== //
-
-export const pink = generateColorFamily({
-    name: "pink",
-    color: {
-        hue: {
-            start: 320,
-            end: 330,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 70,
-            end: 65,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 32,
-            curve: curve.lightness,
-        },
-    },
-})
-
-// Rose ======================================== //
-
-export const rose = generateColorFamily({
-    name: "rose",
-    color: {
-        hue: {
-            start: 345,
-            end: 345,
-            curve: curve.linear,
-        },
-        saturation: {
-            start: 90,
-            end: 70,
-            curve: curve.saturation,
-        },
-        lightness: {
-            start: 97,
-            end: 32,
-            curve: curve.lightness,
-        },
-    },
-})

styles/src/system/ref/curves.ts πŸ”—

@@ -1,25 +0,0 @@
-export interface Curve {
-    name: string
-    value: number[]
-}
-
-export interface Curves {
-    lightness: Curve
-    saturation: Curve
-    linear: Curve
-}
-
-export const curve: Curves = {
-    lightness: {
-        name: "lightnessCurve",
-        value: [0.2, 0, 0.75, 1.0],
-    },
-    saturation: {
-        name: "saturationCurve",
-        value: [0.67, 0.6, 0.55, 1.0],
-    },
-    linear: {
-        name: "linear",
-        value: [0.5, 0.5, 0.5, 0.5],
-    },
-}

styles/src/system/system.ts πŸ”—

@@ -1,32 +0,0 @@
-import chroma from "chroma-js"
-import * as colorFamily from "./ref/color"
-
-const color = {
-    lightgray: chroma
-        .scale(colorFamily.lightgray.scale.values)
-        .mode("lch")
-        .colors(9),
-    darkgray: chroma
-        .scale(colorFamily.darkgray.scale.values)
-        .mode("lch")
-        .colors(9),
-    red: chroma.scale(colorFamily.red.scale.values).mode("lch").colors(9),
-    sunset: chroma.scale(colorFamily.sunset.scale.values).mode("lch").colors(9),
-    orange: chroma.scale(colorFamily.orange.scale.values).mode("lch").colors(9),
-    amber: chroma.scale(colorFamily.amber.scale.values).mode("lch").colors(9),
-    yellow: chroma.scale(colorFamily.yellow.scale.values).mode("lch").colors(9),
-    lemon: chroma.scale(colorFamily.lemon.scale.values).mode("lch").colors(9),
-    citron: chroma.scale(colorFamily.citron.scale.values).mode("lch").colors(9),
-    lime: chroma.scale(colorFamily.lime.scale.values).mode("lch").colors(9),
-    green: chroma.scale(colorFamily.green.scale.values).mode("lch").colors(9),
-    mint: chroma.scale(colorFamily.mint.scale.values).mode("lch").colors(9),
-    cyan: chroma.scale(colorFamily.cyan.scale.values).mode("lch").colors(9),
-    sky: chroma.scale(colorFamily.sky.scale.values).mode("lch").colors(9),
-    blue: chroma.scale(colorFamily.blue.scale.values).mode("lch").colors(9),
-    indigo: chroma.scale(colorFamily.indigo.scale.values).mode("lch").colors(9),
-    purple: chroma.scale(colorFamily.purple.scale.values).mode("lch").colors(9),
-    pink: chroma.scale(colorFamily.pink.scale.values).mode("lch").colors(9),
-    rose: chroma.scale(colorFamily.rose.scale.values).mode("lch").colors(9),
-}
-
-export { color }

styles/src/system/types.ts πŸ”—

@@ -1,66 +0,0 @@
-import { Curve } from "./ref/curves"
-
-export interface ColorAccessibilityValue {
-    value: number
-    aaPass: boolean
-    aaaPass: boolean
-}
-
-/**
- * Calculates the color contrast between a specified color and its corresponding background and foreground colors.
- *
- * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette.
- * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information.
- */
-export interface ColorAccessibility {
-    black: ColorAccessibilityValue
-    white: ColorAccessibilityValue
-}
-
-export type Color = {
-    step: number
-    contrast: ColorAccessibility
-    hex: string
-    lch: number[]
-    rgba: number[]
-    isLight: boolean
-}
-
-export interface ColorScale {
-    colors: Color[]
-    // An array of hex values for each color in the scale
-    values: string[]
-}
-
-export type ColorFamily = {
-    name: string
-    scale: ColorScale
-    invertedScale: ColorScale
-}
-
-export interface ColorFamilyHue {
-    start: number
-    end: number
-    curve: Curve
-}
-
-export interface ColorFamilySaturation {
-    start: number
-    end: number
-    curve: Curve
-}
-
-export interface ColorFamilyLightness {
-    start: number
-    end: number
-    curve: Curve
-}
-
-export interface ColorFamilyConfig {
-    name: string
-    color: {
-        hue: ColorFamilyHue
-        saturation: ColorFamilySaturation
-        lightness: ColorFamilyLightness
-    }
-}

styles/src/theme/color.ts πŸ”—

@@ -1,5 +1,5 @@
 import chroma from "chroma-js"
 
-export function withOpacity(color: string, opacity: number): string {
+export function with_opacity(color: string, opacity: number): string {
     return chroma(color).alpha(opacity).hex()
 }

styles/src/theme/colorScheme.ts πŸ”—

@@ -1,286 +0,0 @@
-import { Scale, Color } from "chroma-js"
-import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
-export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
-import {
-    ThemeConfig,
-    ThemeAppearance,
-    ThemeConfigInputColors,
-} from "./themeConfig"
-import { getRamps } from "./ramps"
-
-export interface ColorScheme {
-    name: string
-    isLight: boolean
-
-    lowest: Layer
-    middle: Layer
-    highest: Layer
-
-    ramps: RampSet
-
-    popoverShadow: Shadow
-    modalShadow: Shadow
-
-    players: Players
-    syntax?: Partial<ThemeSyntax>
-}
-
-export interface Meta {
-    name: string
-    author: string
-    url: string
-    license: License
-}
-
-export interface License {
-    SPDX: SPDXExpression
-}
-
-// License name -> License text
-export interface Licenses {
-    [key: string]: string
-}
-
-// FIXME: Add support for the SPDX expression syntax
-export type SPDXExpression = "MIT"
-
-export interface Player {
-    cursor: string
-    selection: string
-}
-
-export interface Players {
-    "0": Player
-    "1": Player
-    "2": Player
-    "3": Player
-    "4": Player
-    "5": Player
-    "6": Player
-    "7": Player
-}
-
-export interface Shadow {
-    blur: number
-    color: string
-    offset: number[]
-}
-
-export type StyleSets = keyof Layer
-export interface Layer {
-    base: StyleSet
-    variant: StyleSet
-    on: StyleSet
-    accent: StyleSet
-    positive: StyleSet
-    warning: StyleSet
-    negative: StyleSet
-}
-
-export interface RampSet {
-    neutral: Scale
-    red: Scale
-    orange: Scale
-    yellow: Scale
-    green: Scale
-    cyan: Scale
-    blue: Scale
-    violet: Scale
-    magenta: Scale
-}
-
-export type Styles = keyof StyleSet
-export interface StyleSet {
-    default: Style
-    active: Style
-    disabled: Style
-    hovered: Style
-    pressed: Style
-    inverted: Style
-}
-
-export interface Style {
-    background: string
-    border: string
-    foreground: string
-}
-
-export function createColorScheme(theme: ThemeConfig): ColorScheme {
-    const {
-        name,
-        appearance,
-        inputColor,
-        override: { syntax },
-    } = theme
-
-    const isLight = appearance === ThemeAppearance.Light
-    const colorRamps: ThemeConfigInputColors = inputColor
-
-    // Chromajs scales from 0 to 1 flipped if isLight is true
-    const ramps = getRamps(isLight, colorRamps)
-    const lowest = lowestLayer(ramps)
-    const middle = middleLayer(ramps)
-    const highest = highestLayer(ramps)
-
-    const popoverShadow = {
-        blur: 4,
-        color: ramps
-            .neutral(isLight ? 7 : 0)
-            .darken()
-            .alpha(0.2)
-            .hex(), // TODO used blend previously. Replace with something else
-        offset: [1, 2],
-    }
-
-    const modalShadow = {
-        blur: 16,
-        color: ramps
-            .neutral(isLight ? 7 : 0)
-            .darken()
-            .alpha(0.2)
-            .hex(), // TODO used blend previously. Replace with something else
-        offset: [0, 2],
-    }
-
-    const players = {
-        "0": player(ramps.blue),
-        "1": player(ramps.green),
-        "2": player(ramps.magenta),
-        "3": player(ramps.orange),
-        "4": player(ramps.violet),
-        "5": player(ramps.cyan),
-        "6": player(ramps.red),
-        "7": player(ramps.yellow),
-    }
-
-    return {
-        name,
-        isLight,
-
-        ramps,
-
-        lowest,
-        middle,
-        highest,
-
-        popoverShadow,
-        modalShadow,
-
-        players,
-        syntax,
-    }
-}
-
-function player(ramp: Scale): Player {
-    return {
-        selection: ramp(0.5).alpha(0.24).hex(),
-        cursor: ramp(0.5).hex(),
-    }
-}
-
-function lowestLayer(ramps: RampSet): Layer {
-    return {
-        base: buildStyleSet(ramps.neutral, 0.2, 1),
-        variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
-        on: buildStyleSet(ramps.neutral, 0.1, 1),
-        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
-        positive: buildStyleSet(ramps.green, 0.1, 0.5),
-        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
-        negative: buildStyleSet(ramps.red, 0.1, 0.5),
-    }
-}
-
-function middleLayer(ramps: RampSet): Layer {
-    return {
-        base: buildStyleSet(ramps.neutral, 0.1, 1),
-        variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
-        on: buildStyleSet(ramps.neutral, 0, 1),
-        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
-        positive: buildStyleSet(ramps.green, 0.1, 0.5),
-        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
-        negative: buildStyleSet(ramps.red, 0.1, 0.5),
-    }
-}
-
-function highestLayer(ramps: RampSet): Layer {
-    return {
-        base: buildStyleSet(ramps.neutral, 0, 1),
-        variant: buildStyleSet(ramps.neutral, 0, 0.7),
-        on: buildStyleSet(ramps.neutral, 0.1, 1),
-        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
-        positive: buildStyleSet(ramps.green, 0.1, 0.5),
-        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
-        negative: buildStyleSet(ramps.red, 0.1, 0.5),
-    }
-}
-
-function buildStyleSet(
-    ramp: Scale,
-    backgroundBase: number,
-    foregroundBase: number,
-    step: number = 0.08
-): StyleSet {
-    let styleDefinitions = buildStyleDefinition(
-        backgroundBase,
-        foregroundBase,
-        step
-    )
-
-    function colorString(indexOrColor: number | Color): string {
-        if (typeof indexOrColor === "number") {
-            return ramp(indexOrColor).hex()
-        } else {
-            return indexOrColor.hex()
-        }
-    }
-
-    function buildStyle(style: Styles): Style {
-        return {
-            background: colorString(styleDefinitions.background[style]),
-            border: colorString(styleDefinitions.border[style]),
-            foreground: colorString(styleDefinitions.foreground[style]),
-        }
-    }
-
-    return {
-        default: buildStyle("default"),
-        hovered: buildStyle("hovered"),
-        pressed: buildStyle("pressed"),
-        active: buildStyle("active"),
-        disabled: buildStyle("disabled"),
-        inverted: buildStyle("inverted"),
-    }
-}
-
-function buildStyleDefinition(
-    bgBase: number,
-    fgBase: number,
-    step: number = 0.08
-) {
-    return {
-        background: {
-            default: bgBase,
-            hovered: bgBase + step,
-            pressed: bgBase + step * 1.5,
-            active: bgBase + step * 2.2,
-            disabled: bgBase,
-            inverted: fgBase + step * 6,
-        },
-        border: {
-            default: bgBase + step * 1,
-            hovered: bgBase + step,
-            pressed: bgBase + step,
-            active: bgBase + step * 3,
-            disabled: bgBase + step * 0.5,
-            inverted: bgBase - step * 3,
-        },
-        foreground: {
-            default: fgBase,
-            hovered: fgBase,
-            pressed: fgBase,
-            active: fgBase + step * 6,
-            disabled: bgBase + step * 4,
-            inverted: bgBase + step * 2,
-        },
-    }
-}

styles/src/theme/create_theme.ts πŸ”—

@@ -0,0 +1,282 @@
+import { Scale, Color } from "chroma-js"
+import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
+export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
+import {
+    ThemeConfig,
+    ThemeAppearance,
+    ThemeConfigInputColors,
+} from "./theme_config"
+import { get_ramps } from "./ramps"
+
+export interface Theme {
+    name: string
+    is_light: boolean
+
+    lowest: Layer
+    middle: Layer
+    highest: Layer
+
+    ramps: RampSet
+
+    popover_shadow: Shadow
+    modal_shadow: Shadow
+
+    players: Players
+    syntax?: Partial<ThemeSyntax>
+}
+
+export interface Meta {
+    name: string
+    author: string
+    url: string
+    license: License
+}
+
+export interface License {
+    SPDX: SPDXExpression
+}
+
+// License name -> License text
+export interface Licenses {
+    [key: string]: string
+}
+
+// FIXME: Add support for the SPDX expression syntax
+export type SPDXExpression = "MIT"
+
+export interface Player {
+    cursor: string
+    selection: string
+}
+
+export interface Players {
+    "0": Player
+    "1": Player
+    "2": Player
+    "3": Player
+    "4": Player
+    "5": Player
+    "6": Player
+    "7": Player
+}
+
+export interface Shadow {
+    blur: number
+    color: string
+    offset: number[]
+}
+
+export type StyleSets = keyof Layer
+export interface Layer {
+    base: StyleSet
+    variant: StyleSet
+    on: StyleSet
+    accent: StyleSet
+    positive: StyleSet
+    warning: StyleSet
+    negative: StyleSet
+}
+
+export interface RampSet {
+    neutral: Scale
+    red: Scale
+    orange: Scale
+    yellow: Scale
+    green: Scale
+    cyan: Scale
+    blue: Scale
+    violet: Scale
+    magenta: Scale
+}
+
+export type Styles = keyof StyleSet
+export interface StyleSet {
+    default: Style
+    active: Style
+    disabled: Style
+    hovered: Style
+    pressed: Style
+    inverted: Style
+}
+
+export interface Style {
+    background: string
+    border: string
+    foreground: string
+}
+
+export function create_theme(theme: ThemeConfig): Theme {
+    const {
+        name,
+        appearance,
+        input_color,
+        override: { syntax },
+    } = theme
+
+    const is_light = appearance === ThemeAppearance.Light
+    const color_ramps: ThemeConfigInputColors = input_color
+
+    // Chromajs scales from 0 to 1 flipped if is_light is true
+    const ramps = get_ramps(is_light, color_ramps)
+    const lowest = lowest_layer(ramps)
+    const middle = middle_layer(ramps)
+    const highest = highest_layer(ramps)
+
+    const popover_shadow = {
+        blur: 4,
+        color: ramps
+            .neutral(is_light ? 7 : 0)
+            .darken()
+            .alpha(0.2)
+            .hex(), // TODO used blend previously. Replace with something else
+        offset: [1, 2],
+    }
+
+    const modal_shadow = {
+        blur: 16,
+        color: ramps
+            .neutral(is_light ? 7 : 0)
+            .darken()
+            .alpha(0.2)
+            .hex(), // TODO used blend previously. Replace with something else
+        offset: [0, 2],
+    }
+
+    const players = {
+        "0": player(ramps.blue),
+        "1": player(ramps.green),
+        "2": player(ramps.magenta),
+        "3": player(ramps.orange),
+        "4": player(ramps.violet),
+        "5": player(ramps.cyan),
+        "6": player(ramps.red),
+        "7": player(ramps.yellow),
+    }
+
+    return {
+        name,
+        is_light,
+
+        ramps,
+
+        lowest,
+        middle,
+        highest,
+
+        popover_shadow,
+        modal_shadow,
+
+        players,
+        syntax,
+    }
+}
+
+function player(ramp: Scale): Player {
+    return {
+        selection: ramp(0.5).alpha(0.24).hex(),
+        cursor: ramp(0.5).hex(),
+    }
+}
+
+function lowest_layer(ramps: RampSet): Layer {
+    return {
+        base: build_style_set(ramps.neutral, 0.2, 1),
+        variant: build_style_set(ramps.neutral, 0.2, 0.7),
+        on: build_style_set(ramps.neutral, 0.1, 1),
+        accent: build_style_set(ramps.blue, 0.1, 0.5),
+        positive: build_style_set(ramps.green, 0.1, 0.5),
+        warning: build_style_set(ramps.yellow, 0.1, 0.5),
+        negative: build_style_set(ramps.red, 0.1, 0.5),
+    }
+}
+
+function middle_layer(ramps: RampSet): Layer {
+    return {
+        base: build_style_set(ramps.neutral, 0.1, 1),
+        variant: build_style_set(ramps.neutral, 0.1, 0.7),
+        on: build_style_set(ramps.neutral, 0, 1),
+        accent: build_style_set(ramps.blue, 0.1, 0.5),
+        positive: build_style_set(ramps.green, 0.1, 0.5),
+        warning: build_style_set(ramps.yellow, 0.1, 0.5),
+        negative: build_style_set(ramps.red, 0.1, 0.5),
+    }
+}
+
+function highest_layer(ramps: RampSet): Layer {
+    return {
+        base: build_style_set(ramps.neutral, 0, 1),
+        variant: build_style_set(ramps.neutral, 0, 0.7),
+        on: build_style_set(ramps.neutral, 0.1, 1),
+        accent: build_style_set(ramps.blue, 0.1, 0.5),
+        positive: build_style_set(ramps.green, 0.1, 0.5),
+        warning: build_style_set(ramps.yellow, 0.1, 0.5),
+        negative: build_style_set(ramps.red, 0.1, 0.5),
+    }
+}
+
+function build_style_set(
+    ramp: Scale,
+    background_base: number,
+    foreground_base: number,
+    step = 0.08
+): StyleSet {
+    const style_definitions = build_style_definition(
+        background_base,
+        foreground_base,
+        step
+    )
+
+    function color_string(index_or_color: number | Color): string {
+        if (typeof index_or_color === "number") {
+            return ramp(index_or_color).hex()
+        } else {
+            return index_or_color.hex()
+        }
+    }
+
+    function build_style(style: Styles): Style {
+        return {
+            background: color_string(style_definitions.background[style]),
+            border: color_string(style_definitions.border[style]),
+            foreground: color_string(style_definitions.foreground[style]),
+        }
+    }
+
+    return {
+        default: build_style("default"),
+        hovered: build_style("hovered"),
+        pressed: build_style("pressed"),
+        active: build_style("active"),
+        disabled: build_style("disabled"),
+        inverted: build_style("inverted"),
+    }
+}
+
+function build_style_definition(bg_base: number, fg_base: number, step = 0.08) {
+    return {
+        background: {
+            default: bg_base,
+            hovered: bg_base + step,
+            pressed: bg_base + step * 1.5,
+            active: bg_base + step * 2.2,
+            disabled: bg_base,
+            inverted: fg_base + step * 6,
+        },
+        border: {
+            default: bg_base + step * 1,
+            hovered: bg_base + step,
+            pressed: bg_base + step,
+            active: bg_base + step * 3,
+            disabled: bg_base + step * 0.5,
+            inverted: bg_base - step * 3,
+        },
+        foreground: {
+            default: fg_base,
+            hovered: fg_base,
+            pressed: fg_base,
+            active: fg_base + step * 6,
+            disabled: bg_base + step * 4,
+            inverted: bg_base + step * 2,
+        },
+    }
+}

styles/src/theme/index.ts πŸ”—

@@ -1,4 +1,25 @@
-export * from "./colorScheme"
+import { create } from "zustand"
+import { Theme } from "./create_theme"
+
+type ThemeState = {
+    theme: Theme | undefined
+    setTheme: (theme: Theme) => void
+}
+
+export const useThemeStore = create<ThemeState>((set) => ({
+    theme: undefined,
+    setTheme: (theme) => set(() => ({ theme })),
+}))
+
+export const useTheme = (): Theme => {
+    const { theme } = useThemeStore.getState()
+
+    if (!theme) throw new Error("Tried to use theme before it was loaded")
+
+    return theme
+}
+
+export * from "./create_theme"
 export * from "./ramps"
 export * from "./syntax"
-export * from "./themeConfig"
+export * from "./theme_config"

styles/src/theme/ramps.ts πŸ”—

@@ -1,14 +1,14 @@
 import chroma, { Color, Scale } from "chroma-js"
-import { RampSet } from "./colorScheme"
+import { RampSet } from "./create_theme"
 import {
     ThemeConfigInputColors,
     ThemeConfigInputColorsKeys,
-} from "./themeConfig"
+} from "./theme_config"
 
-export function colorRamp(color: Color): Scale {
-    let endColor = color.desaturate(1).brighten(5)
-    let startColor = color.desaturate(1).darken(4)
-    return chroma.scale([startColor, color, endColor]).mode("lab")
+export function color_ramp(color: Color): Scale {
+    const end_color = color.desaturate(1).brighten(5)
+    const start_color = color.desaturate(1).darken(4)
+    return chroma.scale([start_color, color, end_color]).mode("lab")
 }
 
 /**
@@ -18,29 +18,29 @@ export function colorRamp(color: Color): Scale {
     theme so that we don't modify the passed in ramps.
     This combined with an error in the type definitions for chroma js means we have to cast the colors
     function to any in order to get the colors back out from the original ramps.
- * @param isLight 
- * @param colorRamps 
- * @returns 
+ * @param is_light
+ * @param color_ramps
+ * @returns
  */
-export function getRamps(
-    isLight: boolean,
-    colorRamps: ThemeConfigInputColors
+export function get_ramps(
+    is_light: boolean,
+    color_ramps: ThemeConfigInputColors
 ): RampSet {
-    const ramps: RampSet = {} as any
-    const colorsKeys = Object.keys(colorRamps) as ThemeConfigInputColorsKeys[]
+    const ramps: RampSet = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
+    const color_keys = Object.keys(color_ramps) as ThemeConfigInputColorsKeys[]
 
-    if (isLight) {
-        for (const rampName of colorsKeys) {
-            ramps[rampName] = chroma.scale(
-                colorRamps[rampName].colors(100).reverse()
+    if (is_light) {
+        for (const ramp_name of color_keys) {
+            ramps[ramp_name] = chroma.scale(
+                color_ramps[ramp_name].colors(100).reverse()
             )
         }
-        ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
+        ramps.neutral = chroma.scale(color_ramps.neutral.colors(100).reverse())
     } else {
-        for (const rampName of colorsKeys) {
-            ramps[rampName] = chroma.scale(colorRamps[rampName].colors(100))
+        for (const ramp_name of color_keys) {
+            ramps[ramp_name] = chroma.scale(color_ramps[ramp_name].colors(100))
         }
-        ramps.neutral = chroma.scale(colorRamps.neutral.colors(100))
+        ramps.neutral = chroma.scale(color_ramps.neutral.colors(100))
     }
 
     return ramps

styles/src/theme/syntax.ts πŸ”—

@@ -1,6 +1,5 @@
 import deepmerge from "deepmerge"
-import { FontWeight, fontWeights } from "../common"
-import { ColorScheme } from "./colorScheme"
+import { FontWeight, font_weights, useTheme } from "../common"
 import chroma from "chroma-js"
 
 export interface SyntaxHighlightStyle {
@@ -17,13 +16,14 @@ export interface Syntax {
     "comment.doc": SyntaxHighlightStyle
     primary: SyntaxHighlightStyle
     predictive: SyntaxHighlightStyle
+    hint: SyntaxHighlightStyle
 
     // === Formatted Text ====== /
     emphasis: SyntaxHighlightStyle
     "emphasis.strong": SyntaxHighlightStyle
     title: SyntaxHighlightStyle
-    linkUri: SyntaxHighlightStyle
-    linkText: SyntaxHighlightStyle
+    link_uri: SyntaxHighlightStyle
+    link_text: SyntaxHighlightStyle
     /** md: indented_code_block, fenced_code_block, code_span */
     "text.literal": SyntaxHighlightStyle
 
@@ -56,7 +56,7 @@ export interface Syntax {
 
     // == Types ====== /
     // We allow Function here because all JS objects literals have this property
-    constructor: SyntaxHighlightStyle | Function
+    constructor: SyntaxHighlightStyle | Function // eslint-disable-line  @typescript-eslint/ban-types
     variant: SyntaxHighlightStyle
     type: SyntaxHighlightStyle
     // js: predefined_type
@@ -116,25 +116,25 @@ export interface Syntax {
 
 export type ThemeSyntax = Partial<Syntax>
 
-const defaultSyntaxHighlightStyle: Omit<SyntaxHighlightStyle, "color"> = {
+const default_syntax_highlight_style: Omit<SyntaxHighlightStyle, "color"> = {
     weight: "normal",
     underline: false,
     italic: false,
 }
 
-function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
+function build_default_syntax(): Syntax {
+    const theme = useTheme()
+
     // Make a temporary object that is allowed to be missing
     // the "color" property for each style
     const syntax: {
         [key: string]: Omit<SyntaxHighlightStyle, "color">
     } = {}
 
-    const light = colorScheme.isLight
-
     // then spread the default to each style
     for (const key of Object.keys({} as Syntax)) {
         syntax[key as keyof Syntax] = {
-            ...defaultSyntaxHighlightStyle,
+            ...default_syntax_highlight_style,
         }
     }
 
@@ -142,35 +142,46 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
     // predictive color distinct from any other color in the theme
     const predictive = chroma
         .mix(
-            colorScheme.ramps.neutral(0.4).hex(),
-            colorScheme.ramps.blue(0.4).hex(),
+            theme.ramps.neutral(0.4).hex(),
+            theme.ramps.blue(0.4).hex(),
+            0.45,
+            "lch"
+        )
+        .hex()
+    // Mix the neutral and green colors to get a
+    // hint color distinct from any other color in the theme
+    const hint = chroma
+        .mix(
+            theme.ramps.neutral(0.6).hex(),
+            theme.ramps.blue(0.4).hex(),
             0.45,
             "lch"
         )
         .hex()
 
     const color = {
-        primary: colorScheme.ramps.neutral(1).hex(),
-        comment: colorScheme.ramps.neutral(0.71).hex(),
-        punctuation: colorScheme.ramps.neutral(0.86).hex(),
+        primary: theme.ramps.neutral(1).hex(),
+        comment: theme.ramps.neutral(0.71).hex(),
+        punctuation: theme.ramps.neutral(0.86).hex(),
         predictive: predictive,
-        emphasis: colorScheme.ramps.blue(0.5).hex(),
-        string: colorScheme.ramps.orange(0.5).hex(),
-        function: colorScheme.ramps.yellow(0.5).hex(),
-        type: colorScheme.ramps.cyan(0.5).hex(),
-        constructor: colorScheme.ramps.blue(0.5).hex(),
-        variant: colorScheme.ramps.blue(0.5).hex(),
-        property: colorScheme.ramps.blue(0.5).hex(),
-        enum: colorScheme.ramps.orange(0.5).hex(),
-        operator: colorScheme.ramps.orange(0.5).hex(),
-        number: colorScheme.ramps.green(0.5).hex(),
-        boolean: colorScheme.ramps.green(0.5).hex(),
-        constant: colorScheme.ramps.green(0.5).hex(),
-        keyword: colorScheme.ramps.blue(0.5).hex(),
+        hint: hint,
+        emphasis: theme.ramps.blue(0.5).hex(),
+        string: theme.ramps.orange(0.5).hex(),
+        function: theme.ramps.yellow(0.5).hex(),
+        type: theme.ramps.cyan(0.5).hex(),
+        constructor: theme.ramps.blue(0.5).hex(),
+        variant: theme.ramps.blue(0.5).hex(),
+        property: theme.ramps.blue(0.5).hex(),
+        enum: theme.ramps.orange(0.5).hex(),
+        operator: theme.ramps.orange(0.5).hex(),
+        number: theme.ramps.green(0.5).hex(),
+        boolean: theme.ramps.green(0.5).hex(),
+        constant: theme.ramps.green(0.5).hex(),
+        keyword: theme.ramps.blue(0.5).hex(),
     }
 
     // Then assign colors and use Syntax to enforce each style getting it's own color
-    const defaultSyntax: Syntax = {
+    const default_syntax: Syntax = {
         ...syntax,
         comment: {
             color: color.comment,
@@ -185,23 +196,27 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
             color: color.predictive,
             italic: true,
         },
+        hint: {
+            color: color.hint,
+            weight: font_weights.bold,
+        },
         emphasis: {
             color: color.emphasis,
         },
         "emphasis.strong": {
             color: color.emphasis,
-            weight: fontWeights.bold,
+            weight: font_weights.bold,
         },
         title: {
             color: color.primary,
-            weight: fontWeights.bold,
+            weight: font_weights.bold,
         },
-        linkUri: {
-            color: colorScheme.ramps.green(0.5).hex(),
+        link_uri: {
+            color: theme.ramps.green(0.5).hex(),
             underline: true,
         },
-        linkText: {
-            color: colorScheme.ramps.orange(0.5).hex(),
+        link_text: {
+            color: theme.ramps.orange(0.5).hex(),
             italic: true,
         },
         "text.literal": {
@@ -217,7 +232,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
             color: color.punctuation,
         },
         "punctuation.special": {
-            color: colorScheme.ramps.neutral(0.86).hex(),
+            color: theme.ramps.neutral(0.86).hex(),
         },
         "punctuation.list_marker": {
             color: color.punctuation,
@@ -238,10 +253,10 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
             color: color.string,
         },
         constructor: {
-            color: colorScheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         variant: {
-            color: colorScheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         type: {
             color: color.type,
@@ -250,16 +265,16 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
             color: color.primary,
         },
         label: {
-            color: colorScheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         tag: {
-            color: colorScheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         attribute: {
-            color: colorScheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         property: {
-            color: colorScheme.ramps.blue(0.5).hex(),
+            color: theme.ramps.blue(0.5).hex(),
         },
         constant: {
             color: color.constant,
@@ -290,17 +305,21 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
         },
     }
 
-    return defaultSyntax
+    return default_syntax
 }
 
-function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
-    if (!colorScheme.syntax) {
-        return defaultSyntax
+export function build_syntax(): Syntax {
+    const theme = useTheme()
+
+    const default_syntax: Syntax = build_default_syntax()
+
+    if (!theme.syntax) {
+        return default_syntax
     }
 
-    return deepmerge<Syntax, Partial<ThemeSyntax>>(
-        defaultSyntax,
-        colorScheme.syntax,
+    const syntax = deepmerge<Syntax, Partial<ThemeSyntax>>(
+        default_syntax,
+        theme.syntax,
         {
             arrayMerge: (destinationArray, sourceArray) => [
                 ...destinationArray,
@@ -308,12 +327,6 @@ function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax {
             ],
         }
     )
-}
-
-export function buildSyntax(colorScheme: ColorScheme): Syntax {
-    const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme)
-
-    const syntax = mergeSyntax(defaultSyntax, colorScheme)
 
     return syntax
 }

styles/src/theme/themeConfig.ts πŸ”—

@@ -1,148 +0,0 @@
-import { Scale, Color } from "chroma-js"
-import { Syntax } from "./syntax"
-
-interface ThemeMeta {
-    /** The name of the theme */
-    name: string
-    /** The theme's appearance. Either `light` or `dark`. */
-    appearance: ThemeAppearance
-    /** The author of the theme
-     *
-     * Ideally formatted as `Full Name <email>`
-     *
-     * Example: `John Doe <john@doe.com>`
-     */
-    author: string
-    /** SPDX License string
-     *
-     * Example: `MIT`
-     */
-    licenseType?: string | ThemeLicenseType
-    licenseUrl?: string
-    licenseFile: string
-    themeUrl?: string
-}
-
-export type ThemeFamilyMeta = Pick<
-    ThemeMeta,
-    "name" | "author" | "licenseType" | "licenseUrl"
->
-
-export interface ThemeConfigInputColors {
-    neutral: Scale<Color>
-    red: Scale<Color>
-    orange: Scale<Color>
-    yellow: Scale<Color>
-    green: Scale<Color>
-    cyan: Scale<Color>
-    blue: Scale<Color>
-    violet: Scale<Color>
-    magenta: Scale<Color>
-}
-
-export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors
-
-/** Allow any part of a syntax highlight style to be overriden by the theme
- *
- * Example:
- * ```ts
- * override: {
- *   syntax: {
- *     boolean: {
- *       underline: true,
- *     },
- *   },
- * }
- * ```
- */
-export type ThemeConfigInputSyntax = Partial<Syntax>
-
-interface ThemeConfigOverrides {
-    syntax: ThemeConfigInputSyntax
-}
-
-type ThemeConfigProperties = ThemeMeta & {
-    inputColor: ThemeConfigInputColors
-    override: ThemeConfigOverrides
-}
-
-// This should be the format a theme is defined as
-export type ThemeConfig = {
-    [K in keyof ThemeConfigProperties]: ThemeConfigProperties[K]
-}
-
-interface ThemeColors {
-    neutral: string[]
-    red: string[]
-    orange: string[]
-    yellow: string[]
-    green: string[]
-    cyan: string[]
-    blue: string[]
-    violet: string[]
-    magenta: string[]
-}
-
-type ThemeSyntax = Required<Syntax>
-
-export type ThemeProperties = ThemeMeta & {
-    color: ThemeColors
-    syntax: ThemeSyntax
-}
-
-// This should be a theme after all its properties have been resolved
-export type Theme = {
-    [K in keyof ThemeProperties]: ThemeProperties[K]
-}
-
-export enum ThemeAppearance {
-    Light = "light",
-    Dark = "dark",
-}
-
-export enum ThemeLicenseType {
-    MIT = "MIT",
-    Apache2 = "Apache License 2.0",
-}
-
-export type ThemeFamilyItem =
-    | ThemeConfig
-    | { light: ThemeConfig; dark: ThemeConfig }
-
-type ThemeFamilyProperties = Partial<Omit<ThemeMeta, "name" | "appearance">> & {
-    name: string
-    default: ThemeFamilyItem
-    variants: {
-        [key: string]: ThemeFamilyItem
-    }
-}
-
-// Idea: A theme family is a collection of themes that share the same name
-// For example, a theme family could be `One Dark` and have a `light` and `dark` variant
-// The Ayu family could have `light`, `mirage`, and `dark` variants
-
-type ThemeFamily = {
-    [K in keyof ThemeFamilyProperties]: ThemeFamilyProperties[K]
-}
-
-/** The collection of all themes
- *
- * Example:
- * ```ts
- * {
- *   one_dark,
- *   one_light,
- *     ayu: {
- *     name: 'Ayu',
- *     default: 'ayu_mirage',
- *     variants: {
- *       light: 'ayu_light',
- *       mirage: 'ayu_mirage',
- *       dark: 'ayu_dark',
- *     },
- *   },
- *  ...
- * }
- * ```
- */
-export type ThemeIndex = Record<string, ThemeFamily | ThemeConfig>

styles/src/theme/theme_config.ts πŸ”—

@@ -0,0 +1,81 @@
+import { Scale, Color } from "chroma-js"
+import { Syntax } from "./syntax"
+
+interface ThemeMeta {
+    /** The name of the theme */
+    name: string
+    /** The theme's appearance. Either `light` or `dark`. */
+    appearance: ThemeAppearance
+    /** The author of the theme
+     *
+     * Ideally formatted as `Full Name <email>`
+     *
+     * Example: `John Doe <john@doe.com>`
+     */
+    author: string
+    /** SPDX License string
+     *
+     * Example: `MIT`
+     */
+    license_type?: string | ThemeLicenseType
+    license_url?: string
+    license_file: string
+    theme_url?: string
+}
+
+export type ThemeFamilyMeta = Pick<
+    ThemeMeta,
+    "name" | "author" | "license_type" | "license_url"
+>
+
+export interface ThemeConfigInputColors {
+    neutral: Scale<Color>
+    red: Scale<Color>
+    orange: Scale<Color>
+    yellow: Scale<Color>
+    green: Scale<Color>
+    cyan: Scale<Color>
+    blue: Scale<Color>
+    violet: Scale<Color>
+    magenta: Scale<Color>
+}
+
+export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors
+
+/** Allow any part of a syntax highlight style to be overriden by the theme
+ *
+ * Example:
+ * ```ts
+ * override: {
+ *   syntax: {
+ *     boolean: {
+ *       underline: true,
+ *     },
+ *   },
+ * }
+ * ```
+ */
+export type ThemeConfigInputSyntax = Partial<Syntax>
+
+interface ThemeConfigOverrides {
+    syntax: ThemeConfigInputSyntax
+}
+
+type ThemeConfigProperties = ThemeMeta & {
+    input_color: ThemeConfigInputColors
+    override: ThemeConfigOverrides
+}
+
+export type ThemeConfig = {
+    [K in keyof ThemeConfigProperties]: ThemeConfigProperties[K]
+}
+
+export enum ThemeAppearance {
+    Light = "light",
+    Dark = "dark",
+}
+
+export enum ThemeLicenseType {
+    MIT = "MIT",
+    Apache2 = "Apache License 2.0",
+}

styles/src/theme/tokens/colorScheme.ts πŸ”—

@@ -1,81 +0,0 @@
-import { SingleBoxShadowToken, SingleColorToken, SingleOtherToken, TokenTypes } from "@tokens-studio/types"
-import { ColorScheme, Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../colorScheme"
-import { LayerToken, layerToken } from "./layer"
-import { PlayersToken, playersToken } from "./players"
-import { colorToken } from "./token"
-import { Syntax } from "../syntax";
-import editor from "../../styleTree/editor"
-
-interface ColorSchemeTokens {
-    name: SingleOtherToken
-    appearance: SingleOtherToken
-    lowest: LayerToken
-    middle: LayerToken
-    highest: LayerToken
-    players: PlayersToken
-    popoverShadow: SingleBoxShadowToken
-    modalShadow: SingleBoxShadowToken
-    syntax?: Partial<ThemeSyntaxColorTokens>
-}
-
-const createShadowToken = (shadow: Shadow, tokenName: string): SingleBoxShadowToken => {
-    return {
-        name: tokenName,
-        type: TokenTypes.BOX_SHADOW,
-        value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`
-    };
-};
-
-const popoverShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
-    const shadow = colorScheme.popoverShadow;
-    return createShadowToken(shadow, "popoverShadow");
-};
-
-const modalShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => {
-    const shadow = colorScheme.modalShadow;
-    return createShadowToken(shadow, "modalShadow");
-};
-
-type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
-
-function syntaxHighlightStyleColorTokens(syntax: Syntax): ThemeSyntaxColorTokens {
-    const styleKeys = Object.keys(syntax) as (keyof Syntax)[]
-
-    return styleKeys.reduce((acc, styleKey) => {
-        // Hack: The type of a style could be "Function"
-        // This can happen because we have a "constructor" property on the syntax object
-        // and a "constructor" property on the prototype of the syntax object
-        // To work around this just assert that the type of the style is not a function
-        if (!syntax[styleKey] || typeof syntax[styleKey] === 'function') return acc;
-        const { color } = syntax[styleKey] as Required<SyntaxHighlightStyle>;
-        return { ...acc, [styleKey]: colorToken(styleKey, color) };
-    }, {} as ThemeSyntaxColorTokens);
-}
-
-const syntaxTokens = (colorScheme: ColorScheme): ColorSchemeTokens['syntax'] => {
-    const syntax = editor(colorScheme).syntax
-
-    return syntaxHighlightStyleColorTokens(syntax)
-}
-
-export function colorSchemeTokens(colorScheme: ColorScheme): ColorSchemeTokens {
-    return {
-        name: {
-            name: "themeName",
-            value: colorScheme.name,
-            type: TokenTypes.OTHER,
-        },
-        appearance: {
-            name: "themeAppearance",
-            value: colorScheme.isLight ? "light" : "dark",
-            type: TokenTypes.OTHER,
-        },
-        lowest: layerToken(colorScheme.lowest, "lowest"),
-        middle: layerToken(colorScheme.middle, "middle"),
-        highest: layerToken(colorScheme.highest, "highest"),
-        popoverShadow: popoverShadowToken(colorScheme),
-        modalShadow: modalShadowToken(colorScheme),
-        players: playersToken(colorScheme),
-        syntax: syntaxTokens(colorScheme),
-    }
-}

styles/src/theme/tokens/layer.ts πŸ”—

@@ -1,11 +1,11 @@
-import { SingleColorToken } from "@tokens-studio/types";
-import { Layer, Style, StyleSet } from "../colorScheme";
-import { colorToken } from "./token";
+import { SingleColorToken } from "@tokens-studio/types"
+import { Layer, Style, StyleSet } from "../create_theme"
+import { color_token } from "./token"
 
 interface StyleToken {
-    background: SingleColorToken,
-    border: SingleColorToken,
-    foreground: SingleColorToken,
+    background: SingleColorToken
+    border: SingleColorToken
+    foreground: SingleColorToken
 }
 
 interface StyleSetToken {
@@ -27,34 +27,37 @@ export interface LayerToken {
     negative: StyleSetToken
 }
 
-export const styleToken = (style: Style, name: string): StyleToken => {
+export const style_token = (style: Style, name: string): StyleToken => {
     const token = {
-        background: colorToken(`${name}Background`, style.background),
-        border: colorToken(`${name}Border`, style.border),
-        foreground: colorToken(`${name}Foreground`, style.foreground),
+        background: color_token(`${name}Background`, style.background),
+        border: color_token(`${name}Border`, style.border),
+        foreground: color_token(`${name}Foreground`, style.foreground),
     }
 
     return token
 }
 
-export const styleSetToken = (styleSet: StyleSet, name: string): StyleSetToken => {
-    const token: StyleSetToken = {} as StyleSetToken;
+export const style_set_token = (
+    style_set: StyleSet,
+    name: string
+): StyleSetToken => {
+    const token: StyleSetToken = {} as StyleSetToken
 
-    for (const style in styleSet) {
-        const s = style as keyof StyleSet;
-        token[s] = styleToken(styleSet[s], `${name}${style}`);
+    for (const style in style_set) {
+        const s = style as keyof StyleSet
+        token[s] = style_token(style_set[s], `${name}${style}`)
     }
 
-    return token;
+    return token
 }
 
-export const layerToken = (layer: Layer, name: string): LayerToken => {
-    const token: LayerToken = {} as LayerToken;
+export const layer_token = (layer: Layer, name: string): LayerToken => {
+    const token: LayerToken = {} as LayerToken
 
-    for (const styleSet in layer) {
-        const s = styleSet as keyof Layer;
-        token[s] = styleSetToken(layer[s], `${name}${styleSet}`);
+    for (const style_set in layer) {
+        const s = style_set as keyof Layer
+        token[s] = style_set_token(layer[s], `${name}${style_set}`)
     }
 
-    return token;
+    return token
 }

styles/src/theme/tokens/players.ts πŸ”—

@@ -1,28 +1,37 @@
 import { SingleColorToken } from "@tokens-studio/types"
-import { ColorScheme, Players } from "../../common"
-import { colorToken } from "./token"
+import { color_token } from "./token"
+import { Players } from "../create_theme"
+import { useTheme } from "../../../src/common"
 
 export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
 
 export type PlayersToken = Record<keyof Players, PlayerToken>
 
-function buildPlayerToken(colorScheme: ColorScheme, index: number): PlayerToken {
-
-    const playerNumber = index.toString() as keyof Players
+function build_player_token(index: number): PlayerToken {
+    const theme = useTheme()
+    const player_number = index.toString() as keyof Players
 
     return {
-        selection: colorToken(`player${index}Selection`, colorScheme.players[playerNumber].selection),
-        cursor: colorToken(`player${index}Cursor`, colorScheme.players[playerNumber].cursor),
+        selection: color_token(
+            `player${index}Selection`,
+            theme.players[player_number].selection
+        ),
+        cursor: color_token(
+            `player${index}Cursor`,
+            theme.players[player_number].cursor
+        ),
     }
 }
 
-export const playersToken = (colorScheme: ColorScheme): PlayersToken => ({
-    "0": buildPlayerToken(colorScheme, 0),
-    "1": buildPlayerToken(colorScheme, 1),
-    "2": buildPlayerToken(colorScheme, 2),
-    "3": buildPlayerToken(colorScheme, 3),
-    "4": buildPlayerToken(colorScheme, 4),
-    "5": buildPlayerToken(colorScheme, 5),
-    "6": buildPlayerToken(colorScheme, 6),
-    "7": buildPlayerToken(colorScheme, 7)
-})
+export const players_token = (): PlayersToken => {
+    return {
+        "0": build_player_token(0),
+        "1": build_player_token(1),
+        "2": build_player_token(2),
+        "3": build_player_token(3),
+        "4": build_player_token(4),
+        "5": build_player_token(5),
+        "6": build_player_token(6),
+        "7": build_player_token(7),
+    }
+}

styles/src/theme/tokens/theme.ts πŸ”—

@@ -0,0 +1,101 @@
+import {
+    SingleBoxShadowToken,
+    SingleColorToken,
+    SingleOtherToken,
+    TokenTypes,
+} from "@tokens-studio/types"
+import {
+    Shadow,
+    SyntaxHighlightStyle,
+    ThemeSyntax,
+} from "../create_theme"
+import { LayerToken, layer_token } from "./layer"
+import { PlayersToken, players_token } from "./players"
+import { color_token } from "./token"
+import { Syntax } from "../syntax"
+import editor from "../../style_tree/editor"
+import { useTheme } from "../../../src/common"
+
+interface ThemeTokens {
+    name: SingleOtherToken
+    appearance: SingleOtherToken
+    lowest: LayerToken
+    middle: LayerToken
+    highest: LayerToken
+    players: PlayersToken
+    popover_shadow: SingleBoxShadowToken
+    modal_shadow: SingleBoxShadowToken
+    syntax?: Partial<ThemeSyntaxColorTokens>
+}
+
+const create_shadow_token = (
+    shadow: Shadow,
+    token_name: string
+): SingleBoxShadowToken => {
+    return {
+        name: token_name,
+        type: TokenTypes.BOX_SHADOW,
+        value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`,
+    }
+}
+
+const popover_shadow_token = (): SingleBoxShadowToken => {
+    const theme = useTheme()
+    const shadow = theme.popover_shadow
+    return create_shadow_token(shadow, "popover_shadow")
+}
+
+const modal_shadow_token = (): SingleBoxShadowToken => {
+    const theme = useTheme()
+    const shadow = theme.modal_shadow
+    return create_shadow_token(shadow, "modal_shadow")
+}
+
+type ThemeSyntaxColorTokens = Record<keyof ThemeSyntax, SingleColorToken>
+
+function syntax_highlight_style_color_tokens(
+    syntax: Syntax
+): ThemeSyntaxColorTokens {
+    const style_keys = Object.keys(syntax) as (keyof Syntax)[]
+
+    return style_keys.reduce((acc, style_key) => {
+        // Hack: The type of a style could be "Function"
+        // This can happen because we have a "constructor" property on the syntax object
+        // and a "constructor" property on the prototype of the syntax object
+        // To work around this just assert that the type of the style is not a function
+        if (!syntax[style_key] || typeof syntax[style_key] === "function")
+            return acc
+        const { color } = syntax[style_key] as Required<SyntaxHighlightStyle>
+        return { ...acc, [style_key]: color_token(style_key, color) }
+    }, {} as ThemeSyntaxColorTokens)
+}
+
+const syntax_tokens = (): ThemeTokens["syntax"] => {
+    const syntax = editor().syntax
+
+    return syntax_highlight_style_color_tokens(syntax)
+}
+
+export function theme_tokens(): ThemeTokens {
+    const theme = useTheme()
+
+    return {
+        name: {
+            name: "themeName",
+            value: theme.name,
+            type: TokenTypes.OTHER,
+        },
+        appearance: {
+            name: "themeAppearance",
+            value: theme.is_light ? "light" : "dark",
+            type: TokenTypes.OTHER,
+        },
+        lowest: layer_token(theme.lowest, "lowest"),
+        middle: layer_token(theme.middle, "middle"),
+        highest: layer_token(theme.highest, "highest"),
+        popover_shadow: popover_shadow_token(),
+        modal_shadow: modal_shadow_token(),
+        players: players_token(),
+        syntax: syntax_tokens(),
+    }
+}

styles/src/theme/tokens/token.ts πŸ”—

@@ -1,6 +1,10 @@
 import { SingleColorToken, TokenTypes } from "@tokens-studio/types"
 
-export function colorToken(name: string, value: string, description?: string): SingleColorToken {
+export function color_token(
+    name: string,
+    value: string,
+    description?: string
+): SingleColorToken {
     const token: SingleColorToken = {
         name,
         type: TokenTypes.COLOR,
@@ -8,7 +12,8 @@ export function colorToken(name: string, value: string, description?: string): S
         description,
     }
 
-    if (!token.value || token.value === '') throw new Error("Color token must have a value")
+    if (!token.value || token.value === "")
+        throw new Error("Color token must have a value")
 
     return token
 }

styles/src/themes/andromeda/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/andromeda/andromeda.ts πŸ”—

@@ -1,6 +1,6 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -10,10 +10,10 @@ export const dark: ThemeConfig = {
     name: "Andromeda",
     author: "EliverLara",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/EliverLara/Andromeda",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/EliverLara/Andromeda",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma
             .scale([
                 "#1E2025",
@@ -26,14 +26,14 @@ export const dark: ThemeConfig = {
                 "#F7F7F8",
             ])
             .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]),
-        red: colorRamp(chroma("#F92672")),
-        orange: colorRamp(chroma("#F39C12")),
-        yellow: colorRamp(chroma("#FFE66D")),
-        green: colorRamp(chroma("#96E072")),
-        cyan: colorRamp(chroma("#00E8C6")),
-        blue: colorRamp(chroma("#0CA793")),
-        violet: colorRamp(chroma("#8A3FA6")),
-        magenta: colorRamp(chroma("#C74DED")),
+        red: color_ramp(chroma("#F92672")),
+        orange: color_ramp(chroma("#F39C12")),
+        yellow: color_ramp(chroma("#FFE66D")),
+        green: color_ramp(chroma("#96E072")),
+        cyan: color_ramp(chroma("#00E8C6")),
+        blue: color_ramp(chroma("#0CA793")),
+        violet: color_ramp(chroma("#8A3FA6")),
+        magenta: color_ramp(chroma("#C74DED")),
     },
     override: { syntax: {} },
 }

styles/src/themes/atelier/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/atelier/atelier-cave-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Cave Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-cave-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Cave Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-dune-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Dune Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-dune-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Dune Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-estuary-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Estuary Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-estuary-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Estuary Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-forest-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Forest Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-forest-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Forest Light`,
         author: meta.author,
-        appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        appearance: ThemeAppearance.Light,
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-heath-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Heath Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-heath-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Heath Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-lakeside-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Lakeside Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-lakeside-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Lakeside Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-plateau-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Plateau Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-plateau-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Plateau Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-savanna-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Savanna Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-savanna-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Savanna Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-seaside-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Seaside Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-seaside-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Seaside Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-sulphurpool-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Sulphurpool Dark`,
         author: meta.author,
         appearance: ThemeAppearance.Dark,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale([
                 colors.base00,
                 colors.base01,
@@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                 colors.base06,
                 colors.base07,
             ]),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/atelier-sulphurpool-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common"
-import { meta, buildSyntax, Variant } from "./common"
+import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common"
+import { meta, build_syntax, Variant } from "./common"
 
 const variant: Variant = {
     colors: {
@@ -22,19 +22,19 @@ const variant: Variant = {
     },
 }
 
-const syntax = buildSyntax(variant)
+const syntax = build_syntax(variant)
 
-const getTheme = (variant: Variant): ThemeConfig => {
+const get_theme = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     return {
         name: `${meta.name} Sulphurpool Light`,
         author: meta.author,
         appearance: ThemeAppearance.Light,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: {
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: {
             neutral: chroma.scale(
                 [
                     colors.base00,
@@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => {
                     colors.base07,
                 ].reverse()
             ),
-            red: colorRamp(chroma(colors.base08)),
-            orange: colorRamp(chroma(colors.base09)),
-            yellow: colorRamp(chroma(colors.base0A)),
-            green: colorRamp(chroma(colors.base0B)),
-            cyan: colorRamp(chroma(colors.base0C)),
-            blue: colorRamp(chroma(colors.base0D)),
-            violet: colorRamp(chroma(colors.base0E)),
-            magenta: colorRamp(chroma(colors.base0F)),
+            red: color_ramp(chroma(colors.base08)),
+            orange: color_ramp(chroma(colors.base09)),
+            yellow: color_ramp(chroma(colors.base0A)),
+            green: color_ramp(chroma(colors.base0B)),
+            cyan: color_ramp(chroma(colors.base0C)),
+            blue: color_ramp(chroma(colors.base0D)),
+            violet: color_ramp(chroma(colors.base0E)),
+            magenta: color_ramp(chroma(colors.base0F)),
         },
         override: { syntax },
     }
 }
 
-export const theme = getTheme(variant)
+export const theme = get_theme(variant)

styles/src/themes/atelier/common.ts πŸ”—

@@ -24,12 +24,12 @@ export interface Variant {
 export const meta: ThemeFamilyMeta = {
     name: "Atelier",
     author: "Bram de Haan (http://atelierbramdehaan.nl)",
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl:
+    license_type: ThemeLicenseType.MIT,
+    license_url:
         "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/",
 }
 
-export const buildSyntax = (variant: Variant): ThemeSyntax => {
+export const build_syntax = (variant: Variant): ThemeSyntax => {
     const { colors } = variant
     return {
         primary: { color: colors.base06 },

styles/src/themes/ayu/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/ayu/ayu-dark.ts πŸ”—

@@ -1,16 +1,16 @@
 import { ThemeAppearance, ThemeConfig } from "../../common"
-import { ayu, meta, buildTheme } from "./common"
+import { ayu, meta, build_theme } from "./common"
 
 const variant = ayu.dark
-const { ramps, syntax } = buildTheme(variant, false)
+const { ramps, syntax } = build_theme(variant, false)
 
 export const theme: ThemeConfig = {
     name: `${meta.name} Dark`,
     author: meta.author,
     appearance: ThemeAppearance.Dark,
-    licenseType: meta.licenseType,
-    licenseUrl: meta.licenseUrl,
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: ramps,
+    license_type: meta.license_type,
+    license_url: meta.license_url,
+    license_file: `${__dirname}/LICENSE`,
+    input_color: ramps,
     override: { syntax },
 }

styles/src/themes/ayu/ayu-light.ts πŸ”—

@@ -1,16 +1,16 @@
 import { ThemeAppearance, ThemeConfig } from "../../common"
-import { ayu, meta, buildTheme } from "./common"
+import { ayu, meta, build_theme } from "./common"
 
 const variant = ayu.light
-const { ramps, syntax } = buildTheme(variant, true)
+const { ramps, syntax } = build_theme(variant, true)
 
 export const theme: ThemeConfig = {
     name: `${meta.name} Light`,
     author: meta.author,
     appearance: ThemeAppearance.Light,
-    licenseType: meta.licenseType,
-    licenseUrl: meta.licenseUrl,
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: ramps,
+    license_type: meta.license_type,
+    license_url: meta.license_url,
+    license_file: `${__dirname}/LICENSE`,
+    input_color: ramps,
     override: { syntax },
 }

styles/src/themes/ayu/ayu-mirage.ts πŸ”—

@@ -1,16 +1,16 @@
 import { ThemeAppearance, ThemeConfig } from "../../common"
-import { ayu, meta, buildTheme } from "./common"
+import { ayu, meta, build_theme } from "./common"
 
 const variant = ayu.mirage
-const { ramps, syntax } = buildTheme(variant, false)
+const { ramps, syntax } = build_theme(variant, false)
 
 export const theme: ThemeConfig = {
     name: `${meta.name} Mirage`,
     author: meta.author,
     appearance: ThemeAppearance.Dark,
-    licenseType: meta.licenseType,
-    licenseUrl: meta.licenseUrl,
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: ramps,
+    license_type: meta.license_type,
+    license_url: meta.license_url,
+    license_file: `${__dirname}/LICENSE`,
+    input_color: ramps,
     override: { syntax },
 }

styles/src/themes/ayu/common.ts πŸ”—

@@ -1,7 +1,7 @@
 import { dark, light, mirage } from "ayu"
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeLicenseType,
     ThemeSyntax,
     ThemeFamilyMeta,
@@ -13,16 +13,16 @@ export const ayu = {
     mirage,
 }
 
-export const buildTheme = (t: typeof dark, light: boolean) => {
+export const build_theme = (t: typeof dark, light: boolean) => {
     const color = {
-        lightBlue: t.syntax.tag.hex(),
+        light_blue: t.syntax.tag.hex(),
         yellow: t.syntax.func.hex(),
         blue: t.syntax.entity.hex(),
         green: t.syntax.string.hex(),
         teal: t.syntax.regexp.hex(),
         red: t.syntax.markup.hex(),
         orange: t.syntax.keyword.hex(),
-        lightYellow: t.syntax.special.hex(),
+        light_yellow: t.syntax.special.hex(),
         gray: t.syntax.comment.hex(),
         purple: t.syntax.constant.hex(),
     }
@@ -48,20 +48,20 @@ export const buildTheme = (t: typeof dark, light: boolean) => {
                 light ? t.editor.fg.hex() : t.editor.bg.hex(),
                 light ? t.editor.bg.hex() : t.editor.fg.hex(),
             ]),
-            red: colorRamp(chroma(color.red)),
-            orange: colorRamp(chroma(color.orange)),
-            yellow: colorRamp(chroma(color.yellow)),
-            green: colorRamp(chroma(color.green)),
-            cyan: colorRamp(chroma(color.teal)),
-            blue: colorRamp(chroma(color.blue)),
-            violet: colorRamp(chroma(color.purple)),
-            magenta: colorRamp(chroma(color.lightBlue)),
+            red: color_ramp(chroma(color.red)),
+            orange: color_ramp(chroma(color.orange)),
+            yellow: color_ramp(chroma(color.yellow)),
+            green: color_ramp(chroma(color.green)),
+            cyan: color_ramp(chroma(color.teal)),
+            blue: color_ramp(chroma(color.blue)),
+            violet: color_ramp(chroma(color.purple)),
+            magenta: color_ramp(chroma(color.light_blue)),
         },
         syntax,
     }
 }
 
-export const buildSyntax = (t: typeof dark): ThemeSyntax => {
+export const build_syntax = (t: typeof dark): ThemeSyntax => {
     return {
         constant: { color: t.syntax.constant.hex() },
         "string.regex": { color: t.syntax.regexp.hex() },
@@ -80,6 +80,6 @@ export const buildSyntax = (t: typeof dark): ThemeSyntax => {
 export const meta: ThemeFamilyMeta = {
     name: "Ayu",
     author: "dempfi",
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/dempfi/ayu",
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/dempfi/ayu",
 }

styles/src/themes/gruvbox/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/gruvbox/gruvbox-common.ts πŸ”—

@@ -1,6 +1,6 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -11,8 +11,8 @@ import {
 const meta: ThemeFamilyMeta = {
     name: "Gruvbox",
     author: "morhetz <morhetz@gmail.com>",
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/morhetz/gruvbox",
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/morhetz/gruvbox",
 }
 
 const color = {
@@ -73,7 +73,7 @@ interface ThemeColors {
     gray: string
 }
 
-const darkNeutrals = [
+const dark_neutrals = [
     color.dark1,
     color.dark2,
     color.dark3,
@@ -96,7 +96,7 @@ const dark: ThemeColors = {
     gray: color.light4,
 }
 
-const lightNeutrals = [
+const light_neutrals = [
     color.light1,
     color.light2,
     color.light3,
@@ -119,14 +119,6 @@ const light: ThemeColors = {
     gray: color.dark4,
 }
 
-const darkHardNeutral = [color.dark0_hard, ...darkNeutrals]
-const darkNeutral = [color.dark0, ...darkNeutrals]
-const darkSoftNeutral = [color.dark0_soft, ...darkNeutrals]
-
-const lightHardNeutral = [color.light0_hard, ...lightNeutrals]
-const lightNeutral = [color.light0, ...lightNeutrals]
-const lightSoftNeutral = [color.light0_soft, ...lightNeutrals]
-
 interface Variant {
     name: string
     appearance: "light" | "dark"
@@ -167,61 +159,68 @@ const variant: Variant[] = [
     },
 ]
 
-const buildVariant = (variant: Variant): ThemeConfig => {
+const dark_hard_neutral = [color.dark0_hard, ...dark_neutrals]
+const dark_neutral = [color.dark0, ...dark_neutrals]
+const dark_soft_neutral = [color.dark0_soft, ...dark_neutrals]
+
+const light_hard_neutral = [color.light0_hard, ...light_neutrals]
+const light_neutral = [color.light0, ...light_neutrals]
+const light_soft_neutral = [color.light0_soft, ...light_neutrals]
+
+const build_variant = (variant: Variant): ThemeConfig => {
     const { colors } = variant
 
     const name = `Gruvbox ${variant.name}`
 
-    const isLight = variant.appearance === "light"
+    const is_light = variant.appearance === "light"
 
     let neutral: string[] = []
 
     switch (variant.name) {
-        case "Dark Hard": {
-            neutral = darkHardNeutral
+        case "Dark Hard":
+            neutral = dark_hard_neutral
             break
-        }
-        case "Dark": {
-            neutral = darkNeutral
+
+        case "Dark":
+            neutral = dark_neutral
             break
-        }
-        case "Dark Soft": {
-            neutral = darkSoftNeutral
+
+        case "Dark Soft":
+            neutral = dark_soft_neutral
             break
-        }
-        case "Light Hard": {
-            neutral = lightHardNeutral
+
+        case "Light Hard":
+            neutral = light_hard_neutral
             break
-        }
-        case "Light": {
-            neutral = lightNeutral
+
+        case "Light":
+            neutral = light_neutral
             break
-        }
-        case "Light Soft": {
-            neutral = lightSoftNeutral
+
+        case "Light Soft":
+            neutral = light_soft_neutral
             break
-        }
     }
 
     const ramps = {
-        neutral: chroma.scale(isLight ? neutral.reverse() : neutral),
-        red: colorRamp(chroma(variant.colors.red)),
-        orange: colorRamp(chroma(variant.colors.orange)),
-        yellow: colorRamp(chroma(variant.colors.yellow)),
-        green: colorRamp(chroma(variant.colors.green)),
-        cyan: colorRamp(chroma(variant.colors.aqua)),
-        blue: colorRamp(chroma(variant.colors.blue)),
-        violet: colorRamp(chroma(variant.colors.purple)),
-        magenta: colorRamp(chroma(variant.colors.gray)),
+        neutral: chroma.scale(is_light ? neutral.reverse() : neutral),
+        red: color_ramp(chroma(variant.colors.red)),
+        orange: color_ramp(chroma(variant.colors.orange)),
+        yellow: color_ramp(chroma(variant.colors.yellow)),
+        green: color_ramp(chroma(variant.colors.green)),
+        cyan: color_ramp(chroma(variant.colors.aqua)),
+        blue: color_ramp(chroma(variant.colors.blue)),
+        violet: color_ramp(chroma(variant.colors.purple)),
+        magenta: color_ramp(chroma(variant.colors.gray)),
     }
 
     const syntax: ThemeSyntax = {
-        primary: { color: neutral[isLight ? 0 : 8] },
+        primary: { color: neutral[is_light ? 0 : 8] },
         "text.literal": { color: colors.blue },
         comment: { color: colors.gray },
-        punctuation: { color: neutral[isLight ? 1 : 7] },
-        "punctuation.bracket": { color: neutral[isLight ? 3 : 5] },
-        "punctuation.list_marker": { color: neutral[isLight ? 0 : 8] },
+        punctuation: { color: neutral[is_light ? 1 : 7] },
+        "punctuation.bracket": { color: neutral[is_light ? 3 : 5] },
+        "punctuation.list_marker": { color: neutral[is_light ? 0 : 8] },
         operator: { color: colors.aqua },
         boolean: { color: colors.purple },
         number: { color: colors.purple },
@@ -237,10 +236,10 @@ const buildVariant = (variant: Variant): ThemeConfig => {
         function: { color: colors.green },
         "function.builtin": { color: colors.red },
         variable: { color: colors.blue },
-        property: { color: neutral[isLight ? 0 : 8] },
+        property: { color: neutral[is_light ? 0 : 8] },
         embedded: { color: colors.aqua },
-        linkText: { color: colors.aqua },
-        linkUri: { color: colors.purple },
+        link_text: { color: colors.aqua },
+        link_uri: { color: colors.purple },
         title: { color: colors.green },
     }
 
@@ -248,18 +247,18 @@ const buildVariant = (variant: Variant): ThemeConfig => {
         name,
         author: meta.author,
         appearance: variant.appearance as ThemeAppearance,
-        licenseType: meta.licenseType,
-        licenseUrl: meta.licenseUrl,
-        licenseFile: `${__dirname}/LICENSE`,
-        inputColor: ramps,
+        license_type: meta.license_type,
+        license_url: meta.license_url,
+        license_file: `${__dirname}/LICENSE`,
+        input_color: ramps,
         override: { syntax },
     }
 }
 
 // Variants
-export const darkHard = buildVariant(variant[0])
-export const darkDefault = buildVariant(variant[1])
-export const darkSoft = buildVariant(variant[2])
-export const lightHard = buildVariant(variant[3])
-export const lightDefault = buildVariant(variant[4])
-export const lightSoft = buildVariant(variant[5])
+export const dark_hard = build_variant(variant[0])
+export const dark_default = build_variant(variant[1])
+export const dark_soft = build_variant(variant[2])
+export const light_hard = build_variant(variant[3])
+export const light_default = build_variant(variant[4])
+export const light_soft = build_variant(variant[5])

styles/src/themes/index.ts πŸ”—

@@ -1,82 +1,82 @@
 import { ThemeConfig } from "../theme"
-import { darkDefault as gruvboxDark } from "./gruvbox/gruvbox-dark"
-import { darkHard as gruvboxDarkHard } from "./gruvbox/gruvbox-dark-hard"
-import { darkSoft as gruvboxDarkSoft } from "./gruvbox/gruvbox-dark-soft"
-import { lightDefault as gruvboxLight } from "./gruvbox/gruvbox-light"
-import { lightHard as gruvboxLightHard } from "./gruvbox/gruvbox-light-hard"
-import { lightSoft as gruvboxLightSoft } from "./gruvbox/gruvbox-light-soft"
-import { dark as solarizedDark } from "./solarized/solarized"
-import { light as solarizedLight } from "./solarized/solarized"
-import { dark as andromedaDark } from "./andromeda/andromeda"
-import { theme as oneDark } from "./one/one-dark"
-import { theme as oneLight } from "./one/one-light"
-import { theme as ayuLight } from "./ayu/ayu-light"
-import { theme as ayuDark } from "./ayu/ayu-dark"
-import { theme as ayuMirage } from "./ayu/ayu-mirage"
-import { theme as rosePine } from "./rose-pine/rose-pine"
-import { theme as rosePineDawn } from "./rose-pine/rose-pine-dawn"
-import { theme as rosePineMoon } from "./rose-pine/rose-pine-moon"
+import { dark_default as gruvbox_dark } from "./gruvbox/gruvbox-dark"
+import { dark_hard as gruvbox_dark_hard } from "./gruvbox/gruvbox-dark-hard"
+import { dark_soft as gruvbox_dark_soft } from "./gruvbox/gruvbox-dark-soft"
+import { light_default as gruvbox_light } from "./gruvbox/gruvbox-light"
+import { light_hard as gruvbox_light_hard } from "./gruvbox/gruvbox-light-hard"
+import { light_soft as gruvbox_light_soft } from "./gruvbox/gruvbox-light-soft"
+import { dark as solarized_dark } from "./solarized/solarized"
+import { light as solarized_light } from "./solarized/solarized"
+import { dark as andromeda_dark } from "./andromeda/andromeda"
+import { theme as one_dark } from "./one/one-dark"
+import { theme as one_light } from "./one/one-light"
+import { theme as ayu_light } from "./ayu/ayu-light"
+import { theme as ayu_dark } from "./ayu/ayu-dark"
+import { theme as ayu_mirage } from "./ayu/ayu-mirage"
+import { theme as rose_pine } from "./rose-pine/rose-pine"
+import { theme as rose_pine_dawn } from "./rose-pine/rose-pine-dawn"
+import { theme as rose_pine_moon } from "./rose-pine/rose-pine-moon"
 import { theme as sandcastle } from "./sandcastle/sandcastle"
 import { theme as summercamp } from "./summercamp/summercamp"
-import { theme as atelierCaveDark } from "./atelier/atelier-cave-dark"
-import { theme as atelierCaveLight } from "./atelier/atelier-cave-light"
-import { theme as atelierDuneDark } from "./atelier/atelier-dune-dark"
-import { theme as atelierDuneLight } from "./atelier/atelier-dune-light"
-import { theme as atelierEstuaryDark } from "./atelier/atelier-estuary-dark"
-import { theme as atelierEstuaryLight } from "./atelier/atelier-estuary-light"
-import { theme as atelierForestDark } from "./atelier/atelier-forest-dark"
-import { theme as atelierForestLight } from "./atelier/atelier-forest-light"
-import { theme as atelierHeathDark } from "./atelier/atelier-heath-dark"
-import { theme as atelierHeathLight } from "./atelier/atelier-heath-light"
-import { theme as atelierLakesideDark } from "./atelier/atelier-lakeside-dark"
-import { theme as atelierLakesideLight } from "./atelier/atelier-lakeside-light"
-import { theme as atelierPlateauDark } from "./atelier/atelier-plateau-dark"
-import { theme as atelierPlateauLight } from "./atelier/atelier-plateau-light"
-import { theme as atelierSavannaDark } from "./atelier/atelier-savanna-dark"
-import { theme as atelierSavannaLight } from "./atelier/atelier-savanna-light"
-import { theme as atelierSeasideDark } from "./atelier/atelier-seaside-dark"
-import { theme as atelierSeasideLight } from "./atelier/atelier-seaside-light"
-import { theme as atelierSulphurpoolDark } from "./atelier/atelier-sulphurpool-dark"
-import { theme as atelierSulphurpoolLight } from "./atelier/atelier-sulphurpool-light"
+import { theme as atelier_cave_dark } from "./atelier/atelier-cave-dark"
+import { theme as atelier_cave_light } from "./atelier/atelier-cave-light"
+import { theme as atelier_dune_dark } from "./atelier/atelier-dune-dark"
+import { theme as atelier_dune_light } from "./atelier/atelier-dune-light"
+import { theme as atelier_estuary_dark } from "./atelier/atelier-estuary-dark"
+import { theme as atelier_estuary_light } from "./atelier/atelier-estuary-light"
+import { theme as atelier_forest_dark } from "./atelier/atelier-forest-dark"
+import { theme as atelier_forest_light } from "./atelier/atelier-forest-light"
+import { theme as atelier_heath_dark } from "./atelier/atelier-heath-dark"
+import { theme as atelier_heath_light } from "./atelier/atelier-heath-light"
+import { theme as atelier_lakeside_dark } from "./atelier/atelier-lakeside-dark"
+import { theme as atelier_lakeside_light } from "./atelier/atelier-lakeside-light"
+import { theme as atelier_plateau_dark } from "./atelier/atelier-plateau-dark"
+import { theme as atelier_plateau_light } from "./atelier/atelier-plateau-light"
+import { theme as atelier_savanna_dark } from "./atelier/atelier-savanna-dark"
+import { theme as atelier_savanna_light } from "./atelier/atelier-savanna-light"
+import { theme as atelier_seaside_dark } from "./atelier/atelier-seaside-dark"
+import { theme as atelier_seaside_light } from "./atelier/atelier-seaside-light"
+import { theme as atelier_sulphurpool_dark } from "./atelier/atelier-sulphurpool-dark"
+import { theme as atelier_sulphurpool_light } from "./atelier/atelier-sulphurpool-light"
 
 export const themes: ThemeConfig[] = [
-    oneDark,
-    oneLight,
-    ayuLight,
-    ayuDark,
-    ayuMirage,
-    gruvboxDark,
-    gruvboxDarkHard,
-    gruvboxDarkSoft,
-    gruvboxLight,
-    gruvboxLightHard,
-    gruvboxLightSoft,
-    rosePine,
-    rosePineDawn,
-    rosePineMoon,
+    one_dark,
+    one_light,
+    ayu_light,
+    ayu_dark,
+    ayu_mirage,
+    gruvbox_dark,
+    gruvbox_dark_hard,
+    gruvbox_dark_soft,
+    gruvbox_light,
+    gruvbox_light_hard,
+    gruvbox_light_soft,
+    rose_pine,
+    rose_pine_dawn,
+    rose_pine_moon,
     sandcastle,
-    solarizedDark,
-    solarizedLight,
-    andromedaDark,
+    solarized_dark,
+    solarized_light,
+    andromeda_dark,
     summercamp,
-    atelierCaveDark,
-    atelierCaveLight,
-    atelierDuneDark,
-    atelierDuneLight,
-    atelierEstuaryDark,
-    atelierEstuaryLight,
-    atelierForestDark,
-    atelierForestLight,
-    atelierHeathDark,
-    atelierHeathLight,
-    atelierLakesideDark,
-    atelierLakesideLight,
-    atelierPlateauDark,
-    atelierPlateauLight,
-    atelierSavannaDark,
-    atelierSavannaLight,
-    atelierSeasideDark,
-    atelierSeasideLight,
-    atelierSulphurpoolDark,
-    atelierSulphurpoolLight,
+    atelier_cave_dark,
+    atelier_cave_light,
+    atelier_dune_dark,
+    atelier_dune_light,
+    atelier_estuary_dark,
+    atelier_estuary_light,
+    atelier_forest_dark,
+    atelier_forest_light,
+    atelier_heath_dark,
+    atelier_heath_light,
+    atelier_lakeside_dark,
+    atelier_lakeside_light,
+    atelier_plateau_dark,
+    atelier_plateau_light,
+    atelier_savanna_dark,
+    atelier_savanna_light,
+    atelier_seaside_dark,
+    atelier_seaside_light,
+    atelier_sulphurpool_dark,
+    atelier_sulphurpool_light,
 ]

styles/src/themes/one/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/one/one-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import {
     chroma,
-    fontWeights,
-    colorRamp,
+    font_weights,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -11,7 +11,7 @@ const color = {
     white: "#ACB2BE",
     grey: "#5D636F",
     red: "#D07277",
-    darkRed: "#B1574B",
+    dark_red: "#B1574B",
     orange: "#C0966B",
     yellow: "#DFC184",
     green: "#A1C181",
@@ -24,10 +24,11 @@ export const theme: ThemeConfig = {
     name: "One Dark",
     author: "simurai",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url:
+        "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma
             .scale([
                 "#282c34",
@@ -40,14 +41,14 @@ export const theme: ThemeConfig = {
                 "#c8ccd4",
             ])
             .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
-        red: colorRamp(chroma(color.red)),
-        orange: colorRamp(chroma(color.orange)),
-        yellow: colorRamp(chroma(color.yellow)),
-        green: colorRamp(chroma(color.green)),
-        cyan: colorRamp(chroma(color.teal)),
-        blue: colorRamp(chroma(color.blue)),
-        violet: colorRamp(chroma(color.purple)),
-        magenta: colorRamp(chroma("#be5046")),
+        red: color_ramp(chroma(color.red)),
+        orange: color_ramp(chroma(color.orange)),
+        yellow: color_ramp(chroma(color.yellow)),
+        green: color_ramp(chroma(color.green)),
+        cyan: color_ramp(chroma(color.teal)),
+        blue: color_ramp(chroma(color.blue)),
+        violet: color_ramp(chroma(color.purple)),
+        magenta: color_ramp(chroma("#be5046")),
     },
     override: {
         syntax: {
@@ -57,8 +58,8 @@ export const theme: ThemeConfig = {
             "emphasis.strong": { color: color.orange },
             function: { color: color.blue },
             keyword: { color: color.purple },
-            linkText: { color: color.blue, italic: false },
-            linkUri: { color: color.teal },
+            link_text: { color: color.blue, italic: false },
+            link_uri: { color: color.teal },
             number: { color: color.orange },
             constant: { color: color.yellow },
             operator: { color: color.teal },
@@ -66,9 +67,9 @@ export const theme: ThemeConfig = {
             property: { color: color.red },
             punctuation: { color: color.white },
             "punctuation.list_marker": { color: color.red },
-            "punctuation.special": { color: color.darkRed },
+            "punctuation.special": { color: color.dark_red },
             string: { color: color.green },
-            title: { color: color.red, weight: fontWeights.normal },
+            title: { color: color.red, weight: font_weights.normal },
             "text.literal": { color: color.green },
             type: { color: color.teal },
             "variable.special": { color: color.orange },

styles/src/themes/one/one-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import {
     chroma,
-    fontWeights,
-    colorRamp,
+    font_weights,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -11,7 +11,7 @@ const color = {
     black: "#383A41",
     grey: "#A2A3A7",
     red: "#D36050",
-    darkRed: "#B92C46",
+    dark_red: "#B92C46",
     orange: "#AD6F26",
     yellow: "#DFC184",
     green: "#659F58",
@@ -25,11 +25,11 @@ export const theme: ThemeConfig = {
     name: "One Light",
     author: "simurai",
     appearance: ThemeAppearance.Light,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl:
+    license_type: ThemeLicenseType.MIT,
+    license_url:
         "https://github.com/atom/atom/tree/master/packages/one-light-ui",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma
             .scale([
                 "#383A41",
@@ -42,14 +42,14 @@ export const theme: ThemeConfig = {
                 "#FAFAFA",
             ])
             .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]),
-        red: colorRamp(chroma(color.red)),
-        orange: colorRamp(chroma(color.orange)),
-        yellow: colorRamp(chroma(color.yellow)),
-        green: colorRamp(chroma(color.green)),
-        cyan: colorRamp(chroma(color.teal)),
-        blue: colorRamp(chroma(color.blue)),
-        violet: colorRamp(chroma(color.purple)),
-        magenta: colorRamp(chroma(color.magenta)),
+        red: color_ramp(chroma(color.red)),
+        orange: color_ramp(chroma(color.orange)),
+        yellow: color_ramp(chroma(color.yellow)),
+        green: color_ramp(chroma(color.green)),
+        cyan: color_ramp(chroma(color.teal)),
+        blue: color_ramp(chroma(color.blue)),
+        violet: color_ramp(chroma(color.purple)),
+        magenta: color_ramp(chroma(color.magenta)),
     },
     override: {
         syntax: {
@@ -59,17 +59,17 @@ export const theme: ThemeConfig = {
             "emphasis.strong": { color: color.orange },
             function: { color: color.blue },
             keyword: { color: color.purple },
-            linkText: { color: color.blue },
-            linkUri: { color: color.teal },
+            link_text: { color: color.blue },
+            link_uri: { color: color.teal },
             number: { color: color.orange },
             operator: { color: color.teal },
             primary: { color: color.black },
             property: { color: color.red },
             punctuation: { color: color.black },
             "punctuation.list_marker": { color: color.red },
-            "punctuation.special": { color: color.darkRed },
+            "punctuation.special": { color: color.dark_red },
             string: { color: color.green },
-            title: { color: color.red, weight: fontWeights.normal },
+            title: { color: color.red, weight: font_weights.normal },
             "text.literal": { color: color.green },
             type: { color: color.teal },
             "variable.special": { color: color.orange },

styles/src/themes/rose-pine/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/rose-pine/common.ts πŸ”—

@@ -0,0 +1,75 @@
+import { ThemeSyntax } from "../../common"
+
+export const color = {
+    default: {
+        base: "#191724",
+        surface: "#1f1d2e",
+        overlay: "#26233a",
+        muted: "#6e6a86",
+        subtle: "#908caa",
+        text: "#e0def4",
+        love: "#eb6f92",
+        gold: "#f6c177",
+        rose: "#ebbcba",
+        pine: "#31748f",
+        foam: "#9ccfd8",
+        iris: "#c4a7e7",
+        highlight_low: "#21202e",
+        highlight_med: "#403d52",
+        highlight_high: "#524f67",
+    },
+    moon: {
+        base: "#232136",
+        surface: "#2a273f",
+        overlay: "#393552",
+        muted: "#6e6a86",
+        subtle: "#908caa",
+        text: "#e0def4",
+        love: "#eb6f92",
+        gold: "#f6c177",
+        rose: "#ea9a97",
+        pine: "#3e8fb0",
+        foam: "#9ccfd8",
+        iris: "#c4a7e7",
+        highlight_low: "#2a283e",
+        highlight_med: "#44415a",
+        highlight_high: "#56526e",
+    },
+    dawn: {
+        base: "#faf4ed",
+        surface: "#fffaf3",
+        overlay: "#f2e9e1",
+        muted: "#9893a5",
+        subtle: "#797593",
+        text: "#575279",
+        love: "#b4637a",
+        gold: "#ea9d34",
+        rose: "#d7827e",
+        pine: "#286983",
+        foam: "#56949f",
+        iris: "#907aa9",
+        highlight_low: "#f4ede8",
+        highlight_med: "#dfdad9",
+        highlight_high: "#cecacd",
+    },
+}
+
+export const syntax = (c: typeof color.default): Partial<ThemeSyntax> => {
+    return {
+        comment: { color: c.muted },
+        operator: { color: c.pine },
+        punctuation: { color: c.subtle },
+        variable: { color: c.text },
+        string: { color: c.gold },
+        type: { color: c.foam },
+        "type.builtin": { color: c.foam },
+        boolean: { color: c.rose },
+        function: { color: c.rose },
+        keyword: { color: c.pine },
+        tag: { color: c.foam },
+        "function.method": { color: c.rose },
+        title: { color: c.gold },
+        link_text: { color: c.foam, italic: false },
+        link_uri: { color: c.rose },
+    }
+}

styles/src/themes/rose-pine/rose-pine-dawn.ts πŸ”—

@@ -1,39 +1,49 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
 } from "../../common"
 
+import { color as c, syntax } from "./common"
+
+const color = c.dawn
+
+const green = chroma.mix(color.foam, "#10b981", 0.6, "lab")
+const magenta = chroma.mix(color.love, color.pine, 0.5, "lab")
+
 export const theme: ThemeConfig = {
     name: "RosΓ© Pine Dawn",
     author: "edunfelt",
     appearance: ThemeAppearance.Light,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma
-            .scale([
-                "#575279",
-                "#797593",
-                "#9893A5",
-                "#B5AFB8",
-                "#D3CCCC",
-                "#F2E9E1",
-                "#FFFAF3",
-                "#FAF4ED",
-            ])
+            .scale(
+                [
+                    color.base,
+                    color.surface,
+                    color.highlight_high,
+                    color.overlay,
+                    color.muted,
+                    color.subtle,
+                    color.text,
+                ].reverse()
+            )
             .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
-        red: colorRamp(chroma("#B4637A")),
-        orange: colorRamp(chroma("#D7827E")),
-        yellow: colorRamp(chroma("#EA9D34")),
-        green: colorRamp(chroma("#679967")),
-        cyan: colorRamp(chroma("#286983")),
-        blue: colorRamp(chroma("#56949F")),
-        violet: colorRamp(chroma("#907AA9")),
-        magenta: colorRamp(chroma("#79549F")),
+        red: color_ramp(chroma(color.love)),
+        orange: color_ramp(chroma(color.iris)),
+        yellow: color_ramp(chroma(color.gold)),
+        green: color_ramp(chroma(green)),
+        cyan: color_ramp(chroma(color.pine)),
+        blue: color_ramp(chroma(color.foam)),
+        violet: color_ramp(chroma(color.iris)),
+        magenta: color_ramp(chroma(magenta)),
+    },
+    override: {
+        syntax: syntax(color),
     },
-    override: { syntax: {} },
 }

styles/src/themes/rose-pine/rose-pine-moon.ts πŸ”—

@@ -1,39 +1,47 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
 } from "../../common"
 
+import { color as c, syntax } from "./common"
+
+const color = c.moon
+
+const green = chroma.mix(color.foam, "#10b981", 0.6, "lab")
+const magenta = chroma.mix(color.love, color.pine, 0.5, "lab")
+
 export const theme: ThemeConfig = {
     name: "RosΓ© Pine Moon",
     author: "edunfelt",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma
             .scale([
-                "#232136",
-                "#2A273F",
-                "#393552",
-                "#3E3A53",
-                "#56526C",
-                "#6E6A86",
-                "#908CAA",
-                "#E0DEF4",
+                color.base,
+                color.surface,
+                color.highlight_high,
+                color.overlay,
+                color.muted,
+                color.subtle,
+                color.text,
             ])
             .domain([0, 0.3, 0.55, 1]),
-        red: colorRamp(chroma("#EB6F92")),
-        orange: colorRamp(chroma("#EBBCBA")),
-        yellow: colorRamp(chroma("#F6C177")),
-        green: colorRamp(chroma("#8DBD8D")),
-        cyan: colorRamp(chroma("#409BBE")),
-        blue: colorRamp(chroma("#9CCFD8")),
-        violet: colorRamp(chroma("#C4A7E7")),
-        magenta: colorRamp(chroma("#AB6FE9")),
+        red: color_ramp(chroma(color.love)),
+        orange: color_ramp(chroma(color.iris)),
+        yellow: color_ramp(chroma(color.gold)),
+        green: color_ramp(chroma(green)),
+        cyan: color_ramp(chroma(color.pine)),
+        blue: color_ramp(chroma(color.foam)),
+        violet: color_ramp(chroma(color.iris)),
+        magenta: color_ramp(chroma(magenta)),
+    },
+    override: {
+        syntax: syntax(color),
     },
-    override: { syntax: {} },
 }

styles/src/themes/rose-pine/rose-pine.ts πŸ”—

@@ -1,37 +1,44 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
 } from "../../common"
+import { color as c, syntax } from "./common"
+
+const color = c.default
+
+const green = chroma.mix(color.foam, "#10b981", 0.6, "lab")
+const magenta = chroma.mix(color.love, color.pine, 0.5, "lab")
 
 export const theme: ThemeConfig = {
     name: "RosΓ© Pine",
     author: "edunfelt",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/edunfelt/base16-rose-pine-scheme",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma.scale([
-            "#191724",
-            "#1f1d2e",
-            "#26233A",
-            "#3E3A53",
-            "#56526C",
-            "#6E6A86",
-            "#908CAA",
-            "#E0DEF4",
+            color.base,
+            color.surface,
+            color.highlight_high,
+            color.overlay,
+            color.muted,
+            color.subtle,
+            color.text,
         ]),
-        red: colorRamp(chroma("#EB6F92")),
-        orange: colorRamp(chroma("#EBBCBA")),
-        yellow: colorRamp(chroma("#F6C177")),
-        green: colorRamp(chroma("#8DBD8D")),
-        cyan: colorRamp(chroma("#409BBE")),
-        blue: colorRamp(chroma("#9CCFD8")),
-        violet: colorRamp(chroma("#C4A7E7")),
-        magenta: colorRamp(chroma("#AB6FE9")),
+        red: color_ramp(chroma(color.love)),
+        orange: color_ramp(chroma(color.iris)),
+        yellow: color_ramp(chroma(color.gold)),
+        green: color_ramp(chroma(green)),
+        cyan: color_ramp(chroma(color.pine)),
+        blue: color_ramp(chroma(color.foam)),
+        violet: color_ramp(chroma(color.iris)),
+        magenta: color_ramp(chroma(magenta)),
+    },
+    override: {
+        syntax: syntax(color),
     },
-    override: { syntax: {} },
 }

styles/src/themes/sandcastle/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/sandcastle/sandcastle.ts πŸ”—

@@ -1,6 +1,6 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -10,10 +10,10 @@ export const theme: ThemeConfig = {
     name: "Sandcastle",
     author: "gessig",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/gessig/base16-sandcastle-scheme",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/gessig/base16-sandcastle-scheme",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma.scale([
             "#282c34",
             "#2c323b",
@@ -24,14 +24,14 @@ export const theme: ThemeConfig = {
             "#d5c4a1",
             "#fdf4c1",
         ]),
-        red: colorRamp(chroma("#B4637A")),
-        orange: colorRamp(chroma("#a07e3b")),
-        yellow: colorRamp(chroma("#a07e3b")),
-        green: colorRamp(chroma("#83a598")),
-        cyan: colorRamp(chroma("#83a598")),
-        blue: colorRamp(chroma("#528b8b")),
-        violet: colorRamp(chroma("#d75f5f")),
-        magenta: colorRamp(chroma("#a87322")),
+        red: color_ramp(chroma("#B4637A")),
+        orange: color_ramp(chroma("#a07e3b")),
+        yellow: color_ramp(chroma("#a07e3b")),
+        green: color_ramp(chroma("#83a598")),
+        cyan: color_ramp(chroma("#83a598")),
+        blue: color_ramp(chroma("#528b8b")),
+        violet: color_ramp(chroma("#d75f5f")),
+        magenta: color_ramp(chroma("#a87322")),
     },
     override: { syntax: {} },
 }

styles/src/themes/solarized/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/solarized/solarized.ts πŸ”—

@@ -1,6 +1,6 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -19,24 +19,24 @@ const ramps = {
             "#fdf6e3",
         ])
         .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]),
-    red: colorRamp(chroma("#dc322f")),
-    orange: colorRamp(chroma("#cb4b16")),
-    yellow: colorRamp(chroma("#b58900")),
-    green: colorRamp(chroma("#859900")),
-    cyan: colorRamp(chroma("#2aa198")),
-    blue: colorRamp(chroma("#268bd2")),
-    violet: colorRamp(chroma("#6c71c4")),
-    magenta: colorRamp(chroma("#d33682")),
+    red: color_ramp(chroma("#dc322f")),
+    orange: color_ramp(chroma("#cb4b16")),
+    yellow: color_ramp(chroma("#b58900")),
+    green: color_ramp(chroma("#859900")),
+    cyan: color_ramp(chroma("#2aa198")),
+    blue: color_ramp(chroma("#268bd2")),
+    violet: color_ramp(chroma("#6c71c4")),
+    magenta: color_ramp(chroma("#d33682")),
 }
 
 export const dark: ThemeConfig = {
     name: "Solarized Dark",
     author: "Ethan Schoonover",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/altercation/solarized",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: ramps,
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/altercation/solarized",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: ramps,
     override: { syntax: {} },
 }
 
@@ -44,9 +44,9 @@ export const light: ThemeConfig = {
     name: "Solarized Light",
     author: "Ethan Schoonover",
     appearance: ThemeAppearance.Light,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/altercation/solarized",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: ramps,
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/altercation/solarized",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: ramps,
     override: { syntax: {} },
 }

styles/src/themes/summercamp/LICENSE πŸ”—

@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.

styles/src/themes/summercamp/summercamp.ts πŸ”—

@@ -1,6 +1,6 @@
 import {
     chroma,
-    colorRamp,
+    color_ramp,
     ThemeAppearance,
     ThemeLicenseType,
     ThemeConfig,
@@ -10,10 +10,10 @@ export const theme: ThemeConfig = {
     name: "Summercamp",
     author: "zoefiri",
     appearance: ThemeAppearance.Dark,
-    licenseType: ThemeLicenseType.MIT,
-    licenseUrl: "https://github.com/zoefiri/base16-sc",
-    licenseFile: `${__dirname}/LICENSE`,
-    inputColor: {
+    license_type: ThemeLicenseType.MIT,
+    license_url: "https://github.com/zoefiri/base16-sc",
+    license_file: `${__dirname}/LICENSE`,
+    input_color: {
         neutral: chroma
             .scale([
                 "#1c1810",
@@ -26,14 +26,14 @@ export const theme: ThemeConfig = {
                 "#f8f5de",
             ])
             .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]),
-        red: colorRamp(chroma("#e35142")),
-        orange: colorRamp(chroma("#fba11b")),
-        yellow: colorRamp(chroma("#f2ff27")),
-        green: colorRamp(chroma("#5ceb5a")),
-        cyan: colorRamp(chroma("#5aebbc")),
-        blue: colorRamp(chroma("#489bf0")),
-        violet: colorRamp(chroma("#FF8080")),
-        magenta: colorRamp(chroma("#F69BE7")),
+        red: color_ramp(chroma("#e35142")),
+        orange: color_ramp(chroma("#fba11b")),
+        yellow: color_ramp(chroma("#f2ff27")),
+        green: color_ramp(chroma("#5ceb5a")),
+        cyan: color_ramp(chroma("#5aebbc")),
+        blue: color_ramp(chroma("#489bf0")),
+        violet: color_ramp(chroma("#FF8080")),
+        magenta: color_ramp(chroma("#F69BE7")),
     },
     override: { syntax: {} },
 }

styles/src/utils/slugify.ts πŸ”—

@@ -1 +1,10 @@
-export function slugify(t: string): string { return t.toString().toLowerCase().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-').replace(/^-+/, '').replace(/-+$/, '') }
+export function slugify(t: string): string {
+    return t
+        .toString()
+        .toLowerCase()
+        .replace(/\s+/g, "-")
+        .replace(/[^\w-]+/g, "")
+        .replace(/--+/g, "-")
+        .replace(/^-+/, "")
+        .replace(/-+$/, "")
+}

styles/src/utils/snakeCase.ts πŸ”—

@@ -1,35 +0,0 @@
-import { snakeCase } from "case-anything"
-
-// https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case
-
-// Typescript magic to convert any string from camelCase to snake_case at compile time
-type SnakeCase<S> = S extends string
-    ? S extends `${infer T}${infer U}`
-        ? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}`
-        : S
-    : S
-
-type SnakeCased<Type> = {
-    [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
-}
-
-export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
-    const snakeObject: any = {}
-    for (const key in object) {
-        snakeObject[snakeCase(key, { keepSpecialCharacters: true })] =
-            snakeCaseValue(object[key])
-    }
-    return snakeObject
-}
-
-function snakeCaseValue(value: any): any {
-    if (typeof value === "object") {
-        if (Array.isArray(value)) {
-            return value.map(snakeCaseValue)
-        } else {
-            return snakeCaseTree(value)
-        }
-    } else {
-        return value
-    }
-}

styles/tsconfig.json πŸ”—

@@ -20,7 +20,11 @@
         "noFallthroughCasesInSwitch": false,
         "experimentalDecorators": true,
         "strictPropertyInitialization": false,
-        "skipLibCheck": true
+        "skipLibCheck": true,
+        "useUnknownInCatchVariables": false,
+        "baseUrl": "."
     },
-    "exclude": ["node_modules"]
+    "exclude": [
+        "node_modules"
+    ]
 }

styles/vitest.config.ts πŸ”—

@@ -0,0 +1,8 @@
+import { configDefaults, defineConfig } from "vitest/config"
+
+export default defineConfig({
+    test: {
+        exclude: [...configDefaults.exclude, "target/*"],
+        include: ["src/**/*.{spec,test}.ts"],
+    },
+})